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 GUI系列2——View绘制流程 -> 正文阅读

[移动开发]Android GUI系列2——View绘制流程

接上一篇 setContentView
https://www.icode9.com/content-4-610315.html

img

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oTztM1ae-1628265689751)(C:\Users\bafc\AppData\Roaming\Typora\typora-user-images\image-20210806233553658.png)]

1. ActivityThread.java

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    ....
    // 创建Activity,会调用Activity的onCreate方法
    // 从而完成DecorView的创建
    Activity a = performLaunchActivity(r, customIntent);
    if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        Bundle oldState = r.state;
        handleResumeActivity(r.tolen, false, r.isForward, !r.activity..mFinished && !r.startsNotResumed);
    }
}

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume) {
    unscheduleGcIdler();
    mSomeActivitiesChanged = true;
    // 调用Activity的onResume方法
    ActivityClientRecord r = performResumeActivity(token, clearHide);
    if (r != null) {
        final Activity a = r.activity;
        ...
        if (r.window == null &&& !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            // 得到DecorView
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            // 得到了WindowManager,WindowManager是一个接口
            // 并且继承了接口ViewManager
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (a.mVisibleFromClient) {
                a.mWindowAdded = true;
                // WindowManager的实现类是WindowManagerImpl,
                // 所以实际调用的是WindowManagerImpl的addView方法
                wm.addView(decor, l);
            }
        }
    }
}

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    ...

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }
    ...
}

在了解View绘制的整体流程之前,我们必须先了解下ViewRoot和DecorView的概念。ViewRoot对应于ViewRootImpl类,它是连接WindowManager和DecorView的纽带,View的三大流程均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完毕后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联,相关源码如下所示:

// WindowManagerGlobal的addView方法
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
    ...
    ViewRootImpl root;
    View pannelParentView = null;
    synchronized (mLock) {
        ...
        // 创建ViewRootImpl实例
        root = new ViewRootImpl(view..getContext(), display);
        view.setLayoutParams(wparams);
        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }
    try {
        // 把DecorView加载到Window中
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

2. 绘制整体流程

绘制会从根视图ViewRoot的performTraversals()方法开始,从上到下遍历整个视图树,每个View控件负责绘制自己,而ViewGroup还需要负责通知自己的子View进行绘制操作。performTraversals()的核心代码如下。

private void performTraversals() {
    ...
    int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
    int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);
    ...
    //执行测量流程
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
    //执行布局流程
    performLayout(lp, desiredWindowWidth, desiredWindowHeight);
    ...
    //执行绘制流程
    performDraw();
}

3 理解MeasureSpec

3.1 源码解析

MeasureSpec表示的是一个32位的int值,它的高2位表示测量模式SpecMode,低30位表示某种测量模式下的规格大小SpecSize。MeasureSpec是View类的一个静态内部类,用来说明应该如何测量这个View。MeasureSpec的核心代码如下。

public static class MeasureSpec {
    private static final int MODE_SHIFT = 30;
    private static final int MODE_MASK = 0X3 << MODE_SHIFT;

    // 不指定测量模式, 父视图没有限制子视图的大小,子视图可以是想要
    // 的任何尺寸,通常用于系统内部,应用开发中很少用到。
    public static final int UNSPECIFIED = 0 << MODE_SHIFT;

    // 精确测量模式,视图宽高指定为match_parent或具体数值时生效,
    // 表示父视图已经决定了子视图的精确大小,这种模式下View的测量
    // 值就是SpecSize的值。
    public static final int EXACTLY = 1 << MODE_SHIFT;

    // 最大值测量模式,当视图的宽高指定为wrap_content时生效,此时
    // 子视图的尺寸可以是不超过父视图允许的最大尺寸的任何尺寸。
    public static final int AT_MOST = 2 << MODE_SHIFT;

