Android笔记 自定义View(二):View和ViewGroup介绍

参考资料:

http://www.gcssloop.com/timeline  

http://blog.csdn.net/xmxkf/article/details/51490283  

https://blog.csdn.net/carson_ho/article/details/56009827

目录

一、前言

1、继承View及其子类。

2、继承ViewGroup及其子类。

二、实现流程

1、构造函数:

2、onMeasure()测量View的大小

2.1、为什么要测量View的大小?

2.2、测量View的流程?

2.3、MeasureSpec介绍

2.4、ViewGroup的onMeasure

2.5、View的onMeasure

3、onSizeChanged确定View大小

4、onLayout计算View布局位置

4.1、layout()

4.2、onLayout()

5、onDraw()绘制内容

6、对外提供操作方法和监听回调

三、总结


一、前言

自定义View可以分为以下两类:

1、继承View及其子类。

我们知道android中所有UI控件的基类都是view,如:Button、TextView等。我们自定义的view可以直接继承View也可以继承它的子类Button、TextView等来实现我们需要的效果,这类View的特点是不包含子View。一般我们也可以利用图片或者组合动画来实现,但是在性能和实现过程方面有一些差距。

2、继承ViewGroup及其子类。

查看源码可知ViewGroup也是View的子类。继承ViewGroup的自定义View一般是利用现有的组件根据特定的布局方式来组成新的组件。包含子View。例如:应用底部导航条中的条目,一般都是上面图标(ImageView),下面文字(TextView),那么这两个就可以用自定义ViewGroup组合成为一个Veiw,提供两个属性分别用来设置文字和图片,使用起来会更加方便。

Android笔记 自定义View(二):View和ViewGroup介绍_第1张图片

二、实现流程

自定义View绘制流程简化图如下:

Android笔记 自定义View(二):View和ViewGroup介绍_第2张图片

1、构造函数:

构造函数是View的入口,可以用于初始化一些的内容,和获取自定义属性。在AS(Adnroid studio)中创建自定义View时,会提示需要实现四个构造函数:

Android笔记 自定义View(二):View和ViewGroup介绍_第3张图片

通过查看源码可以知道,每个构造函数的作用:

public class CircleView extends RelativeLayout {

    public CircleView(Context context) {
        this(context,null);
        // 如果View是在Java代码里面new的,则调用
    }

    public CircleView(Context context, AttributeSet attrs) {
        this(context, attrs,0);
        //如果View是在xml里声明的,则调用
    }

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        // 不会自动调用,一般是在第二个构造函数里主动调用,如View有style属性时
    }
    
    /*@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public CircleView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        //API21之后才使用,不会自动调用
    }*/
}

在构造函数中我们可以定义一些绘制相关的属性,我们可以利用attr去自定义一些属性。

ps:有一点比较特殊在ImageView中的一些setBitmapXXX()方法会优先构造函数执行。我们在继承ImageView时要做相关的处理。

2、onMeasure()测量View的大小

2.1、为什么要测量View的大小?

创建一个View(执行构造方法)的时候不需要测量控件的大小,只有将这个view放入一个容器(父控件)中的时候才需要测量,而这个测量方法就是父控件唤起调用的。当控件的父控件要放置该控件的时候,父控件会调用子控件的onMeasure方法询问子控件:“你有多大的尺寸,我要给你多大的地方才能容纳你?”,然后传入两个参数(widthMeasureSpec和heightMeasureSpec),这两个参数就是父控件告诉子控件可获得的空间以及关于这个空间的约束条件(MeasureSpec下面会介绍)。子控件拿着这些条件就能正确的测量自身的宽高了。 

2.2、测量View的流程?

上面说到onMeasure方法是由父控件调用的,所有父控件都是ViewGroup的子类,ViewGroup是一个抽象类,它里面有一个抽象方法onLayout,这个方法的作用就是摆放它所有的子控件(安排位置),因为是抽象类,不能直接new对象,所以我们在布局文件中可以使用View但是不能直接使用 ViewGroup。

