Android进阶之自定义View原理(一)View的Measure过程

引言

自定义View作为Android开发者必须掌握的重点和难点,它是android开发的核心技能之一。网络上有很多介绍它们的文章,但存在一些问题:内容不全、浅尝辄止、无源码分析等等。在接下来的几篇博客当中,我将从View的测量、布局、绘制、触摸事件分发机制以及弹性滚动这几方面入手,从源码层面理解它们各自的实现原理,帮助大家彻底明白自定义View的实现原理,踩一踩坑。(源码为API26,与之前版本可能有些改动,但原理不变。)

(一)View的measure流程

1.理解MeasureSpec

   /**源码分析:理解MeasureSpec
     *  <<========分析(1)========>>
     A MeasureSpec encapsulates the layout requirements passed from parent to child.
     * Each MeasureSpec represents a requirement for either the width or the height.
     * A MeasureSpec is comprised of a size and a mode. There are three possible
     * modes:
     * 
*
UNSPECIFIED
*
* The parent has not imposed any constraint on the child. It can be whatever size * it wants. *
* *
EXACTLY
*
* 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. *
* *
AT_MOST
*
* The child can be as large as it wants up to the specified size. *
*
* * MeasureSpecs are implemented as ints to reduce object allocation. This class * is provided to pack and unpack the <size, mode> tuple into the int. */ public static class MeasureSpec { private static final int MODE_SHIFT = 30; private static final int MODE_MASK = 0x3 << MODE_SHIFT; /** @hide */ @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. */ 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. */ 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. */ public static final int AT_MOST = 2 << MODE_SHIFT; /** <<========分析(2)========>> * Creates a measure specification based on the supplied size and mode. * * The mode must always be one of the following: *
    *
  • {@link android.view.View.MeasureSpec#UNSPECIFIED}
  • *
  • {@link android.view.View.MeasureSpec#EXACTLY}
  • *
  • {@link android.view.View.MeasureSpec#AT_MOST}
  • *
* *

Note: On API level 17 and lower, makeMeasureSpec's * implementation was such that the order of arguments did not matter * and overflow in either value could impact the resulting MeasureSpec. * {@link android.widget.RelativeLayout} was affected by this bug. * Apps targeting API levels greater than 17 will get the fixed, more strict * behavior.

* * @param size the size of the measure specification * @param mode the mode of the measure specification * @return the measure specification based on size and mode */ 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); } } /** * Like {@link #makeMeasureSpec(int, int)}, but any spec with a mode of UNSPECIFIED * will automatically get a size of 0. Older apps expect this. * * @hide internal use only for compatibility with system widgets and older apps */ public static int makeSafeMeasureSpec(int size, int mode) { if (sUseZeroUnspecifiedMeasureSpec && mode == UNSPECIFIED) { return 0; } return makeMeasureSpec(size, mode); } /**<<========分析(3)========>> * Extracts the mode from the supplied measure specification. * * @param measureSpec the measure specification to extract the mode from * @return {@link android.view.View.MeasureSpec#UNSPECIFIED}, * {@link android.view.View.MeasureSpec#AT_MOST} or * {@link android.view.View.MeasureSpec#EXACTLY} */ @MeasureSpecMode public static int getMode(int measureSpec) { //noinspection ResourceType return (measureSpec & MODE_MASK); } /**<<========分析(3)========>> * Extracts the size from the supplied measure specification. * * @param measureSpec the measure specification to extract the size from * @return the size in pixels defined in the supplied measure specification */ public static int getSize(int measureSpec) { return (measureSpec & ~MODE_MASK); } static int adjust(int measureSpec, int delta) { final int mode = getMode(measureSpec); int size = getSize(measureSpec); if (mode == UNSPECIFIED) { // No need to adjust size for UNSPECIFIED mode. return makeMeasureSpec(size, UNSPECIFIED); } size += delta; if (size < 0) { Log.e(VIEW_LOG_TAG, "MeasureSpec.adjust: new size would be negative! (" + size + ") spec: " + toString(measureSpec) + " delta: " + delta); size = 0; } return makeMeasureSpec(size, mode); } /** * Returns a String representation of the specified measure * specification. * * @param measureSpec the measure specification to convert to a String * @return a String with the following format: "MeasureSpec: MODE SIZE" */ public static String toString(int measureSpec) { int mode = getMode(measureSpec); int size = getSize(measureSpec); StringBuilder sb = new StringBuilder("MeasureSpec: "); if (mode == UNSPECIFIED) sb.append("UNSPECIFIED "); else if (mode == EXACTLY) sb.append("EXACTLY "); else if (mode == AT_MOST) sb.append("AT_MOST "); else sb.append(mode).append(" "); sb.append(size); return sb.toString(); } }

分析(1):通过该类的注释,我们知道: MeasureSpec封装父布局传给子布局的布局测量要求,是View测量的依据,它包括两个部分:测量模式和测量大小,它们封装在一个int数中,高两位是测量模式,低30位为大小,二者共同确定子布局的期望大小,为啥弄这么复杂呢?了解C语言嵌入式开发的都知道,通过移位操作将两个信息封装到一个int中,减少对象内存分配。
测量模式有三种:
1> UNSPECIFIED:子布局大小没有任何限制,主要用作系统内部测量,实际开发很少用到;
2> EXACTLY:父布局测出子View所期望的大小就是子View的大小,对应子View的布局参数为match_parent或者具体数值;
3> AT_MOST:父布局给出的期望大小size,子View大小不能超过这个size,对应子View布局参数为wrap_content,该模式下父布局只是限定了子View的大小上限,View的大小计算由自身确定,这里会引申出自定义View的布局参数为wrap_content不起作用的问题,后面会解释这个问题。
分析(2): makeMeasureSpec方法将mode和size封装到一个int里。
分析(3):getMode和getSize方法是通过位操作分别取出mode和size。

2.MeasureSpec的生成

前面说了那么多,这个父布局到底是怎么生成MeasureSpec给子View的呢?它是根据父布局的MeasureSpec以及子View的布局参数(一下简称LP)得到的,具体方法在ViewGroup的getChildMeasureSpec中。

   /**源码分析: getChildMeasureSpec
     *作用: 根据父视图的MeasureSpec & 布局参数LP,计算单个子View的MeasureSpec
     * 注:子view的大小由父view的MeasureSpec值 和 子view的LayoutParams属性 共同决定
     * Does the hard part of measureChildren: figuring out the MeasureSpec to
     * pass to a particular child. This method figures out the right MeasureSpec
     * for one dimension (height or width) of one child view.
     *
     * The goal is to combine information from our MeasureSpec with the
     * LayoutParams of the child to get the best possible results. For example,
     * if the this view knows its size (because its MeasureSpec has a mode of
     * EXACTLY), and the child has indicated in its LayoutParams that it wants
     * to be the same size as the parent, the parent should ask the child to
     * layout given an exact size

     * @param spec The requirements for this view
     * @param padding The padding of this view for the current dimension and
     *        margins, if applicable
     * @param childDimension How big the child wants to be in the current
     *        dimension
     * @return a MeasureSpec integer for the child
     */
    public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
      /* 注:@param spec 父布局的测量信息,由它的父布局传过来的
       * @param padding :父布局的padding
       * @param childDimension :子View的LP参数
       */
        //父布局的测量模式
        int specMode = MeasureSpec.getMode(spec);
        //父布局的大小
        int specSize = MeasureSpec.getSize(spec);
       //父布局给子View的剩余空间
        int size = Math.max(0, specSize - padding);

        int resultSize = 0;
        int resultMode = 0;

        //核心代码:通过父view的MeasureSpec和子view的LayoutParams确定子view的大小
        switch (specMode) {
        // Parent has imposed an exact size on us
        case MeasureSpec.EXACTLY://父布局size确定
            if (childDimension >= 0) {//子布局size确定
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
                //测量结果:EXACTLY+childsize
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//LP为match_parent
                // Child wants to be our size. So be it.
                resultSize = size;//大小为父布局size
                resultMode = MeasureSpec.EXACTLY;
                //测量结果:EXACTLY+parentsize
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//LP为wrap_content
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
                //测量模式:AT_MOST+parentsize
            }
            break;

        // Parent has imposed a maximum size on us
        // 当父布局的模式为AT_MOST时,父view强加给子view一个最大的值。(一般是父view设置为wrap_content) 
        case MeasureSpec.AT_MOST:
            if (childDimension >= 0) {//子布局大小为具体值
                // Child wants a specific size... so be it
                resultSize = childDimension;
                resultMode = MeasureSpec.EXACTLY;
                //测量结果:EXACTLY+childsize
            } else if (childDimension == LayoutParams.MATCH_PARENT) {//子view LP=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;
                //测量结果为AT_MOST+parentsize
            } else if (childDimension == LayoutParams.WRAP_CONTENT) {//注意:子view LP = wrap_content时候与LP = match_parent的测量结果相同
                // Child wants to determine its own size. It can't be
                // bigger than us.
                resultSize = size;
                resultMode = MeasureSpec.AT_MOST;
                //测量结果为AT_MOST+parentsize
            }
            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);
    }
