您的位置:首页 > 移动开发 > Android开发

Android艺术开发探索第四章——View的工作原理(下)

2017-06-28 19:56 399 查看

Android艺术开发探索第四章——View的工作原理(下)

我们上篇BB了这么多,这篇就多多少少要来点实战了,上篇主席叫我多点自己的理解,那我就多点真诚,少点套路了,老司机,开车吧!

我们这一篇就扯一个内容,那就是自定义View

自定义View
自定义View的分类
自定义View的须知
自定义View的实例
自定义View的思想

一.自定义View的分类

自定义View百花齐放,没有什么具体的分类,不过可以从特性大致的分为4类,其实在我看来,就三类,继承原生View,继承View和继承ViewGroup。

1.继承View重写onDraw方法

重写了绘制,一般就是想自己实现某些图形了,因为原生控件已经满足不了你了,很显然这需要绘制的方式来完成,采用这个方式需要自身支=warp_content,并且pading也要自己处理,比较考验你的功底了

2.继承ViewGroup派生出来的Layout

这个相当于重写容器了,当某些效果看起来像是View的组合的时候,就是他上场的时候了,不过这个很复杂,需要合理的使用测量和布局这两个过程,还要兼顾子元素的这两个过程

3.继承特定的View

比如TextView,就是重写原生的View嘛,比如你想让TextView默认有颜色之类的,有一些小改动,这个就可以用它的,他相对来说比较简单,这个就不需要自己支持包裹内容和pading了

4.继承特定的ViewGroup

这个和上述一样,只不过是重写容器而已,这个也比较常见,事件分发的时候用的也多

二.自定义View的须知

这节大致的说一下注意事项

1.让View支持warp_content

这个在之前将测量的时候说过,如果你不特殊处理一下是达不到满意的效果的,这里就不重复了

2.如果有有必要,让你的View支持padding

这是因为如果你不处理下的话,那么该属性是不会生效的,在ViewGroup也是一样

3.尽量不要在View中使用Handler

为什么不能用,是因为没有必要,View本身就有一系列的post方法,当然,你想用也没人拦着你,我倒是觉得handler写起来代码简洁很多

4.View中如果有线程或者动画,需要及时停止,参考View#onDetachedFromWindow

这个问题那就更好理解了,你要是不停止这个线程或者动画,容易导致内存溢出的,所以你要在一个合适的机会销毁这些资源,在Activity有生命周期,而在View中,当View被remove的时候,onDetachedFromWindow会被调用,,和此方法对应的是onAttachedToWindow

5.View带有滑动嵌套时,需要处理好滑动冲突

滑动冲突之前就BB过,这里就不讲了

三.自定义View的实例

1.继承View重写onDraw方法

我们来实现一个很简单的图形:圆。尽管如此,还是有很多细节需要注意的,实现的过程中需要考虑warp_content和padding,OK,我们先来看代码

public class CircleView extends View {

//颜色
private int mColor = Color.RED;
//画笔样式
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

public CircleView(Context context) {
super(context);
init();
}

public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

//初始化
private void init() {
//设置颜色
mPaint.setColor(mColor);
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//View的宽
int width = getWidth();
//View的高
int height = getHeight();
//圆的半径 = 宽和高比较出的数 / 2
int radiu = Math.min(width, height) / 2;
//绘制圆
canvas.drawCircle(width / 2, height / 2, radiu, mPaint);
}
}
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


1
2
3
45
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

上面的代码就绘制出了一个圆,运行看下效果



上面的代码很简单,估摸着会点自定义的完全能写出来,我们写这个案例就是要抛砖引玉,不信,我们接着看下去,我们把布局改成这个样子

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<com.liuguilin.viewwork.view.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="#000000" />

</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13


1
2
3
4
5
6
7
8
9
10
11
12
13

现在我们来运行一下你就会看到不一样的效果了



接下来我们再调整一下,只给他增加一个

android:layout_margin="20dp"
1


1

这样会是什么效果呢?



这样按理说也是我们预期的效果,对吧,这样的话margin属性是生效的,这是因为margin由父容器所控制的,所以不需要View去动,我们进一步实验,我现在给他继续增加,加上一个padding