    // 根据指定的大小和模式创建一个MeasureSpec
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }

    // 微调某个MeasureSpec的大小
    static int adjust(int measureSpec, int delta) {
        final int mode = getMode(measureSpec);
        if (mode == UNSPECIFIED) {
            // No need to adjust size for UNSPECIFIED mode.
            return make MeasureSpec(0, UNSPECIFIED);
        }
        int size = getSize(measureSpec) + delta;
        if (size < 0) {
            size = 0;
        }
        return makeMeasureSpec(size, mode);
    }
}

MeasureSpec通过将SpecMode和SpecSize打包成一个int值来避免过多的对象内存分配,为了方便操作,其提供了打包和解包的方法,打包方法为上述源码中的makeMeasureSpec,解包方法源码如下:

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

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

3.2 DecorView的MeasureSpec的创建

//desiredWindowWidth和desiredWindowHeight是屏幕的尺寸
childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width);
childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight, lp.height);
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

private static int getRootMeaureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
        case ViewGroup.LayoutParams.MATRCH_PARENT:
            // Window can't resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
            break;
        case ViewGroup.LayoutParams.WRAP_CONTENT:
            // Window can resize. Set max size for root view.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
            break
        default:
            // Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
            break;
    }
    return measureSpec;
}

3.3 子元素MeasureSpec

// ViewGroup的measureChildWithMargins方法
protected void measureChildWithMargins(View child,
int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();

    // 子元素的MeasureSpec的创建与父容器的MeasureSpec和子元素本身
    // 的LayoutParams有关,此外还和View的margin及padding有关
    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);

    child..measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

public static int getChildMeasureSpec(int spec, int padding, int childDimesion) {
    int specMode = MeasureSpec.getMode(spec);
    int specSize = MeasureSpec.getSize(spec);

    // padding是指父容器中已占用的空间大小,因此子元素可用的
    // 大小为父容器的尺寸减去padding
    int size = Math.max(0, specSize - padding);

    int resultSize = 0;
    int resultMode = 0;

    switch (sepcMode) {
        // 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 (childDimesion == 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 = 0;
                resultMode = MeasureSpec.UNSPECIFIED;
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {
                // Child wants to determine its own size....
                // find out how big it should be
                resultSize = 0;
                resultMode == MeasureSpec.UNSPECIFIED;
            }
            break;
        }
    return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

UNSPECIFIED模式主要用于系统内部多次Measure的情形,一般不需关注。

4 绘制

4.1 Measure 流程

private void perormMeasure(int childWidthMeasureSpec, int childHeightMeasureSpec) {
    ...
    // 具体的测量操作分发给ViewGroup
    mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    ...
}

// 在ViewGroup中的measureChildren()方法中遍历测量ViewGroup中所有的View
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
    final int size = mChildrenCount;
    final View[] children = mChildren;
    for (int i = 0; i < size; ++i) {
        final View child = children[i];
        // 当View的可见性处于GONE状态时,不对其进行测量
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

// 测量某个指定的View
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
    final LayoutParams lp = child.getLayoutParams();

    // 根据父容器的MeasureSpec和子View的LayoutParams等信息计算
    // 子View的MeasureSpec
    final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft + mPaddingRight, lp.width);
    final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec, mPaddingTop + mPaddingBottom, lp.height);
    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

// View的measure方法
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    ...
    // ViewGroup没有定义测量的具体过程,因为ViewGroup是一个
    // 抽象类,其测量过程的onMeasure方法需要各个子类去实现
    onMeasure(widthMeasureSpec, heightMeasureSpec);
    ...
}

// 不同的ViewGroup子类有不同的布局特性,这导致它们的测量细节各不相同,如果需要自定义测量过程,则子类可以重写这个方法
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    // setMeasureDimension方法用于设置View的测量宽高
    setMeasureDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 
    getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

// 如果View没有重写onMeasure方法,则会默认调用getDefaultSize来获得View的宽高
public static int getDefaultSize(int size, int measureSpec) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);

    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
        case MeasureSpec.EXACTLY:
            result = sepcSize;
            break;
    }
    return result;
}

4.2 getSuggestMinimumWidth

protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinmumWidth());
}

protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

public int getMinimumWidth() {
    final int intrinsicWidth = getIntrinsicWidth();
    return intrinsicWidth > 0 ? intrinsicWidth : 0;
}

4.3 自定义View时手动处理wrap_content时的情形

直接继承View的控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent。解决方式如下:

protected void onMeasure(int widthMeasureSpec, 
int height MeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
    int widtuhSpecSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
    // 在wrap_content的情况下指定内部宽/高(mWidth和mHeight)
    int heightSpecSize = MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasuredDimension(mWidth, mHeight);
    } else if (widthSpecMode == MeasureSpec.AT_MOST) {
        setMeasureDimension(mWidth, heightSpecSize);
    } else if (heightSpecMode == MeasureSpec.AT_MOST) {
        setMeasureDimension(widthSpecSize, mHeight);
    }
}

4.4 .LinearLayout的onMeasure方法实现解析

protected void onMeasure(int widthMeasureSpec, int hegithMeasureSpec) {
    if (mOrientation == VERTICAL) {
        measureVertical(widthMeasureSpec, heightMeasureSpec);
    } else {
        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
    }
}

// measureVertical核心源码
// See how tall everyone is. Also remember max width.
for (int i = 0; i < count; ++i) {
    final View child = getVirtualChildAt(i);
    ...
    // Determine how big this child would like to be. If this or 
    // previous children have given a weight, then we allow it to 
    // use all available space (and we will shrink things later 
    // if need)
    measureChildBeforeLayout(
            child, i, widthMeasureSpec, 0, heightMeasureSpec,
            totalWeight == 0 ? mTotalLength : 0);

    if (oldHeight != Integer.MIN_VALUE) {
        lp.height = oldHeight;
    }

    final int childHeight = child.getMeasuredHeight();
    final int totalLength = mTotalLength;
    mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin + 
    lp.bottomMargin + getNextLocationOffset(child));
}

系统会遍历子元素并对每个子元素执行measureChildBeforeLayout方法,这个方法内部会调用子元素的measure方法,这样各个子元素就开始依次进入measure过程,并且系统会通过mTotalLength这个变量来存储LinearLayout在竖直方向的初步高度。每测量一个子元素,mTotalLength就会增加,增加的部分主要包括了子元素的高度以及子元素在竖直方向上的margin等。

// LinearLayout测量自己大小的核心源码
// Add in our padding
mTotalLength += mPaddingTop + mPaddingBottom;
int heightSize = mTotalLength;
// Check against our minimum height
heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
// Reconcile our calculated size with the heightMeasureSpec
int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
...
setMeasuredDimension(resolveSizeAndSize(maxWidth, widthMeasureSpec, childState),
heightSizeAndState);

public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
    int result = size;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    switch (specMode) {
        case MeasureSpec.UNSPECIFIED:
            result = size;
            break;
        case MeasureSpec.AT_MOST:
            // 高度不能超过父容器的剩余空间
            if (specSize < size) {
                result = specSize | MEASURED_STATE_TOO_SMALL;
            } else {
                result = size;
            }
            break;
        case MeasureSpec.EXACTLY:
            result = specSize;
            break;
    }
    return result | (childMeasuredState & MEASURED_STATE_MASK);
}

5. Activity中获取某个view的宽高

由于View的measure过程和Activity的生命周期方法不是同步执行的,如果View还没有测量完毕,那么获得的宽/高就是0。所以在onCreate、onStart、onResume中均无法正确得到某个View的宽高信息。两种解决方式

