IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> Android面试高频问题:UI绘制流程解析 -> 正文阅读

[移动开发]Android面试高频问题:UI绘制流程解析

一、View如何被添加到屏幕窗口

了解View如何被添加到屏幕窗口之前,先理解几个概念

  • Window:是一个抽象类,提供了绘制窗口的一组通过API
  • PhoneWindow: 是Window的唯一继承实现类,该类内部包含一个DecorView的对象,该DecorView对象是所有窗口(Actvivty)的根View
  • DecorView: 是PhoneWindow的内部类,是FrameLayout的子类,是对Framelayout进行功能的修饰(所以叫Decorxxx),是所有应用窗口的根View

以Activity为例(AppCompatActivity略有不同),我们的布局要被加载到窗口中,是通过onCrete方法调用setContentView(layoutResId)传入布局资源id,并经过一下三个过程

  1. 创建顶层布局容器DecorView
  2. 在顶层布局中加载基础布局ViewGroup
  3. 将setContentView(layoutResId)添加到基础布局中的FragmeLayout

涉及的主要实现类为ActivityPhoneWindow,主要代码实现过程如下

Activity.class

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}

PhoneWindow.class

@Override
public void setContentView(int layoutResID) {
    if (mContentParent == null) {
        // step1: 初始化mContentParent
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        //step6:把我们传入的layoutResID绘制成view,并作为mContentParent的子view
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    ...
}

private void installDecor() {
    mForceDecorInstall = false;
    if (mDecor == null) {
        //step1: 创建一个DecorView作为Activity的跟布局
        mDecor = generateDecor(-1);
        ...
    } else {
        mDecor.setWindow(this);
    }
    if (mContentParent == null) {
        //step2: 调用generateLayout创建系统给的基础布局
        mContentParent = generateLayout(mDecor);
    }
    ...
}

protected ViewGroup generateLayout(DecorView decor) {
    ...
    int layoutResource;
    int features = getLocalFeatures();
    ...
    else {
        //step3:根据设置的主题指定一个基础布局,这里以R.layout.screen_simple为例
        layoutResource = R.layout.screen_simple;
    }

    mDecor.startChanging();
    //step4: 把screen_simple绘制成view并add到DecorView中
    mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
    //step5: creen_simple为LinearLayout,其中包含一个id=content的FrameLayout的子view
    //作为后续用来承载我们设置的xml布局的父view
    ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
    return contentParent;
}

关注其中注释step1~6关键代码实现,完成了我们的资源布局加载到了窗口(DecorView)中,如上完成了View的加载过程,但是并没有确定View的坐标宽高等信息,下面就要对我们的View进行绘制以确定View的各类属性

二、View的绘制流程

2.1、绘制入口

ActivityThread.handlerResumeActivity()中调用wm.addView(),而这个wm是WindowManager的实现类为WindowManagerImpl

@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
        String reason) {
    final Activity a = r.activity;
    if (r.window == null && !a.mFinished && willBeVisible) {
        r.window = r.activity.getWindow();
        View decor = r.window.getDecorView();
        decor.setVisibility(View.INVISIBLE);
        //step1: 这里wm的实现类是WindowMamangerImpl
        ViewManager wm = a.getWindowManager();
          ...
        if (a.mVisibleFromClient) {
            if (!a.mWindowAdded) {
                a.mWindowAdded = true;
                //step2: 执行WindowManagerImpl.addView()
                wm.addView(decor, l);
            } else {
                a.onWindowAttributesChanged(l);
            }
        }
        ...
    } 
}

//关注a.getWindowManager(),调用的是Activity的getWindowManager(),而实际又是Window类个mWindowManager
WindowManager = mWindow.getWindowManager();

