变身大佬的重要一环,就是自定义View!

640?wx_fmt=jpeg


/   今日科技快讯   /


北京时间昨晚八点,华为在德国发布了新一代旗舰手机Mate30系列。该产品系列搭载了华为麒麟990 5G芯片,使用7nm+ EUV工艺,相比上代芯片,GPU性能提升高达39%,能效提升高达32%。 同时华为Mate30 Pro 5G版还支持5G SA/NSA双架构,支持双卡双待,5G与4G卡可同时在线。


/   作者简介   /


时间过得就是如此之外,转眼之间又要到开心的周末了,祝大家周末愉快!


本篇文章来自极速24号的投稿,分享了自定义View中你不得不了解的知识点,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。


极速24号的博客地址:

https://juejin.im/user/56da36ea731956005d7fcb41


/   什么是自定义View   /


定义


在Android系统中,界面中所有能看到的元素都是View。默认情况下,Android系统为开发者提供了很多View,比如用于展示文本信息的TextView,用于展示图片的ImageView等等。但有时,这并不能满足开发者的需求,例如,开发者想要用一个饼状图来展示一组数据,这时如果用系统提供的View就不能实现了,只能通过自定义View来实现。那到底什么是自定义View呢?


自定义View就是通过继承View或者View的子类,并在新的类里面实现相应的处理逻辑(重写相应的方法),以达到自己想要的效果。


继承结构


Android中的所有UI元素都是View的子类:


640?wx_fmt=png


PS:由于涉及的类太多,如果将所有涉及到的类全部加到类图里面,类图将十分大,所以此处只列出了View的直接子类。


视图体系用到的设计模式


Android View体系如下:


640?wx_fmt=png


仔细观察,你会发现,Android View的体系结构和设计模式中的组合模式的结构如出一辙:


640?wx_fmt=png


Android View体系结构中的ViewGroup对应于组合模式中抽象构件(Component和Composite),Android View体系结构中的View对应于组合模式中的叶子构件(Leaf):


640?wx_fmt=png


/   为什么要自定义View   /


大多数情况下,开发者常常会因为下面四个原因去自定义View:


  1. 让界面有特定的显示风格、效果
  2. 让控件具有特殊的交互方式
  3. 优化布局
  4. 封装

让界面有特定的显示风格、效果


默认情况下,Android系统为开发者提供了很多控件,但有时,这并不能满足开发者的需求。例如,开发者想要用一个饼状图来展示一组数据,这时如果用系统提供的View就不能实现了,只能通过自定义View来实现。


让控件具有特殊的交互方式


默认情况下,Android系统为开发者提供的控件都有属于它们自己的特定的交互方式,但有时,控件的默认交互方式并不能满足开发者的需求。例如,开发者想要缩放ImageView中的图片内容,这时如果用系统提供的ImageView就不能实现了,只能通过自定义ImageView来实现。


优化布局


有时,有些布局如果用系统提供的控件实现起来相当复杂,需要各种嵌套,虽然最终也能实现了想要的效果,但性能极差,此时就可以通过自定义View来减少嵌套层级、优化布局。


封装


有些控件可能在多个地方使用,如大多数App里面的底部Tab,像这样的经常被用到的控件就可以通过自定义View将它们封装起来,以便在多个地方使用。


/   如何自定义View   /


在说「如何自定义View?」之前,我们需要知道「自定义View都包括哪些内容」?


自定义View包括三部分内容:


  1. 布局(Layout)
  2. 绘制(Drawing)
  3. 触摸反馈(Event Handling)


布局阶段:确定View的位置和尺寸。

绘制阶段:绘制View的内容。

触摸反馈:确定用户点击了哪里。


其中布局阶段包括测量(measure)和布局(layout)两个过程,另外,布局阶段是为绘制和触摸反馈阶段做支持的,它并没有什么直接作用。正是因为在布局阶段确定了 View 的尺寸和位置,绘制阶段才知道往哪里绘制,触摸反馈阶段才知道用户点的是哪里。


在自定义View和自定义ViewGroup中,布局和绘制流程虽然整体上都是一样的,但在细节方面,自定义View和自定义ViewGroup还是不一样的,所以,接下来分两类进行讨论:


  • 自定义View布局、绘制流程
  • 自定义ViewGroup布局、绘制流程

自定义View布局、绘制流程


「自定义 View 布局、绘制」主要包括三个阶段:


  1. 测量阶段(measure)
  2. 布局阶段(layout)
  3. 绘制阶段(draw)

