自定义控件总结---onMeasure onLayout onDraw深入解析

          对于一个Android开发来说,不管你有多不喜欢自定义控件,你都得和她谈一场恋爱。所以我主动谈了这场很费脑子的恋爱,因为阅读源码的能力不好,在寻根抛底的找线索的过程中,搞得反胃,不过最后还是把来龙去脉都捋了一遍。当然今天的角不是常用且高效的组合型自定义控件,而是纯粹绘制出来的View或者重新定义规则的一个ViewGroup。

    简述1: 启动Activity的时候会创建一个PhoneWindow和DectorView,并将DectorView放进PhoneWindow中。这个DectorView(继承FrameLayout的控件View)的资源布局是根据Theme来确定的,这个布局一般是LinearLayout的上下布局,上面是一个title标题栏,下面是一个content的内容栏。我们一般在onCreate中的setContentView(resID),就是往这个内容栏里面放布局。   

    简述2:DectorView和PhoneWindow都有了,布局也setContentView了。 那么是谁启动的测量,布局,绘制这些绘制流程的呢?     答案是ViewRootImp这个类。   还是简单的捋一捋吧:

       1.ActivityThread收到RESUME_ACTIVITY消息,执行handleResumeActivity()方法:将DectorView添加进Window中。

      代码: wm.addView(decor, l);

       2.然后会在WindowManagerGlobal类中调用addView方法:创建ViewrootImp,并将DectorView传进去。

    root = new ViewRootImpl(view.getContext(), display);
    root.setView(view, wparams, panelParentView);

      3.然后在ViewRootImp的setView中触发requestLayout()方法,并执行scheduleTraversals();

     @Override
      public void requestLayout() {
         if (!mHandlingLayoutInLayoutRequest) {
           checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
     }

      4.然后scheduleTraversals()这个方法就是发送一个消息去执行一个线程,这个线程中便是真正开启执行测量,最终调用的是performTraversals方法:

   final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
   void doTraversal() {
       if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
               Debug.stopMethodTracing();
               mProfile = false;
            }
        }
    }

5.然后在这个方法里面调用测量  布局  绘制图  这些方法。performMeasure() performLayout() performDraw();

上面说了这么多就是为了更好的理解这个流程,而不至于一头雾水的留下为啥会调用这些方法,谁最先开启的这些方法等等有头无尾的疑问。  

      简述3:MeasureSpec是Mode和Size共同产生的一个值,32位。前两位是Mode模式,后面30位才是View实实在在的尺寸值。   一般情况下 一个View或者说ViewGroup的MeasureSpec是由父类ViewGroup和自己共同决定的。如下图:

1.  EXACTLY  :这个模式,精确模式,就是我能确定了它的具体值了。

              a.给View 设置了具体的值,100dp   200dp  等

              b.match_parenter:填充父窗体意思。如果父窗体是EXACTLY模式,即是一个确定的,那这个View就和他爹一样大。

2. AT_MOST  :这个模式代表最大不超过某个值的模式。wrap_content.。自适应大小,但是最大不超过他爹的最大值。

3. UNSPECIFIED: 父容器不对View有任何限制,给它想要的任何尺寸。一般用于系统内部,表示一种测量状态。

  一:测量  

      1.  ViewRootImp进行测量的初始  performTraversals()此方法,这里面我们测量了根布局DectorView的宽和高childWidthMeasureSpec 和 childHeightMeasureSpec ,需要注意的是Dedctorview就是老祖宗,没有爹的限制,自己是啥就是啥。而实际上他的 宽高都是match_parent。所以就是EXACTLY+屏幕的尺寸。

          对应的源码是:

     int childWidthMeasureSpec = getRootMeasureSpec(mWidth, lp.width);
     int childHeightMeasureSpec = getRootMeasureSpec(mHeight, lp.height);

     2.  然后  