//而Window类的MWindowManager创建过程如下
public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
        boolean hardwareAccelerated) {
    ...
    if (wm == null) {
        wm = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
    }
    //返回的实现类型为WindowManagerImpl
    mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

确定了WindowManager实际类型为WindowManagerImpl后,继续跟进addView方法,调用过程如下 WindowManagerImpl.addView(decroView,layoutParams)进而通过调用WindowManagerGlobal.addView()方法,并创建ViewRootImpl,调用ViewRootImpl.setView(decorView,layoutParams,parentView),进而调用ViewRootImpl的requestLayout()->sheduleTraversals()->doTraversal()->并最终调用performTraversals(),调用顺序用图形表示如下

img

重点关注最后的performTraversals()方法,在performTraversals()中会依次调用如下三个方法去完成View绘制的关键三步,测量,摆放和绘制

  • performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
  • performLayout(lp, mWidth, mHeight)、
  • performDraw();

2.2、MeasureSpec

private void performMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    if (mView == null) {
        return;
    }
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "measure");
    try {
        mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    } finally {
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

performMeasure()方法调用view的measure()方法。并传入父容器的宽高MeasureSpec作为入参

MeasureSpace是View的一个静态内部类,代表一个 32 位 int 值,高 2 位代表测量模式 SpecMode,低 30 位代表规格大小 SpecSize,MeasureSpec 通过把 SpecMode 和 SpecSize 打包成一个 int 值避免过多的对象内存分配

主要实现如下,用来保存View的测量模式(SpecMode)和大小(SpecSize),并定义了三种测量模式

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK  = 0x3 << MODE_SHIFT;//11000000000000000000000000000000

    public static final int UNSPECIFIED = 0 << MODE_SHIFT;//00000000000000000000000000000000

    public static final int EXACTLY     = 1 << MODE_SHIFT;//01000000000000000000000000000000

    public static final int AT_MOST     = 2 << MODE_SHIFT;//10000000000000000000000000000000

    public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                      @MeasureSpecMode int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    @MeasureSpecMode
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }

    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
}

MeasureSpec定义的三种模式

  1. UNSPECIFIED:父容器对子View的大小不做约束,它的值为0左移30位(00000000000000000000000000000000)
  2. EXACTLY:父容器计算好了子View的具体宽高,子View的大小就是SpecSize,它的值为1左移30位(01000000000000000000000000000000)
  3. AT_MOST:父容器指定了一个可用大小,子View的大小不能超过这个大小,它的值为2左移30位(10000000000000000000000000000000)

通过makeMeasureSpec(int size,int mode)方法把size和mode组装到一个32位的int里面

(size & ~MODE_MASK) | (mode & MODE_MASK)
/*
其中MODE_MASK是0x3左移30位=11000000000000000000000000000000
(size & 00111111111111111111111111111111) | (mode & 11000000000000000000000000000000) 
size & 00111111111111111111111111111111 得到低30位
mode & 11000000000000000000000000000000 得到高2位
再把低30位和高两位取`与`操作,就完成了高 2 位代表测量模式 `SpecMode`,低 30 位代表规格大小 `SpecSize`
*/

2.3、绘制三大步骤

2.3.1、performMeasure - 测量

再来看View的测量过程performMeasure

public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int oWidth  = insets.left + insets.right;
        int oHeight = insets.top  + insets.bottom;
        widthMeasureSpec  = MeasureSpec.adjust(widthMeasureSpec,  optical ? -oWidth  : oWidth);
        heightMeasureSpec = MeasureSpec.adjust(heightMeasureSpec, optical ? -oHeight : oHeight);
    }

    // Suppress sign extension for the low bytes
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);

    final boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;

    // Optimize layout by avoiding an extra EXACTLY pass when the view is
    // already measured as the correct size. In API 23 and below, this
    // extra pass is required to make LinearLayout re-distribute weight.
    final boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
            || heightMeasureSpec != mOldHeightMeasureSpec;
    final boolean isSpecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
            && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
    final boolean matchesSpecSize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
            && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
    final boolean needsLayout = specChanged
            && (sAlwaysRemeasureExactly || !isSpecExactly || !matchesSpecSize);

    if (forceLayout || needsLayout) {
        // first clears the measured dimension flag
        mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;

        resolveRtlPropertiesIfNeeded();

        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            onMeasure(widthMeasureSpec, heightMeasureSpec);
            mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        } else {
            long value = mMeasureCache.valueAt(cacheIndex);
            // Casting a long to int drops the high 32 bits, no mask needed
            setMeasuredDimensionRaw((int) (value >> 32), (int) value);
            mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
        }
        ...
    }

    mOldWidthMeasureSpec = widthMeasureSpec;
    mOldHeightMeasureSpec = heightMeasureSpec;

    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}

