制作的来历

以两张图为最初,让我们对ViewRootImpl的来历和相关性有个大致的认知,因为制作的主体主张者将是ViewRootImpl

对此需求深入了解的,能够看这篇文章 # WindowManager、ViewRootImpl、DocerView几个问题的了解

一文搞定面试 | Android View绘制
一文搞定面试 | Android View绘制

制作流程

这儿以一个极简的demo,输出stack trace以此观测制作的流程

// 这儿layout\draw都是对应的,先xx,再onXX
at com.xxx.widget.ChildView.onMeasure(ChildView.kt)
at android.view.View.measure(View.java)
// ……省略一系列view嵌套
// 这儿将measure\layout\draw三个进程的ViewGroup处列举了一下
// 能够看到在ViewGroup.onXX环节,会先对child主张xx行为递归深搜,回溯后再处理本身
at android.view.ViewGroup.drawChild(ViewGroup.java)
at android.view.ViewGroup.dispatchDraw(ViewGroup.java)
at android.view.View.draw(View.java)
at com.android.internal.policy.DecorView.draw(DecorView.java)
at android.view.View.updateDisplayListIfDirty(View.java)
// ThreadedRenderer归于硬件加快的调用,未敞开的话会有所不同
at android.view.ThreadedRenderer.updateViewTreeDisplayList(ThreadedRenderer.java)
at android.view.ThreadedRenderer.updateRootDisplayList(ThreadedRenderer.java)
at android.view.ThreadedRenderer.draw(ThreadedRenderer.java)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java)
at android.widget.FrameLayout.onLayout(FrameLayout.java)
at com.android.internal.policy.DecorView.onLayout(DecorView.java)
at android.view.View.layout(View.java)
at android.view.ViewGroup.layout(ViewGroup.java)
at android.view.ViewGroup.measureChildWithMargins(ViewGroup.java:6753)
at android.widget.FrameLayout.onMeasure(FrameLayout.java:185)
at com.android.internal.policy.DecorView.onMeasure(DecorView.java)
at android.view.View.measure(View.java:23196)
// 这儿performLayout\performDraw三步流程中不同点在这,measure时中间有一步measureHierarchy
// 三步操作中均对mView:View即DecoreView进行操作,然后递归遍历下去
at android.view.ViewRootImpl.performMeasure(ViewRootImpl.java)
at android.view.ViewRootImpl.measureHierarchy(ViewRootImpl.java)
// View的悉数制作流程均在ViewRootImpl中主张,一起关于为什么View不能在子线程中更新
// 仅因为其间进行created impl's thread check,假如在子线程有维护ViewRootImpl,那就能够更新了
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java)
// 垂直同步信号VSYN回调 在scheduleTraversals中主张,这儿触及同步屏障
// 感兴趣能够看看这篇文章https:///post/7220778786126577720
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java)
at android.view.Choreographer$CallbackRecord.run(Choreographer.java)
at android.view.Choreographer.doCallbacks(Choreographer.java)
at android.view.Choreographer.doFrame(Choreographer.java)
at android.view.Choreographer$FrameHandler.handleMessage(Choreographer.java)
at android.os.Handler.dispatchMessage(Handler.java)
at android.os.Looper.loop(Looper.java)

这是常见的制作流程图,虽然省略了部分细节,但全体环节的笼统描绘恰如其分。一般,在自定义控件时,ViewGroup的职责为存储View,需求重写onMeasureonLayout,而View的职责重在本身的制作,首要需求重写onDraw。留意是首要,详细完成场景,需求依据其需求做的作业和每个环节的才能决议,详见下面的分述章节

一文搞定面试 | Android View绘制

MeasureSpec

MeasureSpec其实便是个Int,其内部又是老生常谈的位运算。分为高2位Mode(因为就3种模式,2位足矣),和低30位Size(最大表明到1048575),对3种模式的释义也附在代码注释里了