自定义View测量阶段


在View的测量阶段会执行两个方法(在测量阶段,View的父View会通过调用 View 的measure()方法将父View对View尺寸要求传进来。紧接着View的measure()方法会做一些前置和优化工作,然后调用View的onMeasure()方法,并通过onMeasure()方法将父View对View的尺寸要求传入。在自定义View中,只有需要修改View的尺寸的时候才需要重写onMeasure()方法。在onMeasure()方法中根据业务需求进行相应的逻辑处理,并在最后通过调用setMeasuredDimension()方法告知父View自己的期望尺寸):


  • measure()
  • onMeasure()


measure():调度方法,主要做一些前置和优化工作,并最终会调用onMeasure()方法执行实际的测量工作;


onMeasure():实际执行测量任务的方法,主要用与测量View尺寸和位置。在自定义View的onMeasure()方法中,View根据自己的特性和父View对自己的尺寸要求算出自己的期望尺寸,并通过setMeasuredDimension()方法告知父View自己的期望尺寸。


onMeasure()计算View期望尺寸方法如下:


参考父View的对View的尺寸要求和实际业务需求计算出View的期望尺寸:


  • 解析widthMeasureSpec

  • 解析heightMeasureSpec

  • 将「根据实际业务需求计算出View的尺寸」根据「父View的对View的尺寸要求」进行相应的修正得出View的期望尺寸(通过调用resolveSize()方法)


通过setMeasuredDimension()保存View的期望尺寸(实际上是通过setMeasuredDimension()告知父View自己的期望尺寸);


注意:


多数情况下,这里的期望尺寸就是View的最终尺寸。不过最终View的期望尺寸和实际尺寸是不是一样还要看它的父View会不会同意。View的父View最终会通过调用View的layout()方法告知View的实际尺寸,并且在layout()方法中View需要将这个实际尺寸保存下来,以便绘制阶段和触摸反馈阶段使用,这也是View需要在layout()方法中保存自己实际尺寸的原因——因为绘制阶段和触摸反馈阶段要使用啊!


自定义View布局阶段


在View的布局阶段会执行两个方法(在布局阶段,View的父View会通过调用View的layout()方法将View的实际尺寸(父View根据View的期望尺寸确定的View的实际尺寸)传给View,View需要在layout()方法中将自己的实际尺寸保存(通过调用View的setFrame()方法保存,在setFrame()方法中,又会通过调用onSizeChanged()方法告知开发者View的尺寸修改了)以便在绘制和触摸反馈阶段使用。保存View的实际尺寸之后,View的layout()方法又会调用View的onLayout()方法,不过View的onLayout()方法是一个空实现,因为它没有子View):


  • layout()
  • onLayout()


layout():保存View的实际尺寸。调用setFrame()方法保存View的实际尺寸,调用onSizeChanged()通知开发者View的尺寸更改了,并最终会调用onLayout()方法让子View布局(如果有子View的话。因为自定义View中没有子View,所以自定义View的onLayout()方法是一个空实现);


onLayout():空实现,什么也不做,因为它没有子View。如果是ViewGroup的话,在onLayout()方法中需要调用子View的layout()方法,将子View的实际尺寸传给它们,让子View保存自己的实际尺寸。因此,在自定义View中,不需重写此方法,在自定义ViewGroup中,需重写此方法。


注意:


layout()&onLayout()并不是「调度」与「实际做事」的关系,layout()和onLayout()均做事,只不过职责不同。


自定义View绘制阶段


在View的绘制阶段会执行一个方法——draw(),draw()是绘制阶段的总调度方法,在其中会调用绘制背景的方法drawBackground()、绘制主体的方法onDraw()、绘制子View的方法dispatchDraw()和绘制前景的方法onDrawForeground()。


draw():绘制阶段的总调度方法,在其中会调用绘制背景的方法drawBackground()、绘制主体的方法onDraw()、绘制子View的方法dispatchDraw()和绘制前景的方法onDrawForeground();


drawBackground():绘制背景的方法,不能重写,只能通过xml布局文件或者setBackground()来设置或修改背景;


onDraw():绘制View主体内容的方法,通常情况下,在自定义View的时候,只用实现该方法即可;


dispatchDraw():绘制子View的方法。同onLayout()方法一样,在自定义View中它是空实现,什么也不做。但在自定义ViewGroup中,它会调用ViewGroup.drawChild()方法,在ViewGroup.drawChild()方法中又会调用每一个子View的View.draw()让子View进行自我绘制;