子View测量结果生成逻辑.png

注意:由图可以看出,自定义View在LP=wrap_content和match_parent,在父布局AT_MOST测量模式下,效果是一样的,因此需要对自定义View在LP=wrap_conten时做特殊处理,指定默认值,这样就解决前面提到的wrap_content失效问题。

3.View的measure()方法

    /**
     * 源码分析:measure()
     * 定义:Measure过程的入口;属于View.java类 & final类型,即子类不能重写此方法
     * 作用:基本测量逻辑的判断
     * 

* This is called to find out how big a view should be. The parent * supplies constraint information in the width and height parameters. *

* *

* measure方法最终还是会调用onMeasure,正真的测量实现是在onMeasure实现,覆写onMeasure方法必须执行setMeasuredDimension()设置View的测量宽高。 * The actual measurement work of a view is performed in * {@link #onMeasure(int, int)}, called by this method. Therefore, only * {@link #onMeasure(int, int)} can and must be overridden by subclasses. *

* * * @param widthMeasureSpec Horizontal space requirements as imposed by the * parent * @param heightMeasureSpec Vertical space requirements as imposed by the * parent * * @see #onMeasure(int, int) */ 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 //避免重复测量,尝试读取缓存,key值有宽高共同决定 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); //上面的标志位都是为了确定这个view是否需要重新测量 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) { //如果缓存没有命中,则调用onMeasure重新测量 // 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; } // flag not set, setMeasuredDimension() was not invoked, we raise // an exception to warn the developer //这里检测测量标记是否置位,如果没有置位,则表示setMeasuredDimension没有调用,抛异常,所以在自定义View的OnMeasure方法里必须调用setMeasuredDimension方法 if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) { throw new IllegalStateException("View with id " + getId() + ": " + getClass().getName() + "#onMeasure() did not set the" + " measured dimension by calling" + " setMeasuredDimension()"); } mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } //暂存本次测量结果用于重复测量判断 mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; //本次测量结果放入缓存 mMeasureCache.put(key, ((long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); // suppress sign extension }