public static int makeMeasureSpec(@IntRange(from = 0, to = (1 << MeasureSpec.MODE_SHIFT) - 1) int size,
                                  @MeasureSpecMode int mode) {
    // ……
    return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
public static int getMode(int measureSpec) {
    return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
    // MODE_MASK取反即低30位全1
    return (measureSpec & ~MODE_MASK);
}
private static final int MODE_SHIFT = 30;
// 0x3 即 二进制 11,左移30位,即取到高2位
private static final int MODE_MASK  = 0x3 << MODE_SHIFT;
@IntDef({UNSPECIFIED, EXACTLY, AT_MOST})
@Retention(RetentionPolicy.SOURCE)
public @interface MeasureSpecMode {}
/**
 * Measure specification mode: The parent has not imposed any constraint
 * on the child. It can be whatever size it wants.
 * child想多大,就多大。这和下面的EXACTLY在必定程度上是一致的,只不过当时child尚还不知道自己会有多大
 * 相似的场景,比方TextView,在动态setText时,bounds会改变,但在WRAP_CONTENT时假如超出parent会被切断,便是第三种
 * 这时,想让TextView的size为本身实践巨细的话,就需求用到这种,相似于Paint.measureText
 */
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
/**
 * Measure specification mode: The parent has determined an exact size
 * for the child. The child is going to be given those bounds regardless
 * of how big it wants to be.
 * 精确值如20dp,再比方matchParent,因为parent size已知,其实便是EXACTLY
 */
public static final int EXACTLY     = 1 << MODE_SHIFT;
/**
 * Measure specification mode: The child can be as large as it wants up
 * to the specified size.
 * 便是常见的wrapContent,约束最大尺度
 */
public static final int AT_MOST     = 2 << MODE_SHIFT;

LayoutParams

addViewnew View()时一般都会运用到LayoutParams,那为什么它会和MeasureSpec发生了相关呢?因为在丈量环节生成MeasureSpec.makeMeasureSpec时一般都会传入lp

选用了源码中getRootMeasureSpec的英文注释,教科书般解说了LayoutParams.xml中设置的与实践的MeasureSpec的对应关系

一起,需求留意的,在对child主张的measure进程中,一般给的child size都取自LayoutParams.width(详细下面会再细讲),而不是child.measuredWidth。所以需求动态resize时,完成measure需求当即设置layoutParams。如在# 丝滑体验:Android 循环轮播跑马灯一文中的详细实践(view是池化收回复用的,且child view size或许大于parent:FrameLayout),感兴趣的能够看看,可直接跳到## 轮询切换章节

childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth, lp.width)
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
    int measureSpec;
    switch (rootDimension) {
    case ViewGroup.LayoutParams.MATCH_PARENT:
        // 强制最大,即尺度固定时为 mode:EXACTLY,size:为强制的parent size
        // Window can't resize. Force root view to be windowSize.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
        break;
    case ViewGroup.LayoutParams.WRAP_CONTENT:
        // 自适应,即约束最大尺度时为 mode:AT_MOST,size:约束的最大尺度
        // Window can resize. Set max size for root view.
        measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
        break;
    default:
        // 不约束,同为固定尺度,只不过这儿size为设置的尺度值
        // Window wants to be an exact size. Force root view to be that size.
        measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
        break;
    }
    return measureSpec;
}

Measure 丈量巨细

先看View里measure,实践经过setMeasuredDimensionRaw,对mMeasuredWidth\mMeasuredHeight进行赋值,之后(比方layout环节,或手动调用measure丈量后)getMeasuredWidth就能取到值了。一起,这儿需求留意forceLayoutneedsLayout两个判别,force取值于mPrivateFlags,与requestLayout的主张有关,后面灵魂提问里会细讲。而needs就很望文生义了,就判别一波尺度是否发生改变,发生改变即认为需求。其间还需求关注下另一个符号位PFLAG_LAYOUT_REQUIRED