onDrawForeground():绘制View前景的方法,也就是说,想要在主体内容之上绘制东西的时候就可以在该方法中实现。


注意:


Android里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。如,你在重叠的位置「先画圆再画方」和「先画方再画圆」所呈现出来的结果是不同的,具体表现为下表:


640?wx_fmt=png


640?wx_fmt=png


自定义View布局、绘制流程时序图


640?wx_fmt=png


 自定义ViewGroup布局、绘制流程


「自定义ViewGroup布局、绘制」主要包括三个阶段:


  1. 测量阶段(measure)
  2. 布局阶段(layout)
  3. 绘制阶段(draw)

自定义ViewGroup测量阶段


同自定义View一样,在自定义ViewGroup的测量阶段会执行两个方法:


  • measure()
  • onMeasure()


measure():调度方法,主要做一些前置和优化工作,并最终会调用onMeasure()方法执行实际的测量工作;


onMeasure() :实际执行测量任务的方法,与自定义View不同,在自定义ViewGroup的 onMeasure() 方法中,ViewGroup会递归调用子View的measure()方法,并通过measure()将ViewGroup对子View的尺寸要求(ViewGroup会根据开发者对子View的尺寸要求、自己的父View(ViewGroup的父View) 对自己的尺寸要求和自己的可用空间计算出自己对子View的尺寸要求)传入,对子View进行测量,并把测量结果临时保存,以便在布局阶段使用。测量出子View的实际尺寸之后,ViewGroup会根据子View的实际尺寸计算出自己的期望尺寸,并通过setMeasuredDimension()方法告知父View(ViewGroup 的父 View)自己的期望尺寸。


具体流程如下:


  1. 运行前,开发者在xml中写入对ViewGroup和ViewGroup子View的尺寸要求layout_xxx

  2. ViewGroup在自己的onMeasure()方法中,根据开发者在xml中写的对ViewGroup子View的尺寸要求、自己的父View(ViewGroup的父View) 对自己的尺寸要求和自己的可用空间计算出自己对子View的尺寸要求,并调用每个子View的measure()将ViewGroup对子View的尺寸要求传入,测量子View尺寸

  3. ViewGroup在子View计算出期望尺寸之后(在ViewGroup的onMeasure()方法中,ViewGroup递归调用每个子View的measure()方法,子View在自己的onMeasure()方法中会通过调用setMeasuredDimension()方法告知父View(ViewGroup)自己的期望尺寸),得出子View的实际尺寸和位置,并暂时保存计算结果,以便布局阶段使用

  4. ViewGroup根据子View的尺寸和位置计算自己的期望尺寸,并通过setMeasuredDimension()方法告知父View自己的期望尺寸。如果想要做的更好,可以在「ViewGroup根据子View的尺寸和位置计算出自己的期望尺寸」之后,再结合ViewGroup的父View对ViewGroup的尺寸要求进行修正(通过resolveSize()方法),这样得出的ViewGroup的期望尺寸更符合ViewGroup的父View对ViewGroup的尺寸要求


自定义ViewGroup布局阶段


同自定义 View 一样,在自定义 ViewGroup 的布局阶段会执行两个方法:


  • layout()
  • onLayout()


layout():保存ViewGroup的实际尺寸。调用setFrame()方法保存ViewGroup的实际尺寸,调用onSizeChanged()通知开发者ViewGroup的尺寸更改了,并最终会调用onLayout()方法让子View布局;


onLayout():ViewGroup会递归调用每个子View的layout()方法,把测量阶段计算出的子View的实际尺寸和位置传给子View,让子View保存自己的实际尺寸和位置。


自定义ViewGroup绘制阶段


同自定义View一样,在自定义ViewGroup的绘制阶段会执行一个方法——draw()。draw()是绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法onDraw()、绘制子View的方法 dispatchDraw()和绘制前景的方法onDrawForeground():


draw():绘制阶段的总调度方法,在其中会调用绘制背景的方法drawBackground()、绘制主体的方法onDraw()、绘制子View的方法dispatchDraw()和绘制前景的方法onDrawForeground();


在ViewGroup中,你也可以重写绘制主体的方法onDraw()、绘制子View的方法dispatchDraw()和绘制前景的方法onDrawForeground()。但大多数情况下,自定义ViewGroup是不需要重写任何绘制方法的。因为通常情况下,ViewGroup的角色是容器,一个透明的容器,它只是用来盛放子View的。


自定义ViewGroup布局、绘制流程时序图


640?wx_fmt=png


自定义View步骤


  1. 自定义属性的声明与获取
  2. 重写测量阶段相关方法(onMeasure())
  3. 重写布局阶段相关方法(onLayout()(仅ViewGroup需要重写))
  4. 重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground() 绘制前景)
  5. onTouchEvent()
  6. onInterceptTouchEvent()(仅ViewGroup有此方法)


/   实战演练   /


自定义View


自定义View的绘制内容


自定义View,它的内容是「三个半径不同、颜色不同的同心圆」,效果图如下:


640?wx_fmt=png


自定义属性的声明与获取


 
  

//在 xml 中自定义 View 属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--CircleView-->
    <declare-styleable name="CircleView">
        <attr name="circle_radius" format="dimension" />
        <attr name="outer_circle_color" format="reference|color" />
        <attr name="middle_circle_color" format="reference|color" />
        <attr name="inner_circle_color" format="reference|color" />
    </declare-styleable>
</resources>


 
  

//在 View 构造函数中获取自定义 View 属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();


重写测量阶段相关方法(onMeasure())


由于不需要自定义View的尺寸,所以,不用重写该方法。


重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))


由于没有子View需要布局,所以,不用重写该方法。


重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground()绘制前景)


 
  

//重写 onDraw() 方法,自定义 View 内容
@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(mOuterCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
    mPaint.setColor(mMiddleCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
    mPaint.setColor(mInnerCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}


onTouchEvent()


由于View不需要和用户交互,所以,不用重写该方法。


onInterceptTouchEvent()(仅ViewGroup有此方法)


ViewGroup的方法。


最终效果如下:


640?wx_fmt=png


此时,即使你在xml中将CircleView的宽、高声明为「match_parent」,你会发现最终的显示效果都是一样的。


主要原因是:默认情况下,View的onMeasure()方法在通过setMeasuredDimension()告知父View自己的期望尺寸时,会调用getDefaultSize()方法。在getDefaultSize()方法中,又会调用getSuggestedMinimumWidth()和getSuggestedMinimumHeight()获取建议的最小宽度和最小高度,并根据最小尺寸和父View对自己的尺寸要求进行修正。


最主要的是,在getDefaultSize()方法中修正的时候,会将MeasureSpec.AT_MOST和MeasureSpec.EXACTLY一视同仁,直接返回父View对View的尺寸要求:


 
  

//1. 默认 onMeasure 的处理
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
            getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

//2. getSuggestedMinimumWidth()
protected int getSuggestedMinimumWidth() {
    return (mBackground == null) ? mMinWidth : max(mMinWidth, mBackground.getMinimumWidth());
}

//3. getSuggestedMinimumHeight()
protected int getSuggestedMinimumHeight() {
    return (mBackground == null) ? mMinHeight : max(mMinHeight, mBackground.getMinimumHeight());
}

//4. 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:
        //MeasureSpec.AT_MOST、MeasureSpec.EXACTLY 一视同仁
        result = specSize;
        break;
    }
    return result;
}


正是因为在getDefaultSize()方法中处理的时候,将MeasureSpec.AT_MOST和MeasureSpec.EXACTLY一视同仁,所以才有了上面「在xml中应用CircleView的时候,无论将CircleView的尺寸设置为match_parent还是wrap_content效果都一样」的现象。


具体分析如下:


640?wx_fmt=png


注:


上表中,「View的父View对View的尺寸要求」是View的父View根据「开发者对子View的尺寸要求」、「自己的父View(View的父View的父View) 对自己的尺寸要求」和「自己的可用空间」计算出自己对子View的尺寸要求。


另外,由执行结果可知,上表中的specSize实际上等于View的尺寸:


 
  

2019-08-13 17:28:26.855 16024-16024/com.smart.a03_view_custom_view_example E/TAG: Width(getWidth()):  1080  Height(getHeight()):  1584


自定义View的尺寸和绘制内容


自定义View,它的内容是「三个半径不同、颜色不同的同心圆」,效果图如下:


640?wx_fmt=png


自定义属性的声明与获取


 
  

//在 xml 中自定义 View 属性
<?xml version="1.0" encoding="utf-8"?>
<resources>
    <!--CircleView-->
    <declare-styleable name="CircleView">
        <attr name="circle_radius" format="dimension" />
        <attr name="outer_circle_color" format="reference|color" />
        <attr name="middle_circle_color" format="reference|color" />
        <attr name="inner_circle_color" format="reference|color" />
    </declare-styleable>
</resources>


 
  

//在 View 构造函数中获取自定义 View 属性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
mRadius = typedArray.getDimension(R.styleable.CircleView_circle_radius, getResources().getDimension(R.dimen.avatar_size));
mOuterCircleColor = typedArray.getColor(R.styleable.CircleView_outer_circle_color, getResources().getColor(R.color.purple_500));
mMiddleCircleColor = typedArray.getColor(R.styleable.CircleView_middle_circle_color, getResources().getColor(R.color.purple_500));
mInnerCircleColor = typedArray.getColor(R.styleable.CircleView_inner_circle_color, getResources().getColor(R.color.purple_500));
typedArray.recycle();


重写测量阶段相关方法(onMeasure())


 
  

//onMeasure()
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    //2.1 根据 View 特点或业务需求计算出 View 的尺寸
    mWidth = (int)(mRadius * 2);
    mHeight = (int)(mRadius * 2);

    //2.2 通过 resolveSize() 方法修正结果
    mWidth = resolveSize(mWidth, widthMeasureSpec);
    mHeight = resolveSize(mHeight, heightMeasureSpec);

    //2.3 通过 setMeasuredDimension() 保存 View 的期望尺寸(通过 setMeasuredDimension() 告知父 View 的期望尺寸)
    setMeasuredDimension(mWidth, mHeight);
}


重写布局阶段相关方法(onLayout()(仅ViewGroup需要重写))


由于没有子View需要布局,所以,不用重写该方法。


重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground()绘制前景)


 
  

// 重写 onDraw() 方法,自定义 View 内容
@Override
protected void onDraw(Canvas canvas) {
    mPaint.setColor(mOuterCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius, mPaint);
    mPaint.setColor(mMiddleCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius * 2/3, mPaint);
    mPaint.setColor(mInnerCircleColor);
    canvas.drawCircle(mRadius, mRadius, mRadius/3, mPaint);
}


onTouchEvent()


由于View不需要和用户交互,所以,不用重写该方法。


onInterceptTouchEvent()(仅 ViewGroup 有此方法)


ViewGroup的方法。


最终效果如下:


640?wx_fmt=png


当在xml中将MeasuredCircleView的宽、高声明为「match_parent」时,显示效果跟CircleView显示效果一样。


640?wx_fmt=png


但是,当在xml中将MeasuredCircleView的宽、高声明为「wrap_content」时,显示效果是下面这个样子:


640?wx_fmt=png


其实,也很好理解:


640?wx_fmt=png


自定义ViewGroup


自定义ViewGroup,标签布局,效果图如下:


640?wx_fmt=png


无论是自定义View还是自定义ViewGroup,大致的流程都是一样的:


  1. 自定义属性的声明与获取

  2. 重写测量阶段相关方法(onMeasure())

  3. 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))

  4. 重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景)

  5. onTouchEvent()

  6. onInterceptTouchEvent()(仅 ViewGroup 有此方法)


只不过,大多数情况下,ViewGroup不需要「自定义属性」和「重写绘制阶段相关方法」,但有些时候还是需要的,如,开发者想在ViewGroup的所有子View上方绘制一些内容,就可以通过重写ViewGroup的onDrawForeground()来实现。


自定义属性的声明与获取


在自定义ViewGroup中「自定义属性的声明与获取」的方法与在自定义View中「自定义属性的声明与获取」的方法一样,且因为大多数情况下,在自定义ViewGroup中是不需要自定义属性的,所以,在这里就不自定义属性了。


重写测量阶段相关方法(onMeasure())


 
  

//2. 重写测量阶段相关方法(onMeasure());
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //2.1 解析 ViewGroup 的父 View 对 ViewGroup 的尺寸要求
    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);
    int heightMode = MeasureSpec.getMode(widthMeasureSpec);
    int heightSize = MeasureSpec.getSize(widthMeasureSpec);

    //2.2 ViewGroup 根据「开发者在 xml 中写的对 ViewGroup 子 View 的尺寸要求」、「自己的父 View(ViewGroup 的父 View)对自己的尺寸要求」和
    //「自己的可用空间」计算出自己对子 View 的尺寸要求,并将该尺寸要求通过子 View 的 measure() 方法传给子 View,让子 View 测量自己(View)的期望尺寸
    //具体代码查看原文...
        //2.3 ViewGroup 暂时保存子 View 的尺寸,以便布局阶段和绘制阶段使用
        Rect childBound;
        if(mChildrenBounds.size() <= i){
            childBound = new Rect();
            mChildrenBounds.add(childBound);
        }else{
            childBound = mChildrenBounds.get(i);
        }
        //此处不能用 child.getxxx() 获取子 View 的尺寸值,因为子 View 只是量了尺寸,还没有布局,这些值都是 0