measure的流程:
1>判断是否需要重新测量,如果需要,则走2>,否则走3>;
2>读取缓存:如果缓存命中,则读取缓存值,解析出宽高spec信息作为本次测量结果,然后通过setMeasuredDimensionRaw()设置测量mMeasuredWidth和mMeasuredHeight;如果未命中,则执行onMeasure方法,在onMeasure方法里面需要执行setMeasuredDimension()方法设置测量宽高;
3>保存本次测量结果并存入缓存
4>measure执行的最终测量大小存放在mMeasuredWidth和mMeasuredHeight中。

4.View的onMeasure()方法

/**
  * 分析:onMeasure()
  * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
  *      b. 存储测量后的View宽 / 高:setMeasuredDimension()
  **/ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
   setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
    // setMeasuredDimension() :获得View宽/高的测量值 
    // 传入的参数通过getDefaultSize()获得
  }

 /**
  * 分析:setMeasuredDimension()
  * 作用:存储测量后的View宽 / 高
  * 注:该方法即为我们重写onMeasure()所要实现的最终目的
  **/
  protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
        // 将测量后子View的宽 / 高值进行传递
        mMeasuredWidth = measuredWidth;  
        mMeasuredHeight = measuredHeight; 
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
  } 
  // 由于setMeasuredDimension()的参数是从getDefaultSize()获得的
// 下面我们继续看getDefaultSize()的介绍

 /**
  * 分析:getDefaultSize()
  * 作用:根据View宽/高的测量规格计算View的宽/高值
  **/
  public static int getDefaultSize(int size, int measureSpec) {  

        // 参数说明:
        // size:提供的默认大小
        // measureSpec:宽/高的测量规格(含模式 & 测量大小)

        // 设置默认大小
       int result = size;
      // 获取宽/高测量规格的模式 & 测量大小
       int specMode = MeasureSpec.getMode(measureSpec);  
       int specSize = MeasureSpec.getSize(measureSpec);  
          
       switch (specMode) {  
           // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
           case MeasureSpec.UNSPECIFIED:  
                result = size;  
                break;  
           // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
           case MeasureSpec.AT_MOST:  
           case MeasureSpec.EXACTLY:  
                result = specSize;  
                break;  
            }  
         // 返回View的宽/高值
        return result;  
   }

getDefaultSize中的size 参数为getSuggestedMinimumHeight/Width()方法得到:

protected int getSuggestedMinimumHeight() { 
        //如果设置背景,则是背景高和mMinHeight的较大值
       //没设置背景则是mMinHeight
        return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
    }

(二)ViewGroup的measure流程

1>(API26)ViewGroup没有覆写measure和onMeasure方法,所以默认情况下measure流程和View的一致
2> ViewGroup不仅要测量自己,还要测量子View,ViewGroup测量子View的方法为measureChildren.
3>和自定义View一样,自定义ViewGroup也需要覆写onMeasure方法,根据子View的测量结果,按照自己的逻辑合并子View的宽高,确定自身的宽高。

/**测量子View
     * Ask all of the children of this view to measure themselves, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * We skip children that are in the GONE state The heavy lifting is done in
     * getChildMeasureSpec.
     *
     * @param widthMeasureSpec The width requirements for this view
     * @param heightMeasureSpec The height requirements for this 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) {
                //忽略GONE掉的View,INVISIBLE的View仍然测量
                measureChild(child, widthMeasureSpec, heightMeasureSpec);
            }
        }
    }

    /**测量单个Child
     * Ask one of the children of this view to measure itself, taking into
     * account both the MeasureSpec requirements for this view and its padding.
     * The heavy lifting is done in getChildMeasureSpec.
     *
     * @param child The child to measure
     * @param parentWidthMeasureSpec The width requirements for this view
     * @param parentHeightMeasureSpec The height requirements for this view
     */
     protected void measureChild(View child, int parentWidthMeasureSpec,
            int parentHeightMeasureSpec) {
        final LayoutParams lp = child.getLayoutParams();
        //根据parentMeasureSpec和子ViewLP生成子View的MeasureSpec,具体代码已经在MeasureSpec生成中分析过
        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
                mPaddingLeft + mPaddingRight, lp.width);
        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
                mPaddingTop + mPaddingBottom, lp.height);
        //子View各自测量
        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
    }

至此,ViewGroup的measure流程分析完毕,分析完原理,我们最终的目的还是为了学以致用,下面介绍View和ViewGroup的onMeasure的基本套路,这里只介绍流程,具体的实践后面的博客中会根据案例具体实现。

(三)覆写onMeasure方法的基本流程

1.View的onMeasure()方法基本流程:

1>拿到父View传过来的spec,解析出size;
2>对LP=wrap_content的情况,设置默大小;
3>根据自己的逻辑,如等比例宽高、宽高最值限定等等逻辑,结合size,得到最终的resultsize;
4>执行setMeasuredDimension(resultsize)设置测量结果;

//onMeasure伪代码
 private int mDefaultWidth = 200;
    private int mDefaultHeight = 400;
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //Step1:拿到父View期望的大小
        int resultWidth = 0;
        int resultHeight = 0;
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
        //先赋值
        resultWidth = widthSize;
        resultHeight = heightSize;
        //Step2:wrap_content处理
        if (getLayoutParams().width == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //在这里实现计算需要wrap_content时需要的宽
            resultWidth = mDefaultWidth;
        }
        if (getLayoutParams().height == ViewGroup.LayoutParams.WRAP_CONTENT) {
            //在这里实现计算需要wrap_content时需要的高
            resultHeight = mDefaultHeight;
        }
        //step3:自己定义View的逻辑,如宽高比,大小限制等等
        resultHeight = resultWidth;
        //step4:设置测量结果
        setMeasuredDimension(resultWidth, resultHeight);
    }

上面的代码简单实现了宽高比为1的自定义View,除了第三步,其他三步为固定套路,可以直接用。

2.覆写View的onMeasure()方法基本流程:

1>遍历所有子View,存放它们的大小;
2>根据自己的逻辑,合并子View的大小,得到最终ViewGroup的大小;
3>:设置大小。

@Override
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
        // 定义存放测量后的View宽/高的变量
        int widthMeasure ;
        int heightMeasure ;
        // Step1. 遍历所有子View(child.measure) or 测量measureChildren()
        measureChildren(widthMeasureSpec, heightMeasureSpec);
        // Step2. 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值
        
         ... // 核心部分自身实现

        // Step3. 存储测量后View宽/高的值:调用setMeasuredDimension()  
        // 类似单一View的过程,此处不作过多描述
        setMeasuredDimension(widthMeasure,  heightMeasure);  
  }

总结:希望读者读完measure源码,对View/ViewGroup的测量原理有更清晰的认识,有关于measure方法的应用场景,一般在自定义ViewGroup中结合layout使用,关于View的Layout原理,在下一篇博客中会详细研究。

你可能感兴趣的:(Android进阶之自定义View原理(一)View的Measure过程)