// View.java
public final void measure(int widthMeasureSpec, int heightMeasureSpec) {
    // ……
    long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
    if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
    // requestLayout()会设置这个符号位,下文可在灵魂提问的1中细看,假如为true,那就比然主张measure
    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) {
        int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
        if (cacheIndex < 0 || sIgnoreMeasureCache) {
            // measure ourselves, this should set the measured dimension flag back
            // 关注下这儿的调用链onMeasure -> setMeasuredDimension -> setMeasuredDimensionRaw
            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;
        }
        // 这儿!!很重要,是layout的符号位,也便是说measure会影响layout行为
        mPrivateFlags |= PFLAG_LAYOUT_REQUIRED;
    }
    // ……
    mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 |
            (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension
}
private void setMeasuredDimensionRaw(int measuredWidth, int measuredHeight) {
    mMeasuredWidth = measuredWidth;
    mMeasuredHeight = measuredHeight;
    mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
}

FrameLayout为例,来看看ViewGroup,是怎么调用到view.measure的吧。先是遍历child进行measureChildWithMargins》》》child.measure,再对本身setMeasuredDimension。其间getChildMeasureSpec及其传入的参数是终究影响child丈量的要害,当然自定义ViewGroup时也能够参照调用getChildMeasureSpec主张对child的丈量

// FrameLayout.java
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int count = getChildCount();
    // ……
    for (int i = 0; i < count; i++) {
        final View child = getChildAt(i);
        if (mMeasureAllChildren || child.getVisibility() != GONE) {
            // 这儿调用的是ViewGroup的measureChildWithMargins
            measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
            // ……
        }
    }
    // ……
    // 这儿比较要害,ViewGroup在丈量完child后,是需求设置自己尺度的,因为parent也在等你的尺度
    // 一般都用setMeasuredDimension赋值,而不是setMeasuredDimensionRaw
    // 其间距离的话,便是insets的一些影响,比方体系的status bar\navigation bar……这块我也不太了解,感兴趣能够再看看
    setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
            resolveSizeAndState(maxHeight, heightMeasureSpec,
                    childState << MEASURED_HEIGHT_STATE_SHIFT));
    // ……
}
// ViewGroup.java
protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    // 这儿便是当时在跑马灯(https:///post/7228182151703052346)一文中遇到的问题
    // getChildMeasureSpec(int spec, int padding, int childDimension)中childDimension传入了child.lp
    final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
    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);
}

跑马灯一文中,在setView后,假如需求从头丈量,就会调用view.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED))去获取例如TextView从头setText之后的尺度,后来发现动画设置的并无反常,但child view本身的尺度被parent 约束了。经debug发现,手动measure之后,其自发又进行了自上而下的measure,而此刻传递下来的SpecMode是parent FrameLayoutEXACTLY,而因为本身默认LayoutParamsWRAP_CONTENT,导致其终究走向AT_MOST with Parent size

// ViewGroup.java
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 them 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);
}

下面这幅图便是上面源码的总结,其终究会被转化为对应的MeasureSpec,不主张硬背,应该在实践运用时或遇到问题时,反复去翻看源码巩固记忆。千万要留意,这是getChildMeasureSpec由parent丈量child时对child的影响,和child自行主张measure是不同的

一文搞定面试 | Android View绘制
讲完上述源码,讲一下开发中的实践运用

以width为例,常用的取值方案有如下几种:

  • view.post 经过确保在attach后回调,获取到丈量完成的值
  • addOnGlobalLayoutListener\addOnPreDrawListener,实践上也依赖于attch,是大局viewtree的观测
  • onLayout\addOnLayoutChangeListener,这个在下面layout会讲,归于便是在后置环节去取
  • 手动调用view.measure(),而且手动生成MeasureSpec.makeMeasureSpec。这个就归于比较全能的了,能够依据自己想要的约束去取到需求的尺度

Layout 摆放方位

先看View里layout,实践经过setFrame,对成员特点mLeft……mBottom进行赋值,且如getWidth\Height也源于此。PFLAG_LAYOUT_REQUIRED再次出现,上文measure说到过,这儿便是印证了