在给子控件确定位置之前,必须要获取到子控件的大小(只有确定了子控件的大小才能正确的确定上下左右四个点的坐标),而ViewGroup并没有重写View的onMeasure方法,也就是说抽象类ViewGroup没有为子控件测量大小的能力,它只能测量自己的大小。但是既然ViewGroup是一个能容纳子控件的容器,系统当然也考虑到测量子控件的问题,所以ViewGroup提供了三个测量子控件相关的方法(measuireChildren\measuireChild\measureChildWithMargins),只是在ViewGroup中没有调用它们,所以它本身不具备为子控件测量大小的能力,但是他有这个潜力。

为什么都有测量子控件的方法了而ViewGroup中不直接重写onMeasure方法,然后在onMeasure中调用呢?因为不同的容器摆放子控件的方式不同,比如RelativeLayout,LinearLayout这两个ViewGroup的子类,它们摆放子控件的方式不同,有的是线性摆放,而有的是叠加摆放,这就导致测量子控件的方式会有所差别,所以ViewGroup就干脆不直接测量子控件,他的子类要测量子控件就根据自己的布局特性重写onMeasure方法去测量。这么看来ViewGroup提供的三个测量子控件的方法岂不是没有作用?答案是NO,既然提供了就肯定有作用,这三个方法只是按照一种通用的方式去测量子控件,很多ViewGruop的子类测量子控件的时候就使用了ViewGroup的measureChildxxx系列方法;还有一个作用就是为我们自定义ViewGroup提供方便。

测量的时候父控件的onMeasure方法会遍历他所有的子控件,挨个调用子控件的measure方法,measure方法会调用onMeasure,然后会调用setMeasureDimension方法保存测量的大小,一次遍历下来,第一个子控件以及这个子控件中的所有子控件都会完成测量工作;然后开始测量第二个子控件…;最后父控件所有的子控件都完成测量以后会调用setMeasureDimension方法保存自己的测量大小。值得注意的是,这个过程不只执行一次,也就是说有可能重复执行,因为有的时候,一轮测量下来,父控件发现某一个子控件的尺寸不符合要求,就会重新测量一遍。

2.3、MeasureSpec介绍

上面说到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;
    /**
     * 父控件已为子控件确定了一个确切的大小,孩子将被给予这些界限,不管子控件自己希望的是多大
     */
    public static final int EXACTLY = 1 << MODE_SHIFT;
    /**
     * 父控件会给子控件尽可能大的尺寸
     */
    public static final int AT_MOST = 2 << MODE_SHIFT;

    /**
     * 根据所提供的大小和模式创建一个测量规范
     */
    public static int makeMeasureSpec(int size, int mode) {
        if (sUseBrokenMakeMeasureSpec) {
            return size + mode;
        } else {
            return (size & ~MODE_MASK) | (mode & MODE_MASK);
        }
    }
    /**
     * 从所提供的测量规范中提取模式
     */
    public static int getMode(int measureSpec) {
        return (measureSpec & MODE_MASK);
    }
    /**
     * 从所提供的测量规范中提取尺寸
     */
    public static int getSize(int measureSpec) {
        return (measureSpec & ~MODE_MASK);
    }
    ...
}

从源码中我们知道,MeasureSpec其实就是尺寸和模式通过各种位运算计算出的一个整型值,它提供了三种模式,还有三个方法(合成约束、分离模式、分离尺寸)。在onMeasure函数中,我们可以使用MeasureSpec类来进行位运算得到SpecMode和SpecSize。可以从onMeasure的两个参数中取出宽高的相关数据:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthsize  MeasureSpec.getSize(widthMeasureSpec);      //取出宽度的确切数值
    int widthmode  MeasureSpec.getMode(widthMeasureSpec);      //取出宽度的测量模式
    
    int heightsize  MeasureSpec.getSize(heightMeasureSpec);    //取出高度的确切数值
    int heightmode  MeasureSpec.getMode(heightMeasureSpec);    //取出高度的测量模式
}