// 此时View已经初始化完毕
// 当Activity的窗口得到焦点和失去焦点时均会被调用一次
// 如果频繁地进行onResume和onPause,那么onWindowFocusChanged也会被频繁地调用
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if (hasFocus) {
        int width = view.getMeasureWidth();
        int height = view.getMeasuredHeight();
    }
}
//=======================================================================
// 通过post可以将一个runnable投递到消息队列的尾部,
// 然后等待Looper调用次runnable的时候,View也已经初始化好了
protected void onStart() {
    super.onStart();
    view.post(new Runnable() {
        @Override
        public void run() {
            int width = view.getMeasuredWidth();
            int height = view.getMeasuredHeight();
        }
});
//=======================================================================
// 当View树的状态发生改变或者View树内部的View的可见性发生改变时,onGlobalLayout方法将被回调
protected void onStart() {
super.onStart();
    ViewTreeObserver observer = view.getViewTreeObserver();
    observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
    @SuppressWarnings("deprecation")
    @Override
    public void onGlobalLayout() {
        view.getViewTreeObserver().removeGlobalOnLayoutListener(this);
        int width = view.getMeasuredWidth();
        int height = view.getMeasuredHeight();
    }
	});
}
    

5 View的绘制流程 Layout

5.1 Layout的基本流程

// ViewRootImpl.java
private void performLayout(WindowManager.LayoutParams lp, int desiredWindowWidth, int desiredWindowHeight) {
    ...
    host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());
    ...
}

// View.java
public void layout(int l, int t, int r, int b) {
    ...
    // 通过setFrame方法来设定View的四个顶点的位置,即View在父容器中的位置
    boolean changed = isLayoutModeOptical(mParent) ? 
    set OpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

    ...
    onLayout(changed, l, t, r, b);
    ...
}

// 空方法,子类如果是ViewGroup类型,则重写这个方法,实现ViewGroup
// 中所有View控件布局流程
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {

}

5.2 LinearLayout的onLayout方法实现解析

protected void onlayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l,)
    }
}

// layoutVertical核心源码
void layoutVertical(int left, int top, int right, int bottom) {
    ...
    final int count = getVirtualChildCount();
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            final int childWidth = child.getMeasureWidth();
            final int childHeight = child.getMeasuredHeight();

            final LinearLayout.LayoutParams lp = 
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            ...
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }

            childTop += lp.topMargin;
            // 为子元素确定对应的位置
            setChildFrame(child, childLeft, childTop + getLocationOffset(child), childWidth, childHeight);
            // childTop会逐渐增大,意味着后面的子元素会被
            // 放置在靠下的位置
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

            i += getChildrenSkipCount(child,i)
        }
    }
}

private void setChildFrame(View child, int left, int top, int width, int height) {
    child.layout(left, top, left + width, top + height);
}

6 View的绘制流程Draw

6.1. Draw基本流程

private void performDraw() {
    ...
    draw(fullRefrawNeeded);
    ...
}

private void draw(boolean fullRedrawNeeded) {
    ...
    if (!drawSoftware(surface, mAttachInfo, xOffest, yOffset, 
    scalingRequired, dirty)) {
        return;
    }
    ...
}

private boolean drawSoftware(Surface surface, AttachInfo attachInfo, 
int xoff, int yoff, boolean scallingRequired, Rect dirty) {
    ...
    mView.draw(canvas);
    ...
}

// 绘制基本上可以分为六个步骤
public void draw(Canvas canvas) {
    ...
    // 步骤一:绘制View的背景
    drawBackground(canvas);

    ...
    // 步骤二:如果需要的话,保持canvas的图层,为fading做准备
    saveCount = canvas.getSaveCount();
    ...
    canvas.saveLayer(left, top, right, top + length, null, flags);

    ...
    // 步骤三:绘制View的内容
    onDraw(canvas);

    ...
    // 步骤四:绘制View的子View
    dispatchDraw(canvas);

    ...
    // 步骤五:如果需要的话,绘制View的fading边缘并恢复图层
    canvas.drawRect(left, top, right, top + length, p);
    ...
    canvas.restoreToCount(saveCount);

    ...
    // 步骤六:绘制View的装饰(例如滚动条等等)
    onDrawForeground(canvas)
}
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-08 11:27:20  更:2021-08-08 11:29:01 
 
开发: 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年5日历 -2024/5/17 14:37:32-

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