// View.java
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;
    }
    // …
    // setOpticalFrame里也终究调用了setFrame,终究确认bounds的方位,一起会依据返回值决议是否要改写调用onLayout、onLayoutChange
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    // 留意看这是或句子,假如尺度改变changed或者PFLAG_LAYOUT_REQUIRED被符号,都会调用onLayout
    // 而上文中起到measure完成后,会设置PFLAG_LAYOUT_REQUIRED,也便是measure后必然跟从layout
    // 当然也不能完全这么说,performLayout的前置条件便是layoutRequested,即requestLayout设置的
    // 单纯的measure不会主动主张layout
    if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
        onLayout(changed, l, t, r, b);
        // ……
        // 这儿还有个OnLayoutChangeListeners的遍历
        listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
    }
    // ……
}
protected boolean setFrame(int left, int top, int right, int bottom) {
    boolean changed = false;
    // ……
    if (mLeft != left || mRight != right || mTop != top || mBottom != bottom) {
        changed = true;
        // Remember our drawn bit
        int drawn = mPrivateFlags & PFLAG_DRAWN;
        int oldWidth = mRight - mLeft;
        int oldHeight = mBottom - mTop;
        int newWidth = right - left;
        int newHeight = bottom - top;
        boolean sizeChanged = (newWidth != oldWidth) || (newHeight != oldHeight);
        // Invalidate our old position
        // 这儿是非常细节的一点了,假如尺度发生改变,会进行invalidate,这一点与灵魂提问1中有相关
        invalidate(sizeChanged);
        mLeft = left;
        mTop = top;
        mRight = right;
        mBottom = bottom;
        mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);
        mPrivateFlags |= PFLAG_HAS_BOUNDS;
        if (sizeChanged) {
            sizeChange(newWidth, newHeight, oldWidth, oldHeight);
        }
        // ……
    }
    return changed;
}

一般取用width时有两种,getMeasuredWidthgetWidthgetWidth在layout完成之后即onDraw环节能够运用,能够取到,值来历于bounds的左右鸿沟差值

public final int getWidth() {
    return mRight - mLeft;
}

因为View.layout由ViewGroup调用,这儿举例LinearLayout源码看一下,在详细自定义ViewGroup时可参阅应该怎么摆放child。其间,以Vertical为例,childTop控制行方位纵向摆放,setChildFrame里终究调用了child.layout进行摆放。全体仍是比较简略的,主张能够测验写个流式布局FlexBoxLayout练练手。

// LinearLayout.java
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    if (mOrientation == VERTICAL) {
        layoutVertical(l, t, r, b);
    } else {
        layoutHorizontal(l, t, r, b);
    }
}
// 举例纵向摆放
void layoutVertical(int left, int top, int right, int bottom) {
    // ……一些Gravity的处理
    for (int i = 0; i < count; i++) {
        final View child = getVirtualChildAt(i);
        if (child == null) {
            childTop += measureNullChild(i);
        } else if (child.getVisibility() != GONE) {
            // 这儿取用的是MeasuredWidth,需求留意
            // 像自行主张的measure,对实践本身不会发生影响,因为在像TextView.setText
            // resize后会主张requestLayout,并从头measure,所以需求经过layoutParam干预后续的onMeasure
            final int childWidth = child.getMeasuredWidth();
            final int childHeight = child.getMeasuredHeight();
            final LinearLayout.LayoutParams lp =
                    (LinearLayout.LayoutParams) child.getLayoutParams();
            // ……一些child gravity处理
            if (hasDividerBeforeChildAt(i)) {
                childTop += mDividerHeight;
            }
            childTop += lp.topMargin;
            setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                    childWidth, childHeight);
            // 这儿
            childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
            i += getChildrenSkipCount(child, i);
        }
    }
}

Draw Canvas制作

老规矩,先看View.draw,分为如下6步,其间2、5一般不执行。当然每个环节重要的在于其前后环节的含义,其本身含义并不必定那么死板,比方ViewGroup在super.dispatchDraw其实含义等同于override onDraw

  1. 制作布景,我们一般设置的background,终究会被转化为Drawable目标,然后调用其draw办法,这也是GradientDrawable的原理
  2. 保存 Canvas 图层为后续淡出做准备(可选)
  3. 制作 View 的内容,关于View本身就能够在这个环节进行对应的制作了
  4. 制作子 View (dispatchDraw),关于ViewGroup来说,完成了本身便开端分发制作child view
  5. 制作淡出边缘并康复 Canvas 图层(可选)
  6. 制作装修(比方 foreground 和 scrollbar)