我们可以看到两个int参数widthMeasureSpecheightMeasureSpec。Java中int类型是4个字节,也就是32位,这两个int值中的高2位代表SpecMode,也就是测量模式,低32位则是代表SpecSize也就是在某个测量模式下宽高的实际值。

以数值1080(二进制为: 1111011000)为例(其中模式和实际数值是连在一起的,为了展示将他们分开):

模式名称 模式数值 实际数值
UNSPECIFIED 00 000000000000000000001111011000
EXACTLY 01 000000000000000000001111011000
AT_MOST 10 000000000000000000001111011000

从表中可以看出,二进制2位可以表达四种情况,测量模式只有三种情况,每一种情况有其特殊的意思:

模式 布局参数 二进制数值 描述
UNSPECIFIED   00 默认值,父控件没有对子控件施加任何约束,子控件可以得到任意想要的大小(使用较少)。
EXACTLY(完全) match_parent/具体宽高值 01 父容器测量出View需要的精确大小,子控件将被限定在给定的边界里而忽略它本身大小。特别说明如果是填充父容器,说明父控件已经明确知道子控件想要多大的尺寸了(就是剩余的空间都要了)
AT_MOST(至多) wrap_content 10 父容器并不知道子控件到底需要多大尺寸(具体值),但是父容器会设置上限,上限一般为父View大小。

2.4、ViewGroup的onMeasure

通过上面的介绍,我们知道,如果要自定义ViewGroup就必须重写onMeasure方法,在这里测量子控件的尺寸。子控件的尺寸怎么测量呢?ViewGroup中提供了三个关于测量子控件的方法:

/**
  *遍历ViewGroup中所有的子控件,调用measuireChild测量宽高
  */
 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);
        }
    }
}

/**
* 测量某一个child的宽高
*/
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);
}

/**
* 测量某一个child的宽高,考虑margin值
*/
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);
}

这三个方法分别做了那些工作注释已经说明。measureChildren 就是遍历所有子控件挨个测量,最终测量子控件的方法就是measureChild 和measureChildWithMargins 了。

  • measureChildWithMargins跟measureChild的区别?

区别就是父控件支不支持margin属性。支不支持margin属性对子控件的测量是有影响的,比如我们的屏幕是1080x1920的,子控件的宽度为填充父窗体,如果使用了marginLeft并设置值为100; 在测量子控件的时候,如果用measureChild,计算的宽度是1080,而如果是使用measureChildWithMargins,计算的宽度是1080-100 = 980。

ViewGroup中有两个内部类ViewGroup.LayoutParams和ViewGroup.MarginLayoutParams,MarginLayoutParams继承自LayoutParams ,这两个内部类就是VIewGroup的布局参数类,比如我们在LinearLayout等布局中使用的layout_width\layout_hight等以“layout_ ”开头的属性都是布局属性。在View中有一个mLayoutParams的变量用来保存这个View的所有布局属性。

  • 为什么LayoutParams 类要定义在ViewGroup中? 
      大家都知道ViewGroup是所有容器的基类,一个控件需要被包裹在一个容器中,这个容器必须提供一种规则控制子控件的摆放,比如你的宽高是多少,距离那个位置多远等。所以ViewGroup有义务提供一个布局属性类,用于控制子控件的布局属性。

  • 为什么View中会有一个mLayoutParams 变量? 
      我们在之前学习自定义控件的时候学过自定义属性,我们在构造方法中,初始化布局文件中的属性值,我们姑且把属性分为两种。一种是本View的绘制属性,比如TextView的文本、文字颜色、背景等,这些属性是跟View的绘制相关的。另一种就是以“layout_”打头的叫做布局属性,这些属性是父控件对子控件的大小及位置的一些描述属性,这些属性在父控件摆放它的时候会使用到,所以先保存起来,而这些属性都是ViewGroup.LayoutParams定义的,所以用一个变量保存着。