measure方法中会调用onMeasure(widthMeasureSpec, heightMeasureSpec)方法,

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {
    boolean optical = isLayoutModeOptical(this);
    if (optical != isLayoutModeOptical(mParent)) {
        Insets insets = getOpticalInsets();
        int opticalWidth  = insets.left + insets.right;
        int opticalHeight = insets.top  + insets.bottom;

        measuredWidth  += optical ? opticalWidth  : -opticalWidth;
        measuredHeight += optical ? opticalHeight : -opticalHeight;
    }
    setMeasuredDimensionRaw(measuredWidth, measuredHeight);
}

private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;

    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

最终调用的setMeasuredDimensionRaw方法并确定mMeasuredWidthmMeasureHeight的值,也就是测量的过程目的就是为了确定宽高的值

再回到onMeasure方法,如果此时是ViewGroup,我们一般需要重写onMeasure方法,以FrameLayout为例

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();

    final boolean measureMatchParentChildren =
            MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
            MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
    mMatchParentChildren.clear();

    int maxHeight = 0;
    int maxWidth = 0;
    int childState = 0;
    //step 1: 遍历子View 
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            //step 2: 测量子View 
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
            maxWidth = Math.max(maxWidth,
                    child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
            maxHeight = Math.max(maxHeight,
                    child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
            childState = combineMeasuredStates(childState, child.getMeasuredState());
            if (measureMatchParentChildren) {
                if (lp.width == LayoutParams.MATCH_PARENT ||
                        lp.height == LayoutParams.MATCH_PARENT) {
                    mMatchParentChildren.add(child);
                }
            }
        }
    }

    //step 3:根据子view的测量结果,计算当前Framelayout的最终宽高 
    maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
    maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();

    // Check against our minimum height and width
    maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
    maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());

    // Check against our foreground's minimum height and width
    final Drawable drawable = getForeground();
    if (drawable != null) {
        maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
        maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
    }

    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
      ...
}

在FrameLayout的onMeasure中,首选要遍历子View,通过measureChildWithMargins方法中再调用getChildMeasureSpec确定View的SpecMode个SpecSize

public abstract class ViewGroup extends View implements ViewParent, ViewManager {
    protected void measureChildWithMargins(View child,
            int parentWidthMeasureSpec, int widthUsed,
            int parentHeightMeasureSpec, int heightUsed) {
        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
           //获取子view的MeasureSpec
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        + widthUsed, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed, lp.height);
        //step 4:把当前测量的子view的MeasureSpec作为入参,调用子View的measure方法,
        //递归调用,使得View树进入下一层级的测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
        int specMode = MeasureSpec.getMode(spec);
        int specSize = MeasureSpec.getSize(spec);

        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY:
            if (childDimension >= 0) {
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size. So be it.
                resultSize = size;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent has imposed a maximum size on us
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size, but our size is not fixed.
                // Constrain child to not be bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
            }
            break;

        // Parent asked to see how big we want to be
        case MeasureSpec.UNSPECIFIED:
            if (childDimension >= 0) {
                // Child wants a specific size... let him have it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
            } else if (childDimension == LayoutParams.MATCH_PARENT) {
                // Child wants to be our size... find out how big it should
                // be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size.... find out how
                // big it should be
                resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;
                resultMode = MeasureSpec.UNSPECIFIED;
            }
            break;
        }
        //noinspection ResourceType
        return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
    }
}

getChildMeasureSpec(int spec, int padding, int childDimension)实现如上

