2.4 封装
有些控件可能在多个地方使用,如大多数 App 里面的底部 Tab,像这样的经常被用到的控件就可以通过自定义 View 将它们封装起来,以便在多个地方使用。
3. 如何自定义 View?
在说「如何自定义 View?」之前,我们需要知道「自定义 View 都包括哪些内容」?
自定义 View 包括三部分内容:
- 布局(Layout)
- 绘制(Drawing)
- 触摸反馈(Event Handling)
布局阶段:确定 View 的位置和尺寸。
绘制阶段:绘制 View 的内容。
触摸反馈:确定用户点击了哪里。
其中布局阶段包括测量(measure)和布局(layout)两个过程,另外,布局阶段是为绘制和触摸反馈阶段做支持的,它并没有什么直接作用。正是因为在布局阶段确定了 View 的尺寸和位置,绘制阶段才知道往哪里绘制,触摸反馈阶段才知道用户点的是哪里。
另外,由于触摸反馈是一个大的话题,限于篇幅,就不在这里讲解了,后面有机会的话,我会再补上一篇关于触摸反馈的文章。
在自定义 View 和自定义 ViewGroup 中,布局和绘制流程虽然整体上都是一样的,但在细节方面,自定义 View 和自定义 ViewGroup 还是不一样的,所以,接下来分两类进行讨论:
- 自定义 View 布局、绘制流程
- 自定义 ViewGroup 布局、绘制流程
3.1 自定义 View 布局、绘制流程
「自定义 View 布局、绘制」主要包括三个阶段:
- 测量阶段(measure)
- 布局阶段(layout)
- 绘制阶段(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 期望尺寸方法如下:
-
参考父 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() 方法中保存自己实际尺寸的原因——因为绘制阶段和触摸反馈阶段要使用啊!
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 布局、绘制」主要包括三个阶段:
- 测量阶段(measure)
- 布局阶段(layout)
- 绘制阶段(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) 自己的期望尺寸。
具体流程如下:
- 运行前,开发者在 xml 中写入对 ViewGroup 和 ViewGroup 子 View 的尺寸要求 layout_xxx;
- ViewGroup 在自己的 onMeasure() 方法中,根据开发者在 xml 中写的对 ViewGroup 子 View 的尺寸要求、自己的父 View(ViewGroup 的父 View) 对自己的尺寸要求和自己的可用空间计算出自己对子 View 的尺寸要求,并调用每个子 View 的 measure() 将 ViewGroup 对子 View 的尺寸要求传入,测量子 View 尺寸;
- ViewGroup 在子 View 计算出期望尺寸之后(在 ViewGroup 的 onMeasure() 方法中,ViewGroup 递归调用每个子 View 的 measure() 方法,子 View 在自己的 onMeasure() 方法中会通过调用 setMeasuredDimension() 方法告知父 View(ViewGroup) 自己的期望尺寸),得出子 View 的实际尺寸和位置,并暂时保存计算结果,以便布局阶段使用;
- 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 步骤
- 自定义属性的声明与获取;
- 重写测量阶段相关方法(onMeasure());
- 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写));
- 重写绘制阶段相关方法(onDraw() 绘制主体、dispatchDraw() 绘制子 View 和 onDrawForeground() 绘制前景);
- onTouchEvent();
- 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 在 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();
- 重写测量阶段相关方法(onMeasure())
由于不需要自定义 View 的尺寸,所以,不用重写该方法。
- 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))
由于没有子 View 需要布局,所以,不用重写该方法。
- 重写绘制阶段相关方法(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);
}
- onTouchEvent()
由于 View 不需要和用户交互,所以,不用重写该方法。
- 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 | |
specSize | specSize | |
android:layout_width=“match_parent” | ||
android:layout_height=“match_parent” | MeasureSpec.EXACTLY | |
specSize | specSize |
注:
上表中,「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 在 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();
- 重写测量阶段相关方法(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);
}
- 重写布局阶段相关方法(onLayout()(仅 ViewGroup 需要重写))
由于没有子 View 需要布局,所以,不用重写该方法。
- 重写绘制阶段相关方法(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);
}
- onTouchEvent()
由于 View 不需要和用户交互,所以,不用重写该方法。
- 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)收录,自学资源及系列文章持续更新中...**