getChildMeasureSpec方法

measureChildWithMargins跟measureChild 都调用了这个方法,其作用就是通过父控件的宽高约束规则和父控件加在子控件上的宽高布局参数生成一个子控件的约束。我们知道View的onMeasure方法需要两个参数(父控件对View的宽高约束),这个宽高约束就是通过这个方法生成的。有人会问为什么不直接拿着子控件的宽高参数去测量子控件呢?打个比方,父控件的宽高约束为wrap_content,而子控件为match_perent,是不是很有意思,父控件说我的宽高就是包裹我的子控件,我的子控件多大我就多大,而子控件说我的宽高填充父窗体,父控件多大我就多大。最后该怎么确定大小呢?所以我们需要为子控件重新生成一个新的约束规则。只要记住,子控件的宽高约束规则是父控件调用getChildMeasureSpec方法生成。 

getChildMeasure方法代码不多,也比较简单,就是几个switch将各种情况考虑后生成一个子控件的新的宽高约束,这个方法的结果能够用一个表来概括:

父控件的约束规则 子控件的宽高属性 子控件的约束规则 说明
EXACTLY(父控件是填充父窗体,或者具体size值) 具体的size(20dip)/MATCH_PARENT EXACTLY 子控件如果是具体值,约束尺寸就是这个值,模式为确定的;子控件为填充父窗体,约束尺寸是父控件剩余大小,模式为确定的。
  WRAP-CONTENT AT_MOST 子控件如果是包裹内容,约束尺寸值为父控件剩余大小 ,模式为至多
AT_MOST(父控件是包裹内容) 具体的size(20dip) EXACTLY 子控件如果是具体值,约束尺寸就是这个值,模式为确定的;
  MATCH_PARENT/WRAP_CONTENT AT_MOST 子控件为填充父窗体或者包裹内容 ,约束尺寸是父控件剩余大小 ,模式为至多
UNSPECIFIED(父控件未指定) 具体的size(20dip) EXACTLY 子控件如果是具体值,约束尺寸就是这个值,模式为确定的;
  MATCH_PARENT/WRAP_CONTENT UNSPECIFIED 子控件为填充父窗体或者包裹内容 ,约束尺寸0,模式为未指定

进行了上面的步骤,接下来就是在measureChildWithMarginsh或者measureChild中 调用子控件的measure方法测量子控件的尺寸了。

2.5、View的onMeasure

View中onMeasure方法已经默认为我们的控件测量了宽高,我们看看它做了什么工作:

protected void onMeasure( int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension( getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
/**
 * 为宽度获取一个建议最小值
 */
protected int getSuggestedMinimumWidth () {
    return (mBackground == null) ? mMinWidth : max(mMinWidth , mBackground.getMinimumWidth());
}
/**
 * 获取默认的宽高值
 */
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;
}

从源码可以知道:

  • 如果View的宽高模式为未指定,他的宽高将设置为android:minWidth/Height =”“值与背景宽高值中较大的一个;
  • 如果View的宽高 模式为 EXACTLY (具体的size ),最终宽高就是这个size值;
  • 如果View的宽高模式为EXACTLY (填充父控件 ),最终宽高将为填充父控件;
  • 如果View的宽高模式为AT_MOST (包裹内容),最终宽高也是填充父控件。

也就是说如果我们的自定义控件在布局文件中,只需要设置指定的具体宽高,或者MATCH_PARENT 的情况,我们可以不用重写onMeasure方法。但如果自定义控件需要设置包裹内容WRAP_CONTENT ,我们需要重写onMeasure方法,为控件设置需要的尺寸;默认情况下WRAP_CONTENT 的处理也将填充整个父控件。

setMeasuredDimension方法:

onMeasure方法最后需要调用setMeasuredDimension方法来保存测量的宽高值,如果不调用这个方法,可能会产生不可预测的问题。

好了。onMeasure介绍到这里,后面在进行补充。

3、onSizeChanged确定View大小

/**
 * This is called during layout when the size of this view has changed. If
 * you were just added to the view hierarchy, you're called with the old
 * values of 0.
 *
 * @param w Current width of this view.
 * @param h Current height of this view.
 * @param oldw Old width of this view.
 * @param oldh Old height of this view.
 */
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
}

