android 自定义View

概览:

  1. View 的生命周期
  2. Traversals
  3. Measure
  4. Draw
  5. Custom Attributes
  6. 监听事件很简单,不讲了
  7. 状态保存
  8. 参考链接

1. View 的生命周期

  • Attachment/detachment(一般不用care)
  • Traversals(必须care)
  • State save/restore(看你的需求了)

Attachment的回调接口是onAttachedToWindow(),在这里你可以做以下操作

  • Call super.onAttachedToWindow()!
  • Perform any relevant state resets
  • Start listening for state changes

detachment的回调接口是onDetachedFromWindow(),在这里你可以做以下操作

  • Call super.onDetachedFromWindow()!
  • Remove any posted Runnables
  • Stop listening for data changes
  • Clean up resources
    • Bitmaps
    • Threads

看看官方的SwipeRefreshLayout的运用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onAttachedToWindow() {
super.onAttachedToWindow();
removeCallbacks(mCancel);
removeCallbacks(mReturnToStartPosition);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
removeCallbacks(mReturnToStartPosition);
removeCallbacks(mCancel);
}

打个简单地比喻就是,如果你的View和网络操作相关的话,最好在这里做取消操作。它这里的操作其实就是取消或重置动画。好了,这里就不多说了,一般用不到。下面说说遍历Traversals。

2. Traversals

Traversals 分四个阶段

Animate—>Measure—>Layout—>Draw

我们现在关心的是Measure和Draw,分别对应onMeasure(),onDraw(),先看看View的默认实现吧。

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
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}
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:
result = specSize;
break;
}
return result;
}
protected void onDraw(Canvas canvas) {
}

现在来看看具体的Measure

3. Measure

如果你自定义的View没有特殊尺寸要求的话,也可以不重写onMeasure()。现在来说说比较难理解的MeasureSpec 模式,三张图搞定它

MeasureSpec.EXACTLY

MeasureSpec.AT_MOST

MeasureSpec.UNSPECIFIED

onMeasure()里由于没有回调,框架依赖你最后设置尺寸,所以你一定要调用setMeasuredDimension(),否则运行时会crash。

看看大牛Dave Smith 给的一个模板吧:

1
2
3
4
5
6
7
8
9
10
11
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
//Get the width measurement
int widthSize = MeasureUtils.getMeasurement(widthMeasureSpec, getDesiredWidth());
//Get the height measurement
int heightSize = MeasureUtils.getMeasurement(heightMeasureSpec, getDesiredHeight());
//MUST call this to store the measurements
setMeasuredDimension(widthSize, heightSize);
}

工具方法

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
public class MeasureUtils {
/**
* Utility to return a view's standard measurement. Uses the
* supplied size when constraints are given. Attempts to
* hold to the desired size unless it conflicts with provided
* constraints.
*
* @param measureSpec Constraints imposed by the parent
* @param contentSize Desired size for the view
* @return The size the view should be.
*/
public static int getMeasurement(int measureSpec, int contentSize) {
int specMode = View.MeasureSpec.getMode(measureSpec);
int specSize = View.MeasureSpec.getSize(measureSpec);
int resultSize = 0;
switch (specMode) {
case View.MeasureSpec.UNSPECIFIED:
//Big as we want to be
resultSize = contentSize;
break;
case View.MeasureSpec.AT_MOST:
//Big as we want to be, up to the spec
resultSize = Math.min(contentSize, specSize);
break;
case View.MeasureSpec.EXACTLY:
//Must be the spec size
resultSize = specSize;
break;
}
return resultSize;
}
}

4. Draw

这其实没有太多要说的,onDraw()回调已经给你了一个Canvas,你要做的就是画各种你需要的东西了。看看Canvas的官方定义吧:

The Canvas class holds the “draw” calls. To draw something, you need 4 basic components: A Bitmap to hold the pixels, a Canvas to host the draw calls (writing into the bitmap), a drawing primitive (e.g. Rect, Path, text, Bitmap), and a paint (to describe the colors and styles for the drawing).

这里的Canvas已经包含一个mutable 的Bitmap了。需要注意的是onDraw()调用的很频繁,实例化对象的操作应该都放到外面。随便看个例子吧:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.d(tag,"onDraw called");
int w = this.getWidth();
int h = this.getHeight();
int t = this.getTop();
int l = this.getLeft();
int ox = w/2;
int oy = h/2;
int rad = Math.min(ox,oy)/2;
canvas.drawCircle(ox, oy, rad, getBrush());
}

其实这里的获取宽高的操作应该放到onSizeChanged()里面,这个是在onLayout()里调用的,而且一般只遍历一次。

5. Custom Attributes

这个没什么要讲的,看例子就理解了。