android:padding="20dp"
1


1

这里是重头戏了,我们运行后会发现,他没什么反应呀,我们之前说过,如果你直接继承View,在测量的时候需要做点处理的,不然的话,你的warp_content就和match_parent是一样的了。

为了解决这几个问题,我们需要做如下的处理

首先,关于warp_content的问题,我们只需要指定一个warp_content模式宽/高即可,比如设置200px作为默认的宽高

其次,针对padding的问题,我们再绘制的时候考虑进去就好了,修改后的onDraw如下

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//padding值
int left = getPaddingLeft();
int right = getPaddingRight();
int top = getPaddingTop();
int bottom = getPaddingBottom();
//View的宽
int width = getWidth() - left - right;
//View的高
int height = getHeight() - top - bottom;
//圆的半径 = 宽和高比较出的数 / 2
int radiu = Math.min(width, height) / 2;
//绘制圆
canvas.drawCircle(left + width / 2, top + height / 2, radiu, mPaint);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这样就解决了,主要的逻辑就是绘制的时候考虑到View四周的空白即可,圆心和半径都会考虑到,现在我们来运行下,就有效果了



最后,为了让View更加容易应用,我们需要提供一些自定义的属性,这些怎么玩呢,我们继续看

第一步实在values目录下面创建自定义属性的xml,比如attrs.xml,也可以其他名字,名字没什么限制,不过为了规范,还是…你懂的,我们就来写一个

<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="CircleView">
<attr name="circle_color" format="color" />
</declare-styleable>
</resources>
1
2
3
4
5
6


1
2
3
4
5
6

这个很简单吧,我们只定义了一个颜色的属性,这里面有个format是类型,看下就懂了,然后呢

第二步,在View的构造方法里解析到我们这个属性,仔细看代码:

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//没有指定颜色的话默认红色
mColor = type.getColor(R.styleable.CircleView_circle_color, Color.RED);
type.recycle();
init();
}
1
2
3
4
5
6
7
8
9


1
2
3
4
5
6
7
8
9

这段代码就是加载一个资源文件,拿到里面的属性,如果没有指定的话,默认就是红色了,那我们要使用的话,写一个命名空间,然后…:

<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">

<com.liuguilin.viewwork.view.CircleView
android:layout_width="match_parent"
android:layout_height="100dp"
android:layout_margin="20dp"
android:background="#000000"
android:padding="20dp"
app:circle_color="@color/colorPrimary" />

</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上门的布局唯一要注意的就是这个命名空间了 xmlns:app=”http://schemas.Android.com/apk/res-auto”,然后就可以使用app:属性的方式添加了,那我们运行一下,效果也很明显,来看下全部的代码吧:

public class CircleView extends View {

//颜色
private int mColor = Color.RED;
//画笔样式
private Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);

public CircleView(Context context) {
super(context);
init();
}

public CircleView(Context context, AttributeSet attrs) {
super(context, attrs);
}

public CircleView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray type = context.obtainStyledAttributes(attrs, R.styleable.CircleView);
//没有指定颜色的话默认红色
mColor = type.getColor(R.styleable.CircleView_circle_color, Color.RED);
type.recycle();
init();
}

//初始化
private void init() {
//设置颜色
mPaint.setColor(mColor);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSpecSize, 200);
}
}

@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//padding值
int left = getPaddingLeft();
int right = getPaddingRight();
int top =
110f1
getPaddingTop();
int bottom = getPaddingBottom();
//View的宽
int width = getWidth() - left - right;
//View的高
int height = getHeight() - top - bottom;
//圆的半径 = 宽和高比较出的数 / 2
int radiu = Math.min(width, height) / 2;
//绘制圆
canvas.drawCircle(left + width / 2, top + height / 2, radiu, mPaint);
}
}
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


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

这代码清晰脱俗吧,简单好记,就是这样

2.继承ViewGroup派生出来的Layout

这个同等于自定义布局,在之前介绍滑动的时候,有过类似的例子,主席就偷懒的搬上来了,当时分析滑动冲突的两种自定义View:HorizontalScrollViewEx和StickyLayout,其中HorizontalScrollViewEx就是通过继承ViewGroup来实现的,我们再次来分析他的测量和布局过程