// View.java
public void draw(Canvas canvas) {
    // Step 1, draw the background, if needed
    int saveCount;
    // 这儿不支持override,但会调用backgroud的Drawable.draw()
    drawBackground(canvas);
    // skip step 2 & 5 if possible (common case)
    // 一般情况下第 2 步和第 5 步是不执行的。
    if (!verticalEdges && !horizontalEdges) {
        // Step 3, draw the content
        // RecyclerView的ItemDecoration便是这个环节进行分发制作的
        onDraw(canvas);
        // Step 4, draw the children
        // 当然也有许多ViewGroup不在onDraw里进行自己的制作,而选择在super.dispatchDraw之前进行制作一些辅助内容
        // 比方ListView的divline,SmartRefreshLayout的header\footer能够进行一些缩放&方位处理
        dispatchDraw(canvas);
        drawAutofilledHighlight(canvas);
        // Overlay is part of the content and draws beneath Foreground
        if (mOverlay != null && !mOverlay.isEmpty()) {
            mOverlay.getOverlayView().dispatchDraw(canvas);
        }
        // Step 6, draw decorations (foreground, scrollbars)
        onDrawForeground(canvas);
        // we're done...
        return;
    }

关于Canvas的制作,其间还有许多好办法和玩法,想要通晓还得多多练习。这边奉上 抛物线 大佬的HenCoder系列,下面是第1节

# HenCoder Android 开发进阶: 自定义 View 1-1 制作根底

灵魂提问

1. invalidate和requestLayout的差异

从主张环节来看,requestLayout设置了PFLAG_FORCE_LAYOUT(上文measure和layout环节提过了,没留意的快去回忆一下,一起与PFLAG_FORCE_LAYOUT相关的是PFLAG_LAYOUT_REQUIRED)和PFLAG_INVALIDATED两个标志位,而invalidate仅设置了PFLAG_INVALIDATED一个标志位,共同点是parent.requestLayoutp.invalidateChild(View中) -> parent.invalidateChildInParent(ViewGroup中)均向上进行传递

// View.java
public void requestLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();
    // ……
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
    if (mParent != null && !mParent.isLayoutRequested()) {
        // 这儿看仔细条件,假如Parent不在requestLayout进程中,就向上传递,假如还在上一次的layout中,那就不会持续了
        mParent.requestLayout();
    }
    // ……
}
// invalidateCache是invalidate调用时传入用于符号部分改写仍是悉数改写,默认true为悉数改写
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    // ……
    // 一些不合法状态判别,比方不行见、mCurrentAnimation
    if (skipInvalidate()) {
        return;
    }
    // 脏区域符号
    mPrivateFlags |= PFLAG_DIRTY;
    if (invalidateCache) {
        // invalidateCache是硬件加快中用于重建View的display list的符号
        // 这个或许解说的不太恰当,我这儿了解因为仅在ThreadedRenderer取用了该符号位
        mPrivateFlags |= PFLAG_INVALIDATED;
        // 铲除PFLAG_DRAWING_CACHE_VALID
        mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
    }
    // Propagate the damage rectangle to the parent view.
    // 传递需求重绘的区域给parent
    final AttachInfo ai = mAttachInfo;
    final ViewParent p = mParent;
    if (p != null && ai != null && l < r && t < b) {
        final Rect damage = ai.mTmpInvalRect;
        damage.set(l, t, r, b);
        p.invalidateChild(this, damage);
    }
    // ……
}

PFLAG_INVALIDATED会被符号在view.mRecreateDisplayList,关于draw在分发进程中是否触发draw有决议作用。

或许本文对该PFLAG_INVALIDATED符号的认知了解不太正确,假如有了解的大佬欢迎指点

// ThreadedRenderer.java
view.mRecreateDisplayList = (view.mPrivateFlags & View.PFLAG_INVALIDATED) == View.PFLAG_INVALIDATED;
// ViewGroup.java
public RenderNode updateDisplayListIfDirty() {
    // ……
    if ((mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == 0
            || !renderNode.hasDisplayList()
            || (mRecreateDisplayList)) {
        // mRecreateDisplayList为true只是其间一个条件,再结合下一个判别,那mRecreateDisplayList为true便是draw的必要条件了
        // PFLAG_DRAWING_CACHE_VALID的铲除在这是也起作用了
        if (renderNode.hasDisplayList()
                && !mRecreateDisplayList) {
            // mRecreateDisplayList为false的话就return了
            mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
            mPrivateFlags &= ~PFLAG_DIRTY_MASK;
            dispatchGetDisplayList();
            return renderNode; // no work needed
        }
        // ……
        try {
            if (layerType == LAYER_TYPE_SOFTWARE) {
                // ……
            } else {
                // ……
                    draw(canvas);
                // ……
            }
        } finally {
            // ……
        }
    } else {
        mPrivateFlags |= PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID;
        mPrivateFlags &= ~PFLAG_DIRTY_MASK;
    }
    return renderNode;
}

关于invalidate来说,有点特别,稍作打开,差异在硬件加快和非硬件加快(即软件加快),关于这两种的差异,这边不作打开,仅记载结论。硬件加快重在向上传递需求重绘的view,而软件加快重在向上传递需求重绘的dirtyRect区域,关于整个分发流程,调用的办法也会不一致(这一点在文章最初的堆栈就说明晰)。关于制作流程来说,假如dirty不为空,那就需求进行制作,而硬件制作仅对target view标定,能够减少不必要的制作

// xml中硬件加快装备
<application
    android:hardwareAccelerated="true" />
// ViewGroup.java
public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null && attachInfo.mHardwareAccelerated) {
        // HW accelerated fast path
        onDescendantInvalidated(child, child);
        return;
    }
    ViewParent parent = this;
    if (attachInfo != null) {
        // ……
        do {
            View view = null;
            if (parent instanceof View) {
                view = (View) parent;
            }
            // ……
            parent = parent.invalidateChildInParent(location, dirty);
            // ……
        } while (parent != null);
    }
}

ViewGroup.invalidateChildInParent首要完成了dirty区域的兼并核算,一直传递到ViewRootImpl.invalidateChildInParent,而其间不管是invalidate仍是invalidateRectOnScreen,所以不管invalidaterequestLayout终究均调用了ViewRootImpl.scheduleTraversals,然后开端了文章最初的三板斧流程

// ViewRootImpl.java
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        // 这儿是提过的线程校验
        checkThread();
        // 这个影响performLayout主张
        mLayoutRequested = true;
        scheduleTraversals();
    }
}
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread();
    // ……
    if (dirty == null) {
        invalidate();
        return null;
    } else if (dirty.isEmpty() && !mIsAnimating) {
        return null;
    }
    // ……
    invalidateRectOnScreen(dirty);
    return null;
}

最终兜兜转转,我们仍是回来了

// ViewRootImpl.java
private void performTraversals() {
    //mLayoutRequested 在requestLayout时赋值为true
    boolean layoutRequested = mLayoutRequested && (!mStopped || mReportNextDraw);
    if (layoutRequested) {
        //measure 进程
        windowSizeMayChange |= measureHierarchy(host, lp, res,
                desiredWindowWidth, desiredWindowHeight);
    }
    ...
    final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
    if (didLayout) {
        //layout 进程
        performLayout(lp, mWidth, mHeight);
    }
    ...
}

而postInvalidate,便是Handler机制的简略包装,假如对Handler还不清楚,能够看看这篇一文搞定面试:Handler源码解析

总结: 其实requestLayoutinvalidate均会主张scheduleTraversals之后的三大流程,但关于是否分发深入下去,其间的符号位起到了决议性作用。因此

requestLayout必然会进行measure、layout、draw(会依据size是否change在layout时主张invalidate,假如不确认是否change且需求重绘,能够再手动调用invalidate())

invalidate仅会进行draw,当然假如在期间环节,某个view.layoutParams发生改变,导致measure时判别needsLayout为true,那也会进行measure、layout

2. 为什么Activity.onResume时还取不到控件高度

