[toc]
View
View 是 Android 中所有 Widget 的基类,在屏幕上占据一个矩形区域。View 主要分两类控件
和容器
。控件有自己的功能,例如 TextView 用来显示文本。容器用来控制内部控件的排列方式和位置,例如 LinearLayout 用来将内部控件进行线性布局。
容器是 ViewGroup 的子类,ViewGroup 是 View 的子类。容器也是一个 View,又可以包含多个 View,所以 Android 中的布局就成了一个 View 树。
自定义 View
自定义 View 有三种形式:
- 继承已有控件,拓展功能
- 组合多个控件,完成复杂的功能
- 继承 View,完全自定义。
这里我们只讨论第三种情况,在这种情况下我们一般要去实现自定义 xml 属性、测量、绘制。实现这些功能需要重写 View 中不同的方法:
- 实现自定义 xml 属性 -> 构造方法
- 测量 -> onMeasure()
- 绘制 -> onDraw()
自定义 xml 属性
xml 属性应用在布局文件的 xml 中,用来设置 View 的属性。常见的如 android:id
有两部分组成,命名空间:属性名
。
xmlns(Xml NameSpace)
命名空间在布局 xml 文件的根节点定义:
我们设置控件属性的时候一直在打android:xxx
就是因为第一行设置了 xmlns:android。xmlns
是 xml namespace 缩写,android
是命名空间的名字。命名空间的具体作用取决于定义时等号后面的值, android
代表 Android 系统属性。
如果改成 xmlns:abc
,布局文件中再使用原来的属性就是abc:xxx
。例如: android:id
=> abc:id
。(不建议修改)
添加自定义属性
自定义属性名在 res/values/attrs.xml中定义。
format 中的 reference 表示可以接受 res 中定义资源的引用,例如 @color/colorPrimary
。
在布局文件 xml 中使用自定义属性需要在根标签中添加 xmlns。使用 AS 创建布局文件时会在根标签中默认加入 app
xmlns,代表应用自定义属性和 support 包中定义的属性。
xmlns:xxx="http://schemas.android.com/apk/res/[your package name]" 在AS中会报错。 Gradle Project 中建议使用 res-auto , 不推荐硬编码包名
在自定义 View 的标签中加入定义好的属性:
在构造函数中使用自定义属性
public class CustomView extends View {
private static final String TAG = "CustomView";
private final String DEFAULT_TITLE = "CustomView";
private final int DEFAULT_TITLE_COLOR = Color.rgb(0,133,119);
private String title;
private int titleColor;
private int titleStyle;
private Paint titlePaint;
/**
* 使用代码创建对象的构造,不解析 xml ,在构造中给自定义属性默认值
* @param context
*/
public CustomView(Context context) {
super(context);
//设置属性默认值
title = DEFAULT_TITLE;
titleColor = DEFAULT_TITLE_COLOR;
//初始化画笔
initTitlePaint(titleColor,titleStyle);
}
/**
* xml 解析生成对象时使用的构造
* @param context
* @param attrs 布局中的属性 解析后 存入 AttributeSet 集合中
*/
public CustomView(Context context,AttributeSet attrs) {
super(context, attrs);
// 从 AttributeSet 中取出 R.styleable.MyAttr 定义的属性
TypedArray myAttrs = context.obtainStyledAttributes(attrs,R.styleable.MyAttr);
try {
title = myAttrs.getString(R.styleable.MyAttr_title);
titleColor = myAttrs.getColor(R.styleable.MyAttr_titleColor, DEFAULT_TITLE_COLOR);
titleStyle = myAttrs.getInt(R.styleable.MyAttr_titleStyle,0);
}finally {
// TypedArray 类型的对象在使用完成以一定要调用 recycle() 进行回收
myAttrs.recycle();
}
Log.d(TAG, "CustomView: title = " + title +
"; titleColor = " + titleColor +
"; titleStyle = " + (titleStyle == 0 ? "Normal" : "Bold"));
//初始化画笔
initTitlePaint(titleColor,titleStyle);
}
}
日志输出:D/CustomView: CustomView: title = CustomViewDemo; titleColor = -2614432; titleStyle = Bold
, 自定义属性大功告成。
测量 onMeasure
目前为止即便我们给 CustomView 的宽高设置成 wrap_content 依旧会填满整个父容器,这一点可以给 CustomView 设置背景颜色来验证。 或者我们希望 CustomView 是个正方形,它仍然是个矩形,除非我们明确指定 CustomView 的宽高。
先来看一下 onMeasure() 方法,方法接受两个参数 widthMeasureSpec、heightMeasureSpec,但这并不是 View 的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
MeasureSpec
MeasureSpec 是 int 类型所以有 32 位,其中高 2 位代表测量模式(SpecMode),后 30 位代表测量模式下的大小(SpecSize)。
// widthMeasureSpec 分解成 SpecMode 和 SpecSize
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
Mode 有三种:
SpceMode | 含义 | 布局参数 | SpceSize |
---|---|---|---|
UNSPECIFIED | 父容器不对当前 View 做任何限制,想多大就多大。 | 一般用于系统 | 0 |
EXACTLY | SpecSize 的值就是当前 View 的确切大小。 | match_parent 或 指定的数值 | parentSize 或 指定的数值 |
AT_MOST | 大小不确定,但不能大于 SpecSize 的值。 | wrap_content | parentSize |
(指定的数值 是指在布局文件中显示的设置大小。如:100dp)
现在我们知道为什么即使 CustomView 设置了 wrap_content 它还是会填满父容器了,因为它的 SpceSize 是父容器的大小。
onMeasure()
onMeasure() 方法的作用是 利用参数中的 MeasureSpec 计算出真正的宽高并调用 setMeasuredDimension() 方法设置真实宽高。
在 onMeasure 时我们会有两个宽高:
- 参数传递的 specSize(parentSize 、布局中指定的宽高、0)
- 根据内容计算出的宽高。例如: CustomView 中可以根据 title 显示的文本计算出内容的宽高。
再根据 SpceMode 得出合理的宽高。
private int getRealSize(int contentSize, int measureSpec){
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode){
case MeasureSpec.EXACTLY:// EXACTLY 明确知道了大小,返回 specSize
return specSize;
case MeasureSpec.AT_MOST:// AT_MOST 不能超过 specSize ,返回最小的
return Math.min(contentSize,specSize);
case MeasureSpec.UNSPECIFIED: // UNSPECIFIED 不限制大小 想多大就多大 ,返回 contentSize
default:
return contentSize;
}
}
最后不要忘了处理 padding。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//根据 font 和 title 的内容, 计算出 title 的宽高
Paint.FontMetrics fontMetrics = titlePaint.getFontMetrics();
// 处理 padding
int contentHeight = (int) Math.ceil(fontMetrics.bottom - fontMetrics.top) + getPaddingTop() + getPaddingBottom();
int contentWidth = (int) titlePaint.measureText(title) + getPaddingLeft() + getPaddingRight();
// 根据 MeasureSpec 和 ContentSize 计算出真正的宽高
int realWidth = resolveSizeAndState1(contentWidth,widthMeasureSpec,MEASURED_STATE_MASK);
int realHeight = getRealSize(contentHeight,heightMeasureSpec);
//设置测量后的宽高
setMeasuredDimension(realWidth,realHeight);
}
编写 getRealSize() 方法可以帮助我们理解怎样计算真正需要的尺寸, View 类中 resolveSizeAndState() 方法同样可以帮我们计算出真正需要的尺寸,所以我们在自定义 View 时不用每次都编写 getRealSize() 方法。
resolveSizeAndState()
这个方法跟我们写的 getRealSize() 方法类似,返回一个带掩码的尺寸,这个尺寸可以直接传递给 setMeasuredDimension() 方法。
- 第一个参数传内容大小
- 第二个参数传 onMeasure() 方法中的参数
- 第三个参数传什么都行(0,1,MEASURED_STATE_MASK,对尺寸的计算没有影响,具体作用没弄清楚)
如果了解位运算可以看下面的解析,不了解就跳过解析直接把它当成 getRealSize() 方法用。
public static int resolveSizeAndState(int size, int measureSpec, int childMeasuredState) {
final int specMode = MeasureSpec.getMode(measureSpec);
final int specSize = MeasureSpec.getSize(measureSpec);
final int result;
switch (specMode) {
case MeasureSpec.AT_MOST:
if (specSize < size) { // 1
result = specSize | MEASURED_STATE_TOO_SMALL;
} else {
result = size;
}
break;
case MeasureSpec.EXACTLY:
result = specSize;
break;
case MeasureSpec.UNSPECIFIED:
default:
result = size;
}
return result | (childMeasuredState & MEASURED_STATE_MASK);//2
}
跟 getRealSize() 区别在于
注释 1 处: 如果 specSize(parentSize)小于内容的大小,用按位或运算加上 MEASURED_STATE_TOO_SMALL 掩码。
// public static final int MEASURED_STATE_TOO_SMALL = 0x01000000; 系统定义的值
// 假如 specSize 1080 ,计算后内容的宽度是 1081
// 进入注释 1 逻辑
0000 0000 0000 0000 0000 0100 0011 1000 // 1080
|
0000 0001 0000 0000 0000 0000 0000 0000 //MEASURED_STATE_TOO_SMALL
=
0000 0001 0000 0000 0000 0100 0011 1001
注释1:处最后的解释是 如果内容的大小(期望大小)大于 specSize(最大的限制),计算后的大小是将 specSize 高8位标记为1的值,表示这个 specSize 比实际期望的小。
注释2:同理在 result 的高1~8位上做标记。
最后系统在使用的时候 会将高 1~8位擦除,所以 resolveSizeAndState() 方法的返回值可以直接传递给 setMeasuredDimension() 方法。
public final int getMeasuredWidth() {
//MEASURED_SIZE_MASK = 0x00ffffff
return mMeasuredWidth & MEASURED_SIZE_MASK;
}
也就说这个 int 值只有24位用来表示实际大小,最大为 2^24 = 16777215对于尺寸来说足够用了。
绘制 onDraw()
绘制的主要任务是将内容用 Paint 画在 Canvas 上,Paint 和 Canvas 具体使用不在这里讲解。
需要注意的是 onDraw() 方法调用的频率非常高,所以尽量不要在 onDraw() 方法中创建对象。
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 计算基线 y
float baseLineY = (getHeight() - getPaddingTop() - getPaddingBottom()) / 2 + Math.abs(titlePaint.ascent() + titlePaint.descent()) / 2;
canvas.drawText(title,getPaddingLeft(),getPaddingTop() + baseLineY,titlePaint);
}
绘制文本推荐博客:https://www.jianshu.com/p/c3c9aea4cb01
自定义 ViewGroup
有了自定义 View 的基础,自定义 ViewGroup 就 so easy。我们来继承 ViewGroup 简单实现一个垂直线性布局 CustomViewGroup。
onMeasure()
根据子 View 的数量、子 View 的 margin 及 CustomViewGroup 的padding 计算出 CustomViewGroup 实际的宽高。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int maxWidth = 0;
int totalHeight = 0;
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE){ // GONE 不显示 ,直接下一个
continue;
}
//测量子 View , 重点 WithMargins
measureChildWithMargins(child,widthMeasureSpec,0,heightMeasureSpec,0);
//子 View 的宽高
int childWidth = child.getMeasuredWidth() ;
int childHeight = child.getMeasuredHeight();
maxWidth = Math.max(maxWidth,childWidth);
totalHeight += childHeight;
}
//不要忘了处理 ViewGroup 的 padding
int realWidth = resolveSizeAndState(maxWidth + getPaddingLeft() + getPaddingRight(),widthMeasureSpec,0) ;
int realHight = resolveSizeAndState(totalHeight + getPaddingTop() + getPaddingBottom(),heightMeasureSpec,0);
setMeasuredDimension(realWidth,realHight);
}
onLayout()
设置子 View 的位置,要考虑 CustomViewGroup 的 padding 及子 View 的 margin。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int left = getPaddingLeft();
int top = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
if (child.getVisibility() == View.GONE){ // GONE 不显示 ,直接下一个
continue;
}
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
//计算子 View 的 left, top, right, bottom
int childLeft = left + lp.leftMargin;
int childTop = top + lp.topMargin;
int childRight = childLeft + child.getMeasuredWidth();
int childBottom = childTop + child.getMeasuredHeight();
child.layout(childLeft,childTop,childRight,childBottom);
top = childBottom + lp.bottomMargin;
}
}
ViewGroup generateLayoutParams() 方法默认返回 ViewGroup.LayoutParams 运行时会报错,重写此方法返回 LinearLayout.LayoutParams 对象。
@Override
public LinearLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
// LinearLayout 的 LayoutParams 继承 ViewGroup.MarginLayoutParams 拿来直接用了
return new LinearLayout.LayoutParams(getContext(), attrs);
}
LayoutParams 也是一个比较重要的知识点,留个小尾巴。