这里BB一句,要规范的写View,需要一定的代价,这个,需要去看线性布局去了解了,他们的实现都很复杂,对于HorizontalScrollViewEx来说,就不这么精细了

回顾下HorizontalScrollViewEx的功能,他类似于ViewPager,或者说水平方向的线性布局,它内部的View可以竖直滑动,解决他的冲突的代码就不提了,我们主要还是看下他的测量

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if(childCount ==0){
setMeasuredDimension(0, 0);
}else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth, measureHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize, measureHeight);
}
}
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


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

这里发现一点小bug,不过不碍事,这里的逻辑呢,可以这样理理,首先有没有子元素,没有就全部都是0,有的话再去判断是否是warp_content,,如果是包裹内容,那这个控件的宽度就是所以的总和了,如果高度采用包裹内容,那这个控件就是第一个子元素的高度,这样说应该好理解一点

再回来说说规范性,上面的代码可以说有两点吧,首先,是不应该直接设置为0,还有就是测量的时候没有考虑到padding和子元素的maggin,好的我们继续来看下onLayout

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

这个布局的逻辑也没多少代码,我们拿到子元素之后将其放在合适的位置,位置是从左往右的,但是仍然没有考虑padding和子元素的maggin,这个也不是很规范,好的,那我们直接撸完整代码:

public class HorizontalScrollViewEx extends ViewGroup {

private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;

//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;

//分别记录上次滑动的坐标
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;

private Scroller mScroller;

private VelocityTracker mVelocityTracker;

public HorizontalScrollViewEx(Context context) {
super(context);
init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}

public HorizontalScrollViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}

private void init() {
if (mScroller == null) {
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();

switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false;
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true;
} else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}

@Override
public boolean onTouchEvent(MotionEvent event) {
mVelocityTracker.addMovement(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
if (!mScroller.isFinished()) {
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int scrollX = getScrollX();
mVelocityTracker.computeCurrentVelocity(1000);
float xVelocity = mVelocityTracker.getXVelocity();

if (Math.abs(xVelocity) >= 50) {
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
mChildIndex = Math.max(0, Math.min(mChildIndex, mChildrenSize - 1));
int dx = mChildIndex * mChildWidth - scrollX;
smoothScrollBy(dx, 0);
mVelocityTracker.clear();
break;
}
mLastX = x;
mLastY = y;
return true;
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec, heightMeasureSpec);

int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSpecSize = MeasureSpec.getMode(widthMeasureSpec);
int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSpecSize = MeasureSpec.getMode(heightMeasureSpec);
if (childCount == 0) {
setMeasuredDimension(0, 0);
} else if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth, measureHeight);
} else if (widthSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth, heightSpecSize);
} else if (heightSpecMode == MeasureSpec.AT_MOST) {
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpecSize, measureHeight);
}
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for (int i = 0; i < childCount; i++) {
final View childView = getChildAt(i);
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft, 0, childLeft + childWidth, childView.getMeasuredHeight());
childLeft += childWidth;
}
}

private void smoothScrollBy(int dx, int dy) {
mScroller.startScroll(getScrollX(), 0, dx, 0, 500);
invalidate();
}

@Override
public void computeScroll() {
if (mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}

@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170


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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170

OK,代码慢慢看

四.自定义View的思想

整体来讲,还是有点模糊,不过精髓都已经体现出来了,自定义算是一个综合体系,大多数情况下还是要灵活一点,而且有些可能需要公式计算,所以比较五花八门,那我们这里肯定不能一一去概括了,但是基本功大家应该都已经了解了,我在后续的章节中会挑一些好的View来介绍,这是我,不是书上的,最主要的是基本功然后就是实现思路了,这点我特别推荐去学习优秀的开源库了解一下,好了,我们第四章,View的工作原理到这里就GG了,下章再见!!!

PPT:http://download.csdn.net/detail/qq_26787115/9699388

MakeDown:http://pan.baidu.com/s/1o7Z4Djs 密码:xdgt

Sample:http://download.csdn.net/detail/qq_26787115/9699383

我正在参加2016博客之星,请投我一票吧!

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: