自定义View之View的工作原理

简介: 自定义View之View的工作原理



1.View的绘制流程从哪里开始

      View的绘制流程是从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来。

     如图所示,performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程,其中在performMeasure中会调用measure方法,在measure方法中会调用onMeasure方法,在onMeasure方法这则会对所有的子元素进行measure过程,这时候meaure流程就从父容器传递到子元素中了,这样就完成了一次measure过程。同理,performLayout和performDraw的传递流程和performMeasure是类似的。

2.什么是MeasureSpec?

        MeasureSpec代表一个32位int值,高2位代表SpecMode,就是测量模式,低30位代表SpecSize,就是规格大小。

(1)SpecMode有三类
  • UNSPECIFIED      

父容器对子 View 没有施加任何限制,子 View 可以任意大小。一般用于系统内部。

  • EXACTLY            

父容器已经为子 View 精确指定了大小,子 View 应该匹配这个大小。它对应于LayoutParams中的match_parent和具体的数值这两个模式。

  • AT_MOST            

子 View 可以是任何大小,但不能超过父容器指SpecSize。它对应于LayoutParams中的wrap_content。

(2)创建MeasureSpec
MeasureSpec measureSpec=MeasureSpec.makeMeasureSpec(size,mode);

      MeasureSpec 是通过静态方法 MeasureSpec.makeMeasureSpec() 创建的,该方法接受两个参数:大小和测量模式。

(3)获取测量模式SpecMode和大小SpecSize
int selfwidthSpecMode=MeasureSpec.getMode(widthMeasureSpec);
int selfwidthSpecSize=MeasureSpec.getSize(widthMeasureSpec);

        我们可以通过 MeasureSpec.getMode() 和 MeasureSpec.getSize() 方法来获取测量模式和大小,然后根据这些信息来确定 View 的最终大小。

(4)MeasureSpec和LayoutParams的对应关系

       在View测量的时候,系统会将LayoutParams在父容器的约束下转换成对应的MeasureSpec,然后再根据这个MeasureSpec来确定View测量后的宽/高。需要注意的是,MeasureSpec不是唯一由LayoutParams决定的,LayoutParams需要和父容器一起才能决定View的MeasureSpec从而决定View的宽高。

MeasureSpec的转换流程

对于顶级的View(DecorView),其MeasureSpec由窗口的尺寸和其自身的LayoutParams来共同确定。

其中LayoutParams中的宽高的参数

  • LayoutParams.MATCH_PARENT:精确模式,大小就是窗口的大小。
  • LayoutParams.WRAP_CONTENT:最大模式,大小不定,但是不能超过窗口的大小。
  • 固定大小(比如100dp):精确模式,大小为LayoutParams中指定的大小。

对于普通的View,其MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams来共同确定。

  • 当View采用固定宽/高时,View的MeasureSpec都是精确模式并且其大小遵循Layoutparams中的大小。
  • 当View的宽/高是match_parent时,如果父容器的模式是精确模式,那么View也会是精确模式并且大小是父容器的剩余空间。如果父容器的模式是最大模式,那么View也是最大模式并且其大小不会超过父容器的剩余空间。
  • 当View的宽/高是wrap_content时,View的模式总是最大化并且大小不能超过父容器的剩余空间。

3.View的工作流程

 View的工作流程主要是measure、layout、draw这三大流程,即测量、布局和绘制。

(1)measure过程

       1️⃣  View的measure过程

     View的measure过程由其measure方法来完成,measure方法是一个final类型的方法,子类不能重写该方法,在View的measure方法中会去调用View的onMeasure方法,View的onMeasure方法如下所示:

#onMeasure

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

setMeasuredDimension方法会设置View宽高的测量值。

#getDefaultSize

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 = specSize;
            break;
        }
        return result;
    }

AT_MOST和EXACTLY情况下,getDefaultSize方法返回值为MeasureSpec的specSize 。

UNSPECIFIED情况下,getDefaultSize方法返回值宽/高为getSuggestedMinimumWidth()和getSuggestedMinimumHeight()。

#getSuggestedMinimumWidth()和getSuggestedMinimumHeight()

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

     如果 View 没有设置背景,那么返回android:minWidth 这个属性所指定的值,这个值可以为 0; 如果 View 设置了背景,则返回 android:minWidth 和背景的最小宽度这两者中的最大值,getSuggestedMinimumWidth和getSuggestedMinimumHeight 的返回值就是 View 在 UNSPECIFIED 情况下的测量宽/高。

       2️⃣ViewGroup的measure过程

 对于ViewGroup来说,除了完成自己的measure过程以外,还会去遍历调用所有子元素的measure方法,各个子元素再递归去执行这个过程。ViewGroup是一个抽象类,因此它没有重写View的OnMeasure方法,提供了一个measureChildren方法。

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];
            if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

