自定义view一直是横在android开发者面前的一道坎。
一、view和viewgroup的关系
从view和viewgroup的关系来看,viewgroup继承view。
view的子类,多是功能型的控件,提供绘制的样式,比如imageview,textview等,而viewgroup的子类,多用于管理控件的大小,位置,如linearlayout,relativelayout等,从下图可以看出
从实际应用中看,他们又是组合关系,我们在布局中,常常是一个viewgroup嵌套多个viewgroup或view,而被嵌套的viewgroup又会嵌套多个viewgroup或view
如下
二、view的绘制流程
从view源码来看,主要关系三个方法:
1、measure():测量
一个final方法,控制控件的大小
2、layout():布局
用来控制自己的布局位置
有相对性,只相对于自己的父类布局,不关心祖宗布局
3、draw():绘制
用来控制控件的显示样式
流程: 流程 measure --> layout --> draw
对应于我们要实现的方法是
onmeasure()
onlayout()
ondraw()
实际绘制中,我们的思考顺序一般是这样的:
是否需要控制控件的大小-->是-->onmeasure()
(1)如果这个自定义view不是viewgroup,onmeasure()方法调用setmeasuredeminsion(width,height):用来设置自己的大小
(2)如果是viewgroup,onmeasure()方法调用 ,child.measure()测量孩子的大小,给出孩子的期望大小值,之后-->setmeasuredeminsion(width,height):用来设置自己的大小
是否需要控制控件的摆放位置-->是 -->onlayout ()
是否需要控制控件的样子-->是 -->ondraw ()-->canvas的绘制
下面是我绘制的流程图:
下面以自定义滑动按钮为例,说明自定义view的绘制流程
我们期待实现这样的效果:
拖动或点击按钮,开关向右滑动,变成
其中开关能随着手指的触摸滑动到相应位置,直到最后才固定在开关位置上
新建一个类继承自view,实现其两个构造方法
1
2
3
4
5
6
7
8
9
10
|
public class switchbuttonview extends view { public switchbuttonview(context context) { this (context, null ); } public switchbuttonview(context context, attributeset attrs) { super (context, attrs); } |
drawable资源中添加这两张图片
借此,我们可以用onmeasure()确定这个控件的大小
1
2
3
4
5
6
7
|
@override protected void onmeasure( int widthmeasurespec, int heightmeasurespec) { mswitchbutton = bitmapfactory.decoderesource(getresources(), r.drawable.switch_background); mslidebutton = bitmapfactory.decoderesource(getresources(), r.drawable.slide_button_background); setmeasureddimension(mswitchbutton.getwidth(), mswitchbutton.getheight()); } |
这个控件并不需要控制其摆放位置,略过onlayout();
接下来ondraw()确定其形状。
但我们需要根据我们点击控件的不同行为来确定形状,这需要重写ontouchevent()
其中的逻辑是:
当按下时,触发事件motionevent.action_down,若此时状态为关:
(1)若手指触摸点(通过event.getx()得到)在按钮的 中线右侧,按钮向右滑动一段距离(event.getx()与开关控件一半宽度之差,具体看下文源码)
(2)若手指触摸点在按钮中线左侧,按钮依旧处于最左(即“关”的状态)。
若此时状态为开:
(1)若手指触摸点在按钮中线左侧,按钮向左滑动一段距离
(2)若手指触摸点在按钮中线右侧,按钮依旧处于最右(即“开”的状态)
当滑动时,触发时间motionevent.action_move,逻辑与按下时一致
注意,ontouchevent()需要设置返回值 为 return true,否则无法响应滑动事件
当手指收起时,若开关中线位于整个控件中线左侧,设置状态为关,反之,设置为开。
具体源码如下所示:(还对外提供了一个暴露此时开关状态的接口)
自定义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
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
|
package com.lian.switchtogglebutton; import android.content.context; import android.graphics.bitmap; import android.graphics.bitmapfactory; import android.graphics.canvas; import android.graphics.paint; import android.util.attributeset; import android.util.log; import android.view.motionevent; import android.view.view; /** * created by lian on 2016/3/20. */ public class switchbuttonview extends view { private static final int state_null = 0 ; //默认状态 private static final int state_down = 1 ; private static final int state_move = 2 ; private static final int state_up = 3 ; private bitmap mslidebutton; private bitmap mswitchbutton; private paint mpaint = new paint(); private int buttonstate = state_null; private float mdistance; private boolean isopened = false ; private onswitchlistener mlistener; public switchbuttonview(context context) { this (context, null ); } public switchbuttonview(context context, attributeset attrs) { super (context, attrs); } @override protected void onmeasure( int widthmeasurespec, int heightmeasurespec) { mswitchbutton = bitmapfactory.decoderesource(getresources(), r.drawable.switch_background); mslidebutton = bitmapfactory.decoderesource(getresources(), r.drawable.slide_button_background); setmeasureddimension(mswitchbutton.getwidth(), mswitchbutton.getheight()); } @override protected void ondraw(canvas canvas) { super .ondraw(canvas); if (mswitchbutton!= null ){ canvas.drawbitmap(mswitchbutton, 0 , 0 , mpaint); } //buttonstate的值在ontouchevent()中确定 switch (buttonstate){ case state_down: case state_move: if (!isopened){ float middle = mslidebutton.getwidth() / 2f; if (mdistance > middle) { float max = mswitchbutton.getwidth() - mslidebutton.getwidth(); float left = mdistance - middle; if (left >= max) { left = max; } canvas.drawbitmap(mslidebutton,left, 0 ,mpaint); } else { canvas.drawbitmap(mslidebutton, 0 , 0 ,mpaint); } } else { float middle = mswitchbutton.getwidth() - mslidebutton.getwidth() / 2f; if (mdistance < middle){ float left = mdistance-mslidebutton.getwidth()/2f; float min = 0 ; if (left < 0 ){ left = min; } canvas.drawbitmap(mslidebutton,left, 0 ,mpaint); } else { canvas.drawbitmap(mslidebutton,mswitchbutton.getwidth()-mslidebutton.getwidth(), 0 ,mpaint); } } break ; case state_null: case state_up: if (isopened){ log.d( "开关" , "开着的" ); canvas.drawbitmap(mslidebutton,mswitchbutton.getwidth()-mslidebutton.getwidth(), 0 ,mpaint); } else { log.d( "开关" , "关着的" ); canvas.drawbitmap(mslidebutton, 0 , 0 ,mpaint); } break ; default : break ; } } @override public boolean ontouchevent(motionevent event) { switch (event.getaction()){ case motionevent.action_down: mdistance = event.getx(); log.d( "down" , "按下" ); buttonstate = state_down; invalidate(); break ; case motionevent.action_move: buttonstate = state_move; mdistance = event.getx(); log.d( "move" , "移动" ); invalidate(); break ; case motionevent.action_up: mdistance = event.getx(); buttonstate = state_up; log.d( "up" , "起开" ); if (mdistance >= mswitchbutton.getwidth() / 2f){ isopened = true ; } else { isopened = false ; } if (mlistener != null ){ mlistener.onswitchchanged(isopened); } invalidate(); break ; default : break ; } return true ; } public void setonswitchlistener(onswitchlistener listener){ this .mlistener = listener; } public interface onswitchlistener{ void onswitchchanged( boolean isopened); } } |
demoactivity:
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
|
package com.lian.switchtogglebutton; import android.os.bundle; import android.support.v7.app.appcompatactivity; import android.widget.toast; public class mainactivity extends appcompatactivity { @override protected void oncreate(bundle savedinstancestate) { super .oncreate(savedinstancestate); setcontentview(r.layout.activity_main); switchbuttonview switchbuttonview = (switchbuttonview) findviewbyid(r.id.switchbutton); switchbuttonview.setonswitchlistener( new switchbuttonview.onswitchlistener() { @override public void onswitchchanged( boolean isopened) { if (isopened) { toast.maketext(mainactivity. this , "打开" , toast.length_short).show(); } else { toast.maketext(mainactivity. this , "关闭" , toast.length_short).show(); } } }); } } |
布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
<?xml version= "1.0" encoding= "utf-8" ?> <relativelayout xmlns:android= "http://schemas.android.com/apk/res/android" xmlns:tools= "http://schemas.android.com/tools" android:layout_width= "match_parent" android:layout_height= "match_parent" android:paddingbottom= "@dimen/activity_vertical_margin" android:paddingleft= "@dimen/activity_horizontal_margin" android:paddingright= "@dimen/activity_horizontal_margin" android:paddingtop= "@dimen/activity_vertical_margin" tools:context= "com.lian.switchtogglebutton.mainactivity" > <com.lian.switchtogglebutton.switchbuttonview android:id= "@+id/switchbutton" android:layout_width= "wrap_content" android:layout_height= "wrap_content" /> </relativelayout> |
以上就是本文的全部内容,希望对大家的学习有所帮助。