Android 自定义 View 最少必要知识,互联网寒冬

2.4 封装

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

3. 如何自定义 View?

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

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

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

布局阶段:确定 View 的位置和尺寸。
绘制阶段:绘制 View 的内容。
触摸反馈:确定用户点击了哪里。

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

另外,由于触摸反馈是一个大的话题,限于篇幅,就不在这里讲解了,后面有机会的话,我会再补上一篇关于触摸反馈的文章。

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

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

3.1 自定义 View 布局、绘制流程

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

  1. 测量阶段(measure)
  2. 布局阶段(layout)
  3. 绘制阶段(draw)
3.1.1 自定义 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 期望尺寸方法如下:

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

    • 解析 widthMeasureSpec;
    • 解析 heightMeasureSpec;
    • 将「根据实际业务需求计算出 View 的尺寸」根据「父 View 的对 View 的尺寸要求」进行相应的修正得出 View 的期望尺寸(通过调用 resolveSize() 方法);
  2. 通过 setMeasuredDimension() 保存 View 的期望尺寸(实际上是通过 setMeasuredDimension() 告知父 View 自己的期望尺寸);

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

3.1.2 自定义 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() 均做事,只不过职责不同。

3.1.3 自定义 View 绘制阶段

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

  • draw()

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C6dTxKdT-1631191238319)(https://user-gold-cdn.xitu.io/2019/9/2/16cf0103f4c76c31?imageView2/0/w/1280/h/960/ignore-error/1)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ds34Lfkc-1631191238322)(https://user-gold-cdn.xitu.io/2019/9/2/16cf0103f4a8c299?imageView2/0/w/1280/h/960/ignore-error/1)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hmBWKYqe-1631191238323)(https://user-gold-cdn.xitu.io/2019/9/2/16cf0103f49fee81?imageView2/0/w/1280/h/960/ignore-error/1)]

3.2 自定义 ViewGroup 布局、绘制流程

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

  1. 测量阶段(measure)
  2. 布局阶段(layout)
  3. 绘制阶段(draw)
3.2.1 自定义 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 的尺寸要求。
3.2.2 自定义 ViewGroup 布局阶段

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

  • layout()
  • onLayout()

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

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

3.2.3 自定义 ViewGroup 绘制阶段

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

  • draw()

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

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

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAj2GxlK-1631191238325)(https://user-gold-cdn.xitu.io/2019/9/2/16cf0103f48b0917?imageView2/0/w/1280/h/960/ignore-error/1)]

3.3 自定义 View 步骤

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

4. 实战演练

4.1 自定义 View

4.1.1 自定义 View ——自定义 View 的绘制内容

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-25kzqvhO-1631191238326)(https://user-gold-cdn.xitu.io/2019/9/2/16cf010572139870?imageView2/0/w/1280/h/960/ignore-error/1)]

  1. 自定义属性的声明与获取
//1.1 在 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>

//1.2 在 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(); 
  1. 重写测量阶段相关方法(onMeasure())

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

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

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

  1. 重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景)
//4. 重写 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);
} 
  1. onTouchEvent()

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

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

ViewGroup 的方法。

完整代码如下:

//1. 自定义属性的声明  
<?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>

//2. CircleView  
public class CircleView extends View {

    private float mRadius;
    private int mOuterCircleColor, mMiddleCircleColor, mInnerCircleColor;
    private Paint mPaint;

    public CircleView(Context context) {
        this(context, null);
    }

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

    public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initData(context, attrs);
    }

    private void initData(Context context, AttributeSet attrs) {
        //1. 自定义属性的声明与获取
        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();

        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setColor(mOuterCircleColor);
    }

    //2. 重写测量阶段相关方法(onMeasure());
    //由于不需要自定义 View 的尺寸,所以不用重写该方法
//    @Override
//    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//    }

    //3. 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写));
    //由于没有子 View 需要布局,所以不用重写该方法
//    @Override
//    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
//        super.onLayout(changed, left, top, right, bottom);
//    }

    //4. 重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景);
    @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);
    }

}

//3. 在 xml 中应用 CircleView  
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    tools:context=".custom_view_only_draw.CustomViewOnlyDrawActivity">

    <com.smart.a03_view_custom_view_example.custom_view_only_draw.CircleView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:circle_radius="@dimen/padding_ninety_six"
        app:inner_circle_color="@color/yellow_500"
        app:middle_circle_color="@color/cyan_500"
        app:outer_circle_color="@color/green_500" />

</LinearLayout> 

最终效果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-39w8OETI-1631191238327)(https://user-gold-cdn.xitu.io/2019/9/2/16cf01057261a363?imageView2/0/w/1280/h/960/ignore-error/1)]

此时,即使你在 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 效果都一样」的现象。

具体分析如下:

开发者对 View 的尺寸要求View 的父 View 对 View 的尺寸要求View 的期望尺寸
android:layout_width=“wrap_content”
android:layout_height=“wrap_content”MeasureSpec.AT_MOST
specSizespecSize
android:layout_width=“match_parent”
android:layout_height=“match_parent”MeasureSpec.EXACTLY
specSizespecSize

注:
上表中,「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 
4.1.2 自定义 View ——自定义 View 的尺寸和绘制内容

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TtLLJNGT-1631191238328)(https://user-gold-cdn.xitu.io/2019/9/2/16cf010572139870?imageView2/0/w/1280/h/960/ignore-error/1)]

  1. 自定义属性的声明与获取
//1.1 在 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>

//1.2 在 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(); 
  1. 重写测量阶段相关方法(onMeasure())
//2. 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);
} 
  1. 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))

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

  1. 重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景)
//4. 重写 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);
} 
  1. onTouchEvent()

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

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

ViewGroup 的方法。

完整代码如下:

//1. 自定义属性的声明  
<?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>

//2. MeasuredCircleView
public class MeasuredCircleView extends View {

    private int mWidth, mHeight;
    private float mRadius;
    private int mOuterCircleColor, mMiddleCircleColor, mInnerCircleColor;
    private Paint mPaint;

    public MeasuredCircleView(Context context) {
        this(context, null);
    }

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

    public MeasuredCircleView(Context context, AttributeSet attrs, int defStyleAttr) {


# 总结

Android架构学习进阶是一条漫长而艰苦的道路,不能靠一时激情,更不是熬几天几夜就能学好的,必须养成平时努力学习的习惯。**所以:贵在坚持!**

上面分享的字节跳动公司2021年的面试真题解析大全,笔者还把一线互联网企业主流面试技术要点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
![](https://img-blog.csdnimg.cn/img_convert/00d748fb27f23d73a0a9395a913556d2.png)

**[CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](

)**

**【Android高级架构视频学习资源】**

Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

> **本文已被[腾讯CODING开源托管项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](https://ali1024.coding.net/public/P7/Android/git)收录,自学资源及系列文章持续更新中...**
际上比预期多花了不少精力),包含知识脉络 + 诸多细节。
[外链图片转存中...(img-JVLQ9IBC-1631191238329)]

**[CodeChina开源项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](

)**

**【Android高级架构视频学习资源】**

Android部分精讲视频领取学习后更加是如虎添翼!进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

> **本文已被[腾讯CODING开源托管项目:《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》](https://ali1024.coding.net/public/P7/Android/git)收录,自学资源及系列文章持续更新中...**
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值