回忆一下文章最初的图,handleResumeActivity(在onResume后调用)时才进行了ViewRootImplDecroView的绑定,三大流程都没主张,自然获取不到。至于onCreate,首要完成了layout.xml的inflate解析作业,onResume担任addWindow

一文搞定面试 | Android View绘制

3. 为什么view.post能够获取到控件高度

post即把action放在view的等待行列mRunQueue中,在performTraversals>>view.dispatchAttachedToWindow会取出一切的handler action推送到ViewRootHandler,因为performTraversals本身便是个handler Runnable,所以view.post内容必定会等到performTraversals完成后才会等到音讯行列执行。假如对Handler的音讯行列不熟的,快去我专栏复习!!

// ViewRootImpl.java
private void performTraversals() {
    // ……
    host.dispatchAttachedToWindow(mAttachInfo, 0);
    // ……
    getRunQueue().executeActions(mAttachInfo.mHandler);
    // ……
    performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    // ……
    performLayout(lp, mWidth, mHeight);
    // ……
    performDraw();
}
// View.java
void dispatchAttachedToWindow(AttachInfo info, int visibility) {
    mAttachInfo = info;
    // ……
    if (mRunQueue != null) {
        mRunQueue.executeActions(info.mHandler);
        mRunQueue = null;
    }
}
// View.java
public boolean post(Runnable action) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.post(action);
    }
    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().post(action);
    return true;
}

4. view.setLayoutParams怎么发生作用

看到requestLayout信任我们就应该理解了,再结合measure流程中measureChild一般会用layoutParams,就一望而知了

// View.java
public void setLayoutParams(ViewGroup.LayoutParams params) {
    if (params == null) {
        throw new NullPointerException("Layout parameters cannot be null");
    }
    mLayoutParams = params;
    resolveLayoutParams();
    if (mParent instanceof ViewGroup) {
        ((ViewGroup) mParent).onSetLayoutParams(this, params);
    }
    requestLayout();
}

5. 运用切到后台,丈量、制作等操作还会执行吗

当activity STOP时,会依据如下调用链设置ViewRootImpl.mStopped为true,在performTraversals时,会阻拦measure、layout、draw,虽然draw这边或许有些不清不楚,但盲猜一波是拦住了,因为setWindowStopped直接对renderer进行了stop设置和destroy处理

// Actvity.java
final void performStop(boolean preserveWindow, String reason) {
    // ……
    if (!preserveWindow && mToken != null && mParent == null) {
        WindowManagerGlobal.getInstance().setStoppedState(mToken, true);
    }
    // ……
}
// WindowManagerGlobal.java
public void setStoppedState(IBinder token, boolean stopped) {
    // ……
    ViewRootImpl root = mRoots.get(i);
    root.setWindowStopped(stopped);
    // ……
}
// ViewRootImpl.java
private void performTraversals() {
    // ……
    if (!mStopped || wasReportNextDraw) {
       performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
    }
    // ……
    final boolean didLayout = layoutRequested && (!mStopped || wasReportNextDraw);
    if (didLayout) {
        performLayout(lp, mWidth, mHeight);
    }
    // ……
     boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;
    if (!cancelDraw) {
        // ……
        performDraw();
    } else {
        if (isViewVisible) {
            // Try again
            scheduleTraversals();
        } else {
            if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
                for (int i = 0; i < mPendingTransitions.size(); ++i) {
                    mPendingTransitions.get(i).endChangingAnimations();
                }
                mPendingTransitions.clear();
            }
            // We may never draw since it's not visible. Report back that we're finished
            // drawing.
            if (!wasReportNextDraw && mReportNextDraw) {
                mReportNextDraw = false;
                pendingDrawFinished();
            }
        }
    }
}

6. 暂无,欢迎弥补

关于View的制作方面的面试题一般需讲述全体流程,和部分细节源码,一起也会关注项目中的实践。当然文章最初说到的ViewRootImpl也是中高级的高频切入点之一。关于本文来说,主张面试者亲身手写一个自定义ViewGroup如流式布局,一个自定义View如柱状图,以此加深了解为好,仍是激烈安利HenCoder

其他参阅资料

探索 Android View 制作流程

Android根底之View的制作原理

Android View 烘托原理