Earth Guardian

You are not LATE!You are not EARLY!

0%

自定义 View

基本概念

测量模式 MeasureSpec

MeasureSpec 由两部分组成:

  • mode : 测量模式
  • size : 测量的尺寸大小

0029_MeasureSpec.png

其中模式有三种:

  • UNSPECIFIED
    ViewGroup 没有做约束,想要多大就多大,一般用于系统内部,如 ListView 等。
  • EXACTLY
    默认模式,按照给定的值精确计算,具体高宽值和 match_parent 都是这种模式。
  • AT_MOST
    相当于 wrap_content ,根据自身的内容的高宽来计算。

View 根据 ViewGroup 传入的测量值和模式,对自己宽高进行确定,并完成绘制。 onMeasure 实现测量,然后在 onDraw 中完成对自己的绘制。

重要 API

  • onMeasure
    测量,决定高宽等,不是必须但大部分都会重写,重写主要需要针对 wrap_content 模式计算自身实际的高宽,通过调用 setMeasuredDimension 来设置。如果指定具体的高宽或者 match_parent 可以不用重写,父类默认是以这种方式来测量的。
  • onDraw
    绘制,即如何展现,必须重写

自定义 View 步骤

自定义属性和样式

res/values/ 下建立一个 attrs.xml , 在里面定义 View 的属性和声明整个样式。

1
2
3
4
5
<declare-styleable name="CustomView">
<attr name="custom_text" format="string" />
<attr name="custom_color" format="color" />
<attr name="custom_size" format="integer" />
</declare-styleable>

其中,format 一共有如下几种类型:string, boolean, color, dimension, enum, flag, float, fraction, integer, reference
在布局文件中需要引入这个自定义的属性,先加上这一句(老版本需要手动导入自定义 View 的包名):xmlns:app="http://schemas.android.com/apk/res-auto"。后续就可以通过 app:custom*** 来设置自定义的属性了,如下所示:

1
2
3
4
5
6
7
8
<com.***.view.CustomView
android:id="@+id/view_event_custom_view_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
// 自定义属性
app:custom_text="@string/view_event_custom_view_button"
app:custom_color="@color/colorYellow"
app:custom_size="@dimen/smallTextSize" />

在构造方法中获得自定义属性的值

解析自定义属性时,注意 styleable 是通过 declare-styleable 中名称拼接来的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
final Resources.Theme theme = context.getTheme();
TypedArray a = theme.obtainStyledAttributes(attrs,
R.styleable.CustomView, defStyleAttr, 0);
int n = a.getIndexCount();
for (int i = 0; i < n; i++) {
int attr = a.getIndex(i);
switch (attr) {
// CustomView 和 custom_text 拼接
case R.styleable.CustomView_custom_text:
mText = a.getText(attr).toString();
break;
case R.styleable.CustomView_custom_color:
mColor = a.getColor(attr, Color.BLACK);
break;
case R.styleable.CustomView_custom_size:
int defaultSize = (int) TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_SP,
24, getResources().getDisplayMetrics());
mSize = a.getDimensionPixelSize(attr, defaultSize);
break;
}
}

重写 onMesure

不是必须的,但是大部分都会重写。如果没有重写,当我们设置明确的宽度和高度时,系统帮我们测量的结果就是我们设置的结果;当我们设置为 WRAP_CONTENT 或者 MATCH_PARENT 系统帮我们测量的结果就是 MATCH_PARENT 的长度。所以当设置了 WRAP_CONTENT 时,需要代码中进行测量,即重写 onMesure 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 1. 获取模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
Log.d(TAG, "onMeasure: widthMeasureSpec = " + MeasureSpec.toString(widthMeasureSpec)
+ ", heightMeasureSpec = " + MeasureSpec.toString(heightMeasureSpec));

// 2. 获取文本实际大小 Rect mBound
mPaint.setTextSize(mSize);
mPaint.getTextBounds(mText, 0, mText.length(), mBound);

// 3. 初始化测量高宽
int measureWidth, measureHeight;
// 如果是精确模式,测量高宽就是xml中设置的高宽
if(widthMode == MeasureSpec.EXACTLY){
measureWidth = widthSize;
}else{
// 否则其他模式,设置为文本实际大小的高宽
float textWidth = mBound.width();
measureWidth = (int) (getPaddingLeft() + textWidth + getPaddingRight());
}

if(heightMode == MeasureSpec.EXACTLY){
measureHeight = heightSize;
}else{
float textHeight = mBound.height();
measureHeight = (int) (getPaddingTop() + textHeight + getPaddingBottom());
}

// 4. 设置最终的高宽
setMeasuredDimension(measureWidth, measureHeight);
}

重写 onDraw

主要是通过 PaintCanvas 将需要表达的内容画出来。本例只是仿照 TextView 显示一段文本比较简单,所以只需要画出文本就行。

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 将整个 View 的框画出来,一目了然
mPaint.setColor(Color.BLUE);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);

// 需要显示的文本画出来
mPaint.setColor(mColor);
canvas.drawText(mText, getWidth()/2 - mBound.width()/2,
getHeight()/2 + mBound.height()/2, mPaint);
}

View.draw() 的流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public void draw(Canvas canvas) {
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
*/

// Step 1, draw the background, if needed
drawBackground(canvas);

// skip step 2 & 5 if possible (common case)

// Step 3, draw the content
onDraw(canvas);

// Step 4, draw the children
dispatchDraw(canvas);

// Step 6, draw decorations (foreground, scrollbars)
onDrawForeground(canvas);
...
}

重新布局和绘制

API

  • requestLayout
    会调用 onMeasureonLayout 进行重新测量及布局,但不会调用 draw 的过程,不会重新绘制任何 View 包括该调用者本身
  • invalidate
    只能在 UI 线程中执行。请求重绘 View (也就是 draw方法),哪个 View 请求 invalidate 系列方法,就重绘该 View。即 View 只绘制该 ViewViewGroup 绘制整个 ViewGroup
  • postInvalidate
    UI 线程中请求重绘 View

示例

CustomView 设置文本时请求重新布局和绘制

1
2
3
4
5
public void setText(String text){
mText = text;
requestLayout();
invalidate();
}

目标

  • 自定义 View 常见流程
  • View 的绘制
  • 处理事件分发流程

参考文档

  1. http://blog.csdn.net/lmj623565791/article/details/24252901/
  2. http://blog.csdn.net/congqingbin/article/details/7869730
  3. http://blog.csdn.net/yanbober/article/details/46128379/