//            childBound.set(child.getLeft(), child.getTop(), child.getRight(), child.getBottom());
        childBound.set(lineWidthUsed, heightUsed, lineWidthUsed + child.getMeasuredWidth(), heightUsed + child.getMeasuredHeight());

        lineWidthUsed += child.getMeasuredWidth() + mItemSpace;
        widthUsed = Math.max(lineWidthUsed, widthUsed);
        lineHeight = Math.max(lineHeight, child.getMeasuredHeight());
    }

    //2.4 ViewGroup 将「根据子 View 的实际尺寸计算出的自己(ViewGroup)的尺寸」结合「自己父 View 对自己的尺寸要求」进行修正,并通
    //过 setMeasuredDimension() 方法告知父 View 自己的期望尺寸
    int measuredWidth = resolveSize(widthUsed, widthMeasureSpec);
    int measuredHeight = resolveSize((heightUsed + lineHeight + getPaddingBottom()), heightMeasureSpec);
    setMeasuredDimension(measuredWidth, measuredHeight);
}

//重写generateLayoutParams()
//2.2.1 在自定义 ViewGroup 中调用 measureChildWithMargins() 方法计算 ViewGroup 对子 View 的尺寸要求时,
//必须在 ViewGroup 中重写 generateLayoutParams() 方法,因为 measureChildWithMargins() 方法中用到了 MarginLayoutParams,
//如果不重写 generateLayoutParams() 方法,那调用 measureChildWithMargins() 方法时,MarginLayoutParams 就为 null,
//所以在自定义 ViewGroup 中调用 measureChildWithMargins() 方法时,必须重写 generateLayoutParams() 方法。
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
    return new MarginLayoutParams(getContext(), attrs);
}


重写布局阶段相关方法(onLayout()(仅ViewGroup需要重写))


 
  

//重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写));
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    for (int i = 0; i < getChildCount(); i++) {
        //应用测量阶段计算出的子 View 的尺寸值布局子 View
        View child = getChildAt(i);
        Rect childBound = mChildrenBounds.get(i);
        child.layout(childBound.left, childBound.top, childBound.right, childBound.bottom);
    }
}


重写绘制阶段相关方法(onDraw()绘制主体、dispatchDraw()绘制子View和onDrawForeground()绘制前景)


默认情况下,自定义ViewGroup时是不需要重写任何绘制阶段的方法的,因为ViewGroup的角色是容器,一个透明的容器,它只是用来盛放子View的。


注意:


  • 默认情况下,系统会自动调用View Group的dispatchDraw()方法,所以不需要重写该方法

  • 出于效率的考虑,ViewGroup默认会绕过draw()方法,换而直接执行dispatchDraw(),以此来简化绘制流程。所以如果你自定义了一个ViewGroup,并且需要在它的除dispatchDraw()方法以外的任何一个绘制方法内绘制内容,你可能会需要调用View.setWillNotDraw(false)方法来切换到完整的绘制流程(是「可能」而不是「必须」的原因是,有些ViewGroup是已经调用过setWillNotDraw(false)了的,例如ScrollView)。除了可以通过调用View.setWillNotDraw(false)方法来切换到完整的绘制流程之外,你还可以通过给ViewGroup设置背景来切换到完整的绘制流程


onTouchEvent()


由于ViewGroup不需要和用户交互,所以,不用重写该方法。


onInterceptTouchEvent()(仅ViewGroup有此方法)


由于ViewGroup不需要和用户交互且ViewGroup不需要拦截子View的MotionEvent,所以,不用重写该方法。


最终效果如下:


640?wx_fmt=png


/   总结   /


自定义View包括三部分内容:


  • 布局(Layout)
  • 绘制(Drawing)
  • 触摸反馈(Event Handling)

其中布局阶段确定了View的位置和尺寸,该阶段主要是为了后面的绘制和触摸反馈做支持;绘制阶段主要用于绘制View的内容(大多数情况下,只用实现OnDraw方法(Where)方法、按照指定顺序调用相关API(How)即可实现自定义绘制(What));触摸反馈阶段确定了用户点击了哪里,三者相辅相成,缺一不可。


推荐阅读:





欢迎关注我的公众号

学习技术或投稿


640.png?


640?wx_fmt=jpeg

长按上图,识别图中二维码即可关注


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值