上面代码得出,ViewGroup在measure时,会对每一个子元素进行measure,measureChildren这个方法的实现如下所示:

protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

      measureChild 的思想就是取出子元素的 LayoutParams,然后再通过getChildMeasureSpec 来创建子元素的 MeasureSpec,接着将 MeasureSpec 直接传递给 View的measure 方法来进行测。

(2)layout过程

     Layout的作用是ViewGroup用来确认子元素的位置,当ViewGroup的位置被确定后,它在onLayout中会遍历所有的子元素并调用其layout方法,在layout方法中onLayout方法会被调用。

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;
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            onLayout(changed, l, t, r, b);
            if (shouldDrawRoundScrollbar()) {
                if(mRoundScrollbarRenderer == null) {
                    mRoundScrollbarRenderer = new RoundScrollbarRenderer(this);
                }
            } else {
                mRoundScrollbarRenderer = null;
            }
            mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnLayoutChangeListeners != null) {
                ArrayList<OnLayoutChangeListener> listenersCopy =
                        (ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
                int numListeners = listenersCopy.size();
                for (int i = 0; i < numListeners; ++i) {
                    listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
                }
            }
        }
        final boolean wasLayoutValid = isLayoutValid();
        mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
        mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;

      layout方法的大致流程如下:首先会通过 setFrame方法来设定 View的四个顶点的位置即初始化 mLeft、mRight、mTop 和 mBottom 这四个值,View 的四个顶点一旦确定,那View 在父容器中的位置也就确定了; 接着会调用 onLayout 方法,这个方法的用途是父容器确定子元素的位置,和onMeasure 方法类似,onLayout 的具体实现同样和具体的布局有关,所以 View 和 ViewGroup 均没有真正实现 onLayout 方法。

❗❗View的getMeasureWidth和getWidth这两个方法有什么区别?

在View的默认实现中,View的测量宽/高和最终宽/高是相等的,只不过测量宽/高在measure过程,而最终宽/高在layout过程,即两者的赋值时机不同。但是存在某些特殊情况会导致两者不一致,下面举例说。

如果重写View的layout方法,代码如下:

protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right+100, bottom+100);
    }

上述代码会导致在任何情况下View的最终宽/高总是比测量宽/高大100px。

(3)draw过程

Draw过程的作用是将View绘制到屏幕上面。View的绘制过程遵循如下几步:

  1. 绘制背景background.draw(canvas)。
  2. 绘制自己(onDraw)。
  3. 绘制children(dispatchDraw)。
  4. 绘制装饰(onDrawScrollBars)。

上面可通过源码看出:

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)
         *      7. If necessary, draw the default focus highlight
         */
        // 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);
            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);
            // Step 7, draw the default focus highlight
            drawDefaultFocusHighlight(canvas);
            if (isShowingLayoutBounds()) {
                debugDrawFocus(canvas);
            }
            // we're done...
            return;
        }
...
}
目录
相关文章
|
1月前
|
XML 前端开发 Android开发
Android View的绘制流程和原理详细解说
Android View的绘制流程和原理详细解说
35 3
|
API Android开发
Android View滑动相关的基础知识点
*本文涉及到的知识点:MotionEvent、ViewConfiguration、VelocityTracker 、GestureDetector、scrollTo、scrollBy、Scroller、OverScroller*
|
Android开发
图+源码,读懂View的Layout方法
本篇文章就带大家学习 View 绘制三大方法的第二个方法——Layout 方法。
图+源码,读懂View的Layout方法
|
Android开发 容器
View工作原理分析1 - 初识ViewRoot和 DecorView
以下相关资料均来自 Android艺术探索,部分内容加入了一些我个人的理解。
144 0
|
前端开发 Android开发 iOS开发
Flutter 35: 图解自定义 View 之 Canvas (二)
0 基础学习 Flutter,第三十五步:自定义 View 第三节~
4705 0
|
前端开发 Android开发 存储
Flutter 36: 图解自定义 View 之 Canvas (三)
0 基础学习 Flutter,第三十六步:自定义 View 第四节~
2026 0
|
前端开发
Flutter 34: 图解自定义 View 之 Canvas (一)
0 基础学习 Flutter,第三十四步:自定义 View 第二节~
2673 0
|
XML 前端开发 Android开发
自定义View - 简单的TextView封装
引言 在平常的开发中,我们总会有各种各样的按钮,圆角的、直角的、正常状态的、按下状态的、禁用状态的。一直的做法就是在drawable中写一个selector,然后用item加shap来实现。
1184 0
|
程序员 Android开发
有关自定义View的学习(View的点击事件传递流程)
1、安卓OnTouchListener,onTouchEvent,onClickListener执行顺序 首先我们相对比较熟悉的是处理滑动冲突时候的三个事件(ViewGroup 继承 View) (View是没有onInterceptTouchEvent方法的),先看看ViewGroup和View的事件流程方法: ViewGroup 1.
1180 0