1
2
3
4
5
6
7
Listing 1-18. Defining Custom Attributes in attrs.xml
<resources>
<declare-styleable name="CircleView">
<attr name="strokeWidth" format="integer"/>
<attr name="strokeColor" format="color|reference" />
</declare-styleable>
</resources>

下面这段好好理解
Given the attrs.xml in Listing 1-18, Android generates the following IDs:

1
2
3
4
5
R.attr.strokeWidth (int)
R.attr.srokeColor (int)
R.styleable.CircelView (an array of ints)
R.styleable.CircleView_strokeWidth (offset into the array)
R.styleable.CircelView_strokeColor (offset into the array)

在初始化时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public CircleView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
//Use the array constant to read the bag once
TypedArray t = context.obtainStyledAttributes(attrs,
R.styleable.CircleView,
defStyle, //if any values are in the theme
0); //Do you have your own style group
//Use the offset in the bag to get your value
strokeColor = t.getColor(R.styleable.CircleView_strokeColor, mDefaultStrokeColor);
strokeWidth = t.getInt(R.styleable.CircleView_strokeWidth, mDefaultStrokeWidth);
//Recycle the typed array
t.recycle();
//Go ahead and initialize your class.
initCircleView();
}

DON’T FORGET: TypedArrays are heavyweight objects that should be recycled immediately after all the attributes you need have been extracted.

6. 监听事件很简单,不讲了

7. 状态保存

用系统的方式保存状态,万无一失,下面看模板:

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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/*
* ***************************************************************
* Save and restore work
* ***************************************************************
*/
@Override
protected void onRestoreInstanceState(Parcelable p)
{
this.onRestoreInstanceStateStandard(p);
this.initCircleView();
}
@Override
protected Parcelable onSaveInstanceState()
{
return this.onSaveInstanceStateStandard();
}
/*
* ***************************************************************
* Use a simpler approach
* ***************************************************************
*/
private void onRestoreInstanceStateSimple(Parcelable p)
{
if (!(p instanceof Bundle))
{
throw new RuntimeException("unexpected bundle");
}
Bundle b = (Bundle)p;
defRadius = b.getInt("defRadius");
Parcelable sp = b.getParcelable("super");
//No need to call parent. It is just a base view
super.onRestoreInstanceState(sp);
}
private Parcelable onSaveInstanceStateSimple()
{
//Don't call the base class as it will return a null
Parcelable p = super.onSaveInstanceState();
Bundle b = new Bundle();
b.putInt("defRadius",defRadius);
b.putParcelable("super",p);
return b;
}
/*
* ***************************************************************
* Use a standard approach
* ***************************************************************
*/
private void onRestoreInstanceStateStandard(Parcelable state)
{
//If it is not yours doesn't mean it is BaseSavedState
//You may have a parent in your hierarchy that has their own
//state derived from BaseSavedState
//It is like peeling an onion or a Russian doll
if (!(state instanceof SavedState)) {
super.onRestoreInstanceState(state);
return;
}
//it is our state
SavedState ss = (SavedState)state;
//Peel it and give the child to the super class
super.onRestoreInstanceState(ss.getSuperState());
defRadius = ss.defRadius;
}
private Parcelable onSaveInstanceStateStandard()
{
Parcelable superState = super.onSaveInstanceState();
SavedState ss = new SavedState(superState);
ss.defRadius = this.defRadius;
return ss;
}
/*
* ***************************************************************
* Saved State inner static class
* ***************************************************************
*/
public static class SavedState extends BaseSavedState {
int defRadius;
SavedState(Parcelable superState) {
super(superState);
}
@Override
public void writeToParcel(Parcel out, int flags) {
super.writeToParcel(out, flags);
out.writeInt(defRadius);
}
//Read back the values
private SavedState(Parcel in) {
super(in);
defRadius = in.readInt();
}
@Override
public String toString() {
return "CircleView defRadius:" + defRadius;
}
@SuppressWarnings("hiding")
public static final Parcelable.Creator<SavedState> CREATOR
= new Parcelable.Creator<SavedState>() {
public SavedState createFromParcel(Parcel in) {
return new SavedState(in);
}
public SavedState[] newArray(int size) {
return new SavedState[size];
}
};
}//eof-state-class

注意:你需要在初始化时,调用this.setSaveEnabled(true);,当然,状态保存也不是必须的,看你的需求了。

8. 参考链接

https://www.youtube.com/watch?v=NYtB6mlu7vA
https://thenewcircle.com/s/post/1663/tutorial_enhancing_android_ui_with_custom_views_dave_smith_video
https://dl.dropboxusercontent.com/u/16714463/Google%20IO%202014/Material%20Witness.pdf
http://developer.android.com/training/custom-views/index.html
http://developer.android.com/guide/topics/ui/custom-components.html

《Expert Android》