在 Android 系统中,界面中全部能看到的元素都是 View。默认状况下,Android 系统为开发者提供了不少 View,好比用于展现文本信息的 TextView,用于展现图片的 ImageView 等等。但有时,这并不能知足开发者的需求,例如,开发者想要用一个饼状图来展现一组数据,这时若是用系统提供的 View 就不能实现了,只能经过自定义 View 来实现。那到底什么是自定义 View 呢?android
自定义 View 就是经过继承 View 或者 View 的子类,并在新的类里面实现相应的处理逻辑(重写相应的方法),以达到本身想要的效果。面试
Android 中的全部 UI 元素都是 View 的子类:canvas
PS:因为涉及的类太多,若是将全部涉及到的类所有加到类图里面,类图将十分大,因此此处只列出了 View 的直接子类。设计模式
Android View 体系以下:bash
仔细观察,你会发现,Android View 的体系结构和设计模式中的组合模式的结构一模一样:app
Android View 体系结构中的 ViewGroup 对应于组合模式中抽象构件(Component 和 Composite),Android View 体系结构中的 View 对应于组合模式中的叶子构件(Leaf):ide
Android View 构件 | Composite Pattern 构件 |
---|---|
ViewGroup | Component、Composite |
View | Leaf |
大多数状况下,开发者经常会由于下面四个缘由去自定义 View:函数
默认状况下,Android 系统为开发者提供了不少控件,但有时,这并不能知足开发者的需求。例如,开发者想要用一个饼状图来展现一组数据,这时若是用系统提供的 View 就不能实现了,只能经过自定义 View 来实现。布局
If none of the prebuilt widgets or layouts meets your needs, you can create your own View subclass.post
默认状况下,Android 系统为开发者提供的控件都有属于它们本身的特定的交互方式,但有时,控件的默认交互方式并不能知足开发者的需求。例如,开发者想要缩放 ImageView 中的图片内容,这时若是用系统提供的 ImageView 就不能实现了,只能经过自定义 ImageView 来实现。
有时,有些布局若是用系统提供的控件实现起来至关复杂,须要各类嵌套,虽然最终也能实现了想要的效果,但性能极差,此时就能够经过自定义 View 来减小嵌套层级、优化布局。
有些控件可能在多个地方使用,如大多数 App 里面的底部 Tab,像这样的常常被用到的控件就能够经过自定义 View 将它们封装起来,以便在多个地方使用。
在说「如何自定义 View?」以前,咱们须要知道「自定义 View 都包括哪些内容」?
自定义 View 包括三部份内容:
布局阶段:肯定 View 的位置和尺寸。
绘制阶段:绘制 View 的内容。
触摸反馈:肯定用户点击了哪里。
其中布局阶段包括测量(measure)和布局(layout)两个过程,另外,布局阶段是为绘制和触摸反馈阶段作支持的,它并无什么直接做用。正是由于在布局阶段肯定了 View 的尺寸和位置,绘制阶段才知道往哪里绘制,触摸反馈阶段才知道用户点的是哪里。
另外,因为触摸反馈是一个大的话题,限于篇幅,就不在这里讲解了,后面有机会的话,我会再补上一篇关于触摸反馈的文章。
在自定义 View 和自定义 ViewGroup 中,布局和绘制流程虽然总体上都是同样的,但在细节方面,自定义 View 和自定义 ViewGroup 仍是不同的,因此,接下来分两类进行讨论:
「自定义 View 布局、绘制」主要包括三个阶段:
在 View 的测量阶段会执行两个方法(在测量阶段,View 的父 View 会经过调用 View 的 measure() 方法将父 View 对 View 尺寸要求传进来。紧接着 View 的 measure() 方法会作一些前置和优化工做,而后调用 View 的 onMeasure() 方法,并经过 onMeasure() 方法将父 View 对 View 的尺寸要求传入。在自定义 View 中,只有须要修改 View 的尺寸的时候才须要重写 onMeasure() 方法。在 onMeasure() 方法中根据业务需求进行相应的逻辑处理,并在最后经过调用 setMeasuredDimension() 方法告知父 View 本身的指望尺寸):
measure() : 调度方法,主要作一些前置和优化工做,并最终会调用 onMeasure() 方法执行实际的测量工做;
onMeasure() : 实际执行测量任务的方法,主要用与测量 View 尺寸和位置。在自定义 View 的 onMeasure() 方法中,View 根据本身的特性和父 View 对本身的尺寸要求算出本身的指望尺寸,并经过 setMeasuredDimension() 方法告知父 View 本身的指望尺寸。
onMeasure() 计算 View 指望尺寸方法以下:
参考父 View 的对 View 的尺寸要求和实际业务需求计算出 View 的指望尺寸:
经过 setMeasuredDimension() 保存 View 的指望尺寸(其实是经过 setMeasuredDimension() 告知父 View 本身的指望尺寸);
注意:
多数状况下,这里的指望尺寸就是 View 的最终尺寸。不过最终 View 的指望尺寸和实际尺寸是否是同样还要看它的父 View 会不会赞成。View 的父 View 最终会经过调用 View 的 layout() 方法告知 View 的实际尺寸,而且在 layout() 方法中 View 须要将这个实际尺寸保存下来,以便绘制阶段和触摸反馈阶段使用,这也是 View 须要在 layout() 方法中保存本身实际尺寸的缘由——由于绘制阶段和触摸反馈阶段要使用啊!
在 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() : 保存 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 的绘制阶段会执行一个方法——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 里面的绘制都是按顺序的,先绘制的内容会被后绘制的盖住。如,你在重叠的位置「先画圆再画方」和「先画方再画圆」所呈现出来的结果是不一样的,具体表现为下表:
「自定义 ViewGroup 布局、绘制」主要包括三个阶段:
同自定义 View 同样,在自定义 ViewGroup 的测量阶段会执行两个方法:
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) 本身的指望尺寸。
具体流程以下:
同自定义 View 同样,在自定义 ViewGroup 的布局阶段会执行两个方法:
layout() : 保存 ViewGroup 的实际尺寸。调用 setFrame() 方法保存 ViewGroup 的实际尺寸,调用 onSizeChanged() 通知开发者 ViewGroup 的尺寸更改了,并最终会调用 onLayout() 方法让子 View 布局;
onLayout() : ViewGroup 会递归调用每一个子 View 的 layout() 方法,把测量阶段计算出的子 View 的实际尺寸和位置传给子 View,让子 View 保存本身的实际尺寸和位置。
同自定义 View 同样,在自定义 ViewGroup 的绘制阶段会执行一个方法——draw()。draw() 是绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground():
draw() : 绘制阶段的总调度方法,在其中会调用绘制背景的方法 drawBackground()、绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground();
在 ViewGroup 中,你也能够重写绘制主体的方法 onDraw()、绘制子 View 的方法 dispatchDraw() 和 绘制前景的方法 onDrawForeground()。但大多数状况下,自定义 ViewGroup 是不须要重写任何绘制方法的。由于一般状况下,ViewGroup 的角色是容器,一个透明的容器,它只是用来盛放子 View 的。
自定义 View,它的内容是「三个半径不一样、颜色不一样的同心圆」,效果图以下:
//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();
复制代码
因为不须要自定义 View 的尺寸,因此,不用重写该方法。
因为没有子 View 须要布局,因此,不用重写该方法。
//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);
}
复制代码
因为 View 不须要和用户交互,因此,不用重写该方法。
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>
复制代码
最终效果以下:
此时,即便你在 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
复制代码
自定义 View,它的内容是「三个半径不一样、颜色不一样的同心圆」,效果图以下:
//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();
复制代码
//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);
}
复制代码
因为没有子 View 须要布局,因此,不用重写该方法。
//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);
}
复制代码
因为 View 不须要和用户交互,因此,不用重写该方法。
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) {
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) {
//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);
}
//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 中应用 MeasuredCircleView
<?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"
android:orientation="vertical"
tools:context=".custom_view_measure_draw.CustomViewMeasureDrawActivity">
<com.smart.a03_view_custom_view_example.custom_view_measure_draw.MeasuredCircleView
android:layout_width="match_parent"
android:layout_height="match_parent"
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>
复制代码
最终效果以下:
当在 xml 中将 MeasuredCircleView 的宽、高声明为「match_parent」时,显示效果跟 CircleView 显示效果同样。
开发者对 View 的尺寸要求 | View 的父 View 对 View 的尺寸要求 | View 的指望尺寸 |
---|---|---|
android:layout_width="match_parent" android:layout_height="match_parent" |
MeasureSpec.EXACTLY specSize |
specSize |
可是,当在 xml 中将 MeasuredCircleView 的宽、高声明为「wrap_content」时,显示效果是下面这个样子:
其实,也很好理解:
开发者对 View 的尺寸要求 | View 的父 View 对 View 的尺寸要求 | View 的指望尺寸 |
---|---|---|
android:layout_width="wrap_content" android:layout_height="wrap_content" |
MeasureSpec.AT_MOST specSize |
if(childSize < specSize) childSize if(childSize > specSize) specSize |
自定义 ViewGroup,标签布局,效果图以下:
不管是自定义 View 仍是自定义 ViewGroup,大体的流程都是同样的:
只不过,大多数状况下,ViewGroup 不须要「自定义属性」和「重写绘制阶段相关方法」,但有些时候仍是须要的,如,开发者想在 ViewGroup 的全部子 View 上方绘制一些内容,就能够经过重写 ViewGroup 的 onDrawForeground() 来实现。
在自定义 ViewGroup 中「自定义属性的声明与获取」的方法与在自定义 View 中「自定义属性的声明与获取」的方法同样,且由于大多数状况下,在自定义 ViewGroup 中是不须要自定义属性的,因此,在这里就不自定义属性了。
//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)的指望尺寸
int widthUsed = 0;
int heightUsed = getPaddingTop();
int lineHeight = 0;
int lineWidthUsed = getPaddingLeft();
int maxRight = widthSize - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
//是否须要换行
if(widthMode != MeasureSpec.UNSPECIFIED && (lineWidthUsed + child.getMeasuredWidth() > maxRight)){
lineWidthUsed = getPaddingLeft();
heightUsed += lineHeight + mRowSpace;
lineHeight = 0;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
//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);
}
复制代码
//3. 重写布局阶段相关方法(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);
}
}
复制代码
默认状况下,自定义 ViewGroup 时是不须要重写任何绘制阶段的方法的,由于 ViewGroup 的角色是容器,一个透明的容器,它只是用来盛放子 View 的。
注意:
因为 ViewGroup 不须要和用户交互,因此,不用重写该方法。
因为 ViewGroup 不须要和用户交互且 ViewGroup 不须要拦截子 View 的 MotionEvent,因此,不用重写该方法。
完整代码以下:
//1. TabLayout
public class TabLayout extends ViewGroup {
private ArrayList<Rect> mChildrenBounds;
private int mItemSpace;
private int mRowSpace;
public TabLayout(Context context) {
this(context, null);
}
public TabLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public TabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initData();
}
private void initData(){
mChildrenBounds = new ArrayList<>();
mItemSpace = (int)getResources().getDimension(R.dimen.padding_small);
mRowSpace = (int)getResources().getDimension(R.dimen.padding_small);
}
//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)的指望尺寸
int widthUsed = 0;
int heightUsed = getPaddingTop();
int lineHeight = 0;
int lineWidthUsed = getPaddingLeft();
int maxRight = widthSize - getPaddingRight();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
//是否须要换行
if(widthMode != MeasureSpec.UNSPECIFIED && (lineWidthUsed + child.getMeasuredWidth() > maxRight)){
lineWidthUsed = getPaddingLeft();
heightUsed += lineHeight + mRowSpace;
lineHeight = 0;
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, heightUsed);
}
//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);
}
//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);
}
//3. 重写布局阶段相关方法(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);
}
}
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
return super.onInterceptHoverEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
return super.onTouchEvent(event);
}
}
//2. 在 xml 中应用 TabLayout
<?xml version="1.0" encoding="utf-8"?>
<ScrollView 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:scrollbars="none"
tools:context=".MainActivity">
<com.smart.a04_view_custom_viewgroup_example.custom_layout.TabLayout
android:id="@+id/tag_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/grey_400"
android:padding="@dimen/padding_small">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_bg"
android:text="@string/spending_clothes" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_bg"
android:text="@string/spending_others" />
...
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/common_bg"
android:text="@string/november" />
</com.smart.a04_view_custom_viewgroup_example.custom_layout.TabLayout>
</ScrollView>
复制代码
最终效果以下:
自定义 View 包括三部份内容:
其中布局阶段肯定了 View 的位置和尺寸,该阶段主要是为了后面的绘制和触摸反馈作支持;绘制阶段主要用于绘制 View 的内容(大多数状况下,只用实现 OnDraw 方法(Where)方法、按照指定顺序调用相关 API(How)便可实现自定义绘制(What));触摸反馈阶段肯定了用户点击了哪里,三者相辅相成,缺一不可。