从官方文档中看出,onSizeChanged是在View的大小改变时调用。继续查看源码当View调用setTop、setBottpm等时onSizeChanged会被调用。

可以看出,它又四个参数,分别为 宽度,高度,上一次宽度,上一次高度。这个函数比较简单,我们只需关注 宽度(w), 高度(h) 即可,这两个参数就是View最终的大小。如果是第一次调用oldw、oldh的值为0。

4、onLayout计算View布局位置

我们知道,整棵View树的根节点是DecorView,它是一个FrameLayout,所以它是一个ViewGroup,所以整棵View树的测量是从一个ViewGroup对象的layout()开始的。

4.1、layout()

  • ViewGroup的layout()

源码:

    @Override
    public final void layout(int l, int t, int r, int b) {
        if (!mSuppressLayout && (mTransition == null || !mTransition.isChangingLayout())) {
            if (mTransition != null) {
                mTransition.layoutChange(this);
            }
            //内部实际上是调用View的layout()
            super.layout(l, t, r, b);
        } else {
            // record the fact that we noop'd it; request layout when transition finishes
            mLayoutCalledWhileSuppressed = true;
        }
    }

从源码中可以看出实际上调用的还是View的layout()方法,我们继续往下看:

  • View的layout() 

先看官方文档的描述:

Assign a size and position to a view and all of its descendants.

大致意思就是:确定View的大小和位置。在看代码:

/**
     * 作用:确定View本身的位置,即设置View本身的四个顶点位置
     */
    public void layout(int l, int t, int r, int b) {

        //根据一些flag,如果有需要则进一步measure
        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;

        // 1. 确定View的位置:setFrame() / setOpticalFrame()
        // 即初始化四个顶点的值、判断当前View大小和位置是否发生了变化
        boolean changed = isLayoutModeOptical(mParent) ?
                setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

        // 2. 若视图的大小 & 位置发生变化
        // 会重新确定该View所有的子View在父容器的位置:onLayout()
        if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
            // 对于单一View的laytou过程:由于单一View是没有子View的,故onLayout()是一个空实现->>分析3
            // 对于ViewGroup的laytou过程:由于确定位置与具体布局有关,所以onLayout()在ViewGroup为1个抽象方法,需重写实现(后面会详细说)
            onLayout(changed, l, t, r, b);

            //回调layoutChange事件
            ......
        }
        //标记为已经执行过layout;
        ......
    }

/**
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */ 
  protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
    // 从而确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    mRenderNode.setLeftTopRightBottom(mLeft, mTop, mRight, mBottom);

    }

/**
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */ 
  private boolean setOpticalFrame(int left, int top, int right, int bottom) {

        Insets parentInsets = mParent instanceof View ?
                ((View) mParent).getOpticalInsets() : Insets.NONE;

        Insets childInsets = getOpticalInsets();

        // 内部实际上是调用setFrame()
        return setFrame(
                left   + parentInsets.left - childInsets.left,
                top    + parentInsets.top  - childInsets.top,
                right  + parentInsets.left + childInsets.right,
                bottom + parentInsets.top  + childInsets.bottom);
    }

从源码可以看出layout()最终通过setFrame()方法去确定View的大小和位置,如果发生改变则调用onLayout()方法。

4.2、onLayout()

  • View的onLayout()

查看源码:

/**
  * 分析3:onLayout()
  * 注:对于单一View的laytou过程
  *    a. 由于单一View是没有子View的,故onLayout()是一个空实现
  *    b. 由于在layout()中已经对自身View进行了位置计算,所以单一View的layout过程在layout()后就已完成了
  */ 
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
}  

由于继承View的自定义控件不包含子控件,所以View的onLayout()方法是空实现。我们再去看第二类继承ViewGroup的自定义组件的onLayout。

  • ViewGroup的onLayout()
    /**
    * 作用:计算该ViewGroup包含所有的子View在父容器的位置()
    * 注:
    *    a. 定义为抽象方法,需重写,因:子View的确定位置与具体布局有关,所以onLayout()在ViewGroup没有实现
    *    b. 在自定义ViewGroup时必须复写onLayout()!!!!!
    *    c. 复写原理:遍历子View 、计算当前子View的四个位置值 & 确定自身子View的位置(调用子View layout())
    * */
    @Override
    protected abstract void onLayout(boolean changed,
                                     int l, int t, int r, int b);

ViewGroup中的onLayout方法是抽象方法,当我们的自定义View继承ViewGroup时需要我们自己去实现,下面给出LinearLayout的实现,我们自定义View可以参考:

/**
  * 源码分析:LinearLayout复写的onLayout()
  * 注:复写的逻辑 和 LinearLayout measure过程的 onMeasure()类似
  */ 
  @Override
  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);
      }
  }
      // 由于垂直 / 水平方向类似,所以此处仅分析垂直方向(Vertical)的处理过程 ->>分析1

/**
  * 分析1:layoutVertical(l, t, r, b)
  */
    void layoutVertical(int left, int top, int right, int bottom) {

        // 子View的数量
        final int count = getVirtualChildCount();

        // 1. 遍历子View
        for (int i = 0; i < count; i++) {
            final View child = getVirtualChildAt(i);
            if (child == null) {
                childTop += measureNullChild(i);
            } else if (child.getVisibility() != GONE) {

                // 2. 计算子View的测量宽 / 高值
                final int childWidth = child.getMeasuredWidth();
                final int childHeight = child.getMeasuredHeight();

                // 3. 确定自身子View的位置
                // 即:递归调用子View的setChildFrame(),实际上是调用了子View的layout() ->>分析2
                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
                        childWidth, childHeight);

                // childTop逐渐增大,即后面的子元素会被放置在靠下的位置
                // 这符合垂直方向的LinearLayout的特性
                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);

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

/**
  * 分析2:setChildFrame()
  */
    private void setChildFrame( View child, int left, int top, int width, int height){

        // setChildFrame()仅仅只是调用了子View的layout()而已
        child.layout(left, top, left ++ width, top + height);

        }
    // 在子View的layout()又通过调用setFrame()确定View的四个顶点
    // 即确定了子View的位置
    // 如此不断循环确定所有子View的位置,最终确定ViewGroup的位置

好了,关于布局的流程先介绍到这里。下面我进入绘制流程。

5、onDraw()绘制内容

onDraw是实际绘制的部分,也就是我们真正关心的部分,使用的是Canvas绘图。

@Override
protected void onDraw(Canvas canvas) {
    super.onDraw(canvas);
    //实现
}

关于Canvas绘图也是重点,会在后续章节中进行介绍。

6、对外提供操作方法和监听回调

自定义完View之后,一般会对外暴露一些接口,用于控制View的状态等,或者监听View的变化.

本内容会在后续文章中以实例的方式进讲解。

三、总结

本章大致介绍了自定义View的分类和测量、布局、绘制三大流程,其中绘制的内容涉及较多,会在后续的章节中介绍。由于本人能力和知识有限,在理解前辈的文章过程中可能有错误,如有发现欢迎指出,我会及时修正。

祝:工作愉快!

 

 

你可能感兴趣的:(Android笔记 自定义View(二):View和ViewGroup介绍)