----->  performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
----->  mView.measure(childWidthMeasureSpec, childHeightMeasureSpec);
----->  onMeasure(widthMeasureSpec, heightMeasureSpec);

      3.  onMeasure(widthMeasureSpec, heightMeasureSpec):首先判断这个View是否重写了onMeasure方法,如果重写了则调用重写的,否则就调用View.java 的。    这个方法View.java有,但是ViewGroup.java是没有的,因为继承他的自定义控件是一个容器,所以他的大小与子View的排列有关。因此继承ViewGroup 的自定义控件需要自己重写onMeasure来重新定义子View在其内部的排列规则。虽然View.java是有onMeasure方法,但是自己绘制的自定义View,他默认不处理padding设置的值,另外AT_MOST和MATCH_PARENT模式得到的尺寸是一样的,所以一般也是需要重写onmeasure()方法。下面我们就先看看View.java中的onMeasure()的源码      

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

=====>根据measureSpec的mode来确定view真正的size(注意看了,上面我们说的WRAP_CONTENT和MATCH_PARENT模式得到的宽高尺寸是一样的,并且这个size是父窗体去掉自身的padding值后,最大可用尺寸,所以结果都是填充父窗体,所以得重写onMeasure方法,并且处理AT_MOST的这种情况)

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;
}

=======>将这个真正的size设置进去

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);
}

=====》在onLayout 中或者其他地方,我们想使用测量值的宽或者高的时候就可以调用:

   view.getMeasuredWidth()  或者 view.geMeasureHeight()获取到我们设置进去的具体的实际尺寸值。

       所以如果是绘制的自定义View,最多就是重写onMesure方法,对padding以及AT_MOST这种情况进行处理即可。 如果自定义的是ViewGroup 这种情况则需要重新写onMeasure方法,按自己的规则排列规则来设置其尺寸。

measureChild():测量的子View的宽高,是父ViewGroup去掉padding值后,给留给子View的最大可用尺寸。此时子View设置margin值会不起作用。

measureChildWithMargins():测量的子View的宽高,是父ViewGroup去掉其padding值以及子View的margin值后,给留给子View的最大可用尺寸。一般使用它,因为这样可控性会更好。

下面是ViewGroup提供的方法:

//遍历子View,并调用measureChild()方法对每个子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];
        if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
            measureChild(child, widthMeasureSpec, heightMeasureSpec);
        }
    }
}

 //父View去掉padding 之后的可用最大size值

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);
}

//父View 去掉padding以及子View的margin之后 可用的最大size值

protected void measureChildWithMargins(View child,
        int parentWidthMeasureSpec, int widthUsed,
        int parentHeightMeasureSpec, int heightUsed) {
    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);
}

  //下面这段源码 就是最上面的表格表达的内容:

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);
}

所以在自定义的ViewGroup容器的时候,需要重写onMeasure(),里面实现的东西是:

    1.遍历子View-->2.子View调用measureChildWithMargins() 进行测量-->3.通过childView.getLayoutParams()获取这个子View的去除自身margin值之后的width,以及margin值--->4.然后再根据自定义容器的规则去计算宽高,并设置。

  二:Layout 这个就简单,一般就是讲按规则测量的值设置即可,另外在onLayout()中将所有子View,按照规则进行排列设置即可。

1.performLayout-->host.layout(0, 0, host.getMeasuredWidth(), host.getMeasuredHeight());-->onLayout(changed, l, t, r, b);  规则也都是重写的就掉用重写的,没重写就调用View.java 中的,具体可以自行跟踪和measure类似。

2.如果重写,则遍历子View,获取每一个子View的宽高,margin值然后按规则进行计算,并且调用childView.layout(l,t,r,b)方法进行放置。

三:Draw也很简单:

performDraw()-->draw(fullRedrawNeeded);-->drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)--->mView.draw(canvas);--->View.java的draw()方法。
public void draw(Canvas canvas) {
    final int privateFlags = mPrivateFlags;
    final boolean dirtyOpaque = (privateFlags & PFLAG_DIRTY_MASK) == PFLAG_DIRTY_OPAQUE &&
            (mAttachInfo == null || !mAttachInfo.mIgnoreDirtyState);
    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;

    if (!dirtyOpaque) {
        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
        if (!dirtyOpaque) 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 (debugDraw()) {
            debugDrawFocus(canvas);
        }

        // we're done...
        return;
    }

下面我就简单的仿写一个Linearlayout:

/**
 * 设计一个Linearlayout 包括水平,竖直情况(手动修改参数可以验证水平,竖直,原理一样的)。
 */
public class MyLinearLayout extends ViewGroup {

    Context mContext;

    // 布局方向,-1 水平   -2竖直

    public static final int HORIZONTAL = 0;
    public static final int VERTICAL = 1;
    public static final int mOrientation = 0;


    //自定义View的padding值
    public int mPaddingLeft;
    public int mPaddingRight;
    public int mPaddingTop;
    public int mPaddingBottom;

    //子View的个数
    public int childCount;

    public int mTotalWidth = 0;//子View+方向  确定

    public int mTotlaHeight = 0;//子View+方向  确定


    public MyLinearLayout(Context context) {
        this(context, null);

    }

    public MyLinearLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public MyLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mContext = context;


    }

    int widthMode;
    int widthSize;
    int heightMode;
    int heightSize;

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

        //1.获取自定义ViewGroup的宽高的模式以及参考尺寸
        //2.然后子根据子View的模式以及尺寸,来最终确定自定义ViewGroup的尺寸
        widthMode = MeasureSpec.getMode(widthMeasureSpec);
        widthSize = MeasureSpec.getMode(widthMeasureSpec);
        heightMode = MeasureSpec.getMode(heightMeasureSpec);
        heightSize = MeasureSpec.getMode(heightMeasureSpec);

        mPaddingLeft = getPaddingLeft();
        mPaddingRight = getPaddingRight();
        mPaddingTop = getPaddingTop();
        mPaddingBottom = getPaddingBottom();

        childCount = getChildCount();
        if (mOrientation == HORIZONTAL) {
            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
        } else {
            measureVertical(widthMeasureSpec, heightMeasureSpec);
        }

    }

    private void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {

        int maxWidth = 0;
        mTotlaHeight=0;

        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                continue;
            }
            measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
            int childWidth = childView.getMeasuredWidth();
            int childHigth = childView.getMeasuredHeight();

            //水平方向(计算每一个子View的width并取最大的)
            int cuurWeight = marginLayoutParams.leftMargin + childWidth + marginLayoutParams.rightMargin;
            maxWidth = Math.max(maxWidth, cuurWeight);

            //竖直方向计算(计算所有子View总的长度)
            mTotlaHeight = mTotlaHeight+marginLayoutParams.topMargin + childHigth +           marginLayoutParams.bottomMargin;

        }

         //计算完毕之后,再根据父类框架的Mode  来最终确定父类ViewGroup的宽高
        if (widthMode == MeasureSpec.AT_MOST) {
            //如果AT_MOST 我测量的所有子View加起来  就是父类的宽
            maxWidth =maxWidth+ mPaddingLeft + mPaddingRight;
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.EXACTLY);
        }

        if (heightMode == MeasureSpec.AT_MOST) {
            //如果AT_MOST 我取子View中最大的高 作为父类的高
            mTotlaHeight =mTotlaHeight+ mPaddingTop + mPaddingBottom;
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mTotlaHeight, MeasureSpec.EXACTLY);
        }

        //将处理好的在size和mode设置给自定义的ViewGroup里面
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }


    private void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
        int maxHeight = 0;

        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            if (childView.getVisibility() == GONE) {
                continue;
            }
            measureChildWithMargins(childView, widthMeasureSpec, 0, heightMeasureSpec, 0);
            MarginLayoutParams marginLayoutParams = (MarginLayoutParams) childView.getLayoutParams();
            int childWidth = childView.getMeasuredWidth();
            int childHigth = childView.getMeasuredHeight();
            //水平方向计算(计算所有子View总的长度)
            mTotalWidth += marginLayoutParams.leftMargin + childWidth + marginLayoutParams.rightMargin;
            //竖直方向(计算每一个子View的height并取最大的)
            int cuurHeight = marginLayoutParams.topMargin + childHigth + marginLayoutParams.bottomMargin;
            maxHeight = Math.max(maxHeight, cuurHeight);
        }
        //计算完毕之后,再根据父类框架的Mode  来最终确定父类ViewGroup的宽高
        if (widthMode == MeasureSpec.AT_MOST) {
            //如果AT_MOST 我测量的所有子View加起来  就是父类的宽
            mTotalWidth = mTotalWidth+mPaddingLeft + mPaddingRight;
            widthMeasureSpec = MeasureSpec.makeMeasureSpec(mTotalWidth, MeasureSpec.EXACTLY);
        }
        if (heightMode == MeasureSpec.AT_MOST) {
            //如果AT_MOST 我取子View中最大的高 作为父类的高
            maxHeight =maxHeight+ mPaddingTop + mPaddingBottom;
            heightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.EXACTLY);
        }

        //将处理好的在size和mode设置给自定义的ViewGroup里面
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
                getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
    }

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

    private void layoutVertical(int l, int t, int r, int b) {
        int mTop = mPaddingTop;
        for (int i = 0; i < childCount; i++) {
            View childAtView = getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) childAtView.getLayoutParams();
            int left = mPaddingLeft + layoutParams.leftMargin;
            mTop = mTop + layoutParams.topMargin;
            int right = left + childAtView.getMeasuredWidth();
            int bottom = mTop + childAtView.getMeasuredHeight();
            childAtView.layout(left, mTop, right, bottom);
            mTop =bottom+ layoutParams.topMargin;
        }
    }

    private void layoutHorizontal(int l, int t, int r, int b) {
        int mleft = mPaddingLeft;
        for (int i = 0; i < childCount; i++) {
            View childAtView = getChildAt(i);
            MarginLayoutParams layoutParams = (MarginLayoutParams) childAtView.getLayoutParams();
            mleft = mleft + layoutParams.leftMargin;
            int top = mPaddingTop + layoutParams.topMargin;
            int right = mleft + childAtView.getMeasuredWidth();
            int bottom = top + childAtView.getMeasuredHeight();
            childAtView.layout(mleft, top, right, bottom);
            mleft = right+layoutParams.rightMargin;
        }


    }


    /**
     * 重写获取的默认的LayoutParams,否则或出现强转异常
     *
     * @return
     */
    @Override
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new MyLayoutParams(getContext(), attrs);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
        return new MyLayoutParams(lp);
    }

    @Override
    protected LayoutParams generateDefaultLayoutParams() {
        return new MyLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
    }

    public static class MyLayoutParams extends MarginLayoutParams {

        public MyLayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public MyLayoutParams(int width, int height) {
            super(width, height);
        }

        public MyLayoutParams(LayoutParams lp) {
            super(lp);
        }
    }


}

总结:本篇很长,因为你涉及了一些源码。主要分成两大部分:

1.Activity的内部视图结构以及如何去触发测量,布局,绘制的。

2.具体分析测量,布局,绘制流程。

注意事项:

1.measureChild()和measureChildWithmargin()区别,以及如何使用。上面给出了解释。

2.如果使用measureChildWithmargin()需要重写getLayoutparams(),否则会类型转换异常。

3.系统默认不处理padding值,WRAP_CONTENT和MATCH_PARENET得到的结果是一样的,所以需要重写onMeasure()进行处理。

4. getMeasureWidth()或者getMeasureHeight.()  得到的尺寸是onMeasure测量尺寸。

    getWidth()和getHeight()获取的是onLayout()设置位置之后的尺寸,也就是显示在屏幕中的最终尺寸。

   getWidth=right-left;

    

  
 

  

    

  

猜你喜欢

转载自blog.csdn.net/lk2021991/article/details/88375224