确定SpecMode和SpecSize的影响因素有父容器的MeasureSpec自身的LayoutParams,规则入下表所示

img

当获取到了子View的MeasureSpec后,把MeasureSpec作为入参继续调用子View的measure方法, 继续测量View树的下一层,进而完成整个View树的测量过程

2.3.2、performLayout - 摆放

performLayout方法会调用view.layout方法

public void layout(int l, int t, int r, int b) {
    if ((mPrivateFlags3 & PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT) != 0) {
        onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
        mPrivateFlags3 &= ~PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }

    int oldL = mLeft;
    int oldT = mTop;
    int oldB = mBottom;
    int oldR = mRight;
    //step 1: 通过setFrame方法确定View的摆放位置
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        //step 2: 然后再调用layout方法,实现子view的摆放
        onLayout(changed, l, t, r, b);
        ...
    }

layout方法中首先会调用setFrame()方法设定View的位置,也就是左上右下,确定了自身位置后再通过onMeasure确定子view的位置,我们在自定义ViewGrope时一般需要重写onLayout方法,根据我们ViewGroup的特性以确定子View改最终的摆放位置并调用子view.layout(l,t,r,b)进行摆放

2.3.3、performDraw - 绘制

performDraw ->draw(Canves fullRedrawNeeded) -> drawSoftware -> view.draw(canves) 调用流程如上,最终调用view的draw方法

public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    mPrivateFlags = (privateFlags & ~PFLAG_DIRTY_MASK) | PFLAG_DRAWN;

    /*
     * Draw traversal performs several drawing steps which must be executed
     * in the appropriate order:
     *
     *      1. Draw the background
     *      2. If necessary, save the canvas' layers to prepare for fading
     *      3. Draw view's content
     *      4. Draw children
     *      5. If necessary, draw the fading edges and restore layers
     *      6. Draw decorations (scrollbars for instance)
     */

    // Step 1, draw the background, if needed
    int saveCount;

    drawBackground(canvas);

    // skip step 2 & 5 if possible (common case)
    final int viewFlags = mViewFlags;
    boolean horizontalEdges = (viewFlags & FADING_EDGE_HORIZONTAL) != 0;
    boolean verticalEdges = (viewFlags & FADING_EDGE_VERTICAL) != 0;
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        onDraw(canvas);

        // Step 4, draw the children
        dispatchDraw(canvas);

        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);

        // Step 7, draw the default focus highlight
        drawDefaultFocusHighlight(canvas);
        return;
    }
   ...
}

onDraw(canves)绘制方法共有6步

  1. 绘制背景
  2. 保存 Canvas 图层
  3. 绘制自身内容的内容
  4. 绘制子View (dispatchDraw)
  5. 绘制 Canvas 图层
  6. 绘制装饰(比如 foreground 和 scrollbar)

重点关注绘制的第三步和第四步

  • 第三步:调用了onDraw(canvas),如果我们是自定义View的话一般需要复写onDraw方法,在里面进行Canves自身内容的绘制
  • 第四步:调用了dispatchDraw(canvas),如果当前View是ViewGroup那么就会调用ViewGroup的dispatchDraw方法,遍历所有子View并调用子View的draw方法,完成绘制方法在View树的逐层执行
@Override
protected void dispatchDraw(Canvas canvas) {
    ...
    
    for (int i = 0; i < childrenCount; i++) {
        while (transientIndex >= 0 && mTransientIndices.get(transientIndex) == i) {
            final View transientChild = mTransientViews.get(transientIndex);
            if ((transientChild.mViewFlags & VISIBILITY_MASK) == VISIBLE ||
                    transientChild.getAnimation() != null) {
                    //
                more |= drawChild(canvas, transientChild, drawingTime);
            }
            transientIndex++;
            if (transientIndex >= transientCount) {
                transientIndex = -1;
            }
        }
    }
    ...
}

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    return child.draw(canvas, this, drawingTime);
}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2022-03-30 18:36:39  更:2022-03-30 18:41:10 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 19:58:29-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码