Adnroid鬼点子-自定义Seek控制条


       之前有一篇博文简单的介绍了如何自定义View,以及自定义View的无痛上手,本篇主要介绍的是ViewGroup的自定义,同样也是以一个例子为介绍的。

       下面是那篇博文的传送门上手自定义View

       Android原生的SeekBar(进度控制条)是这样的

来自http://blog.csdn.net/qq942418300/article/details/7171593

       这次使用的例子是这样的,这是一个方形的SeekBar。手指头在这个控件的上方滑动,可以滑出一个扇形,同时可以回调当前的进度,外加手指触摸到这个控件的时候,有个缩放的效果。

tu.gif

       这个例子的关键是触摸事件的处理和子View的布局。Andriod的事件传递机制在我的这篇View的事件分发机制博文里面做了介绍,不太了解的同学可以先去看看。

       首先这里有个背景和一个扇形,还有一个图标,图标使用的是IconFont,因为是IconFont,所以我自定义了以个TextView,以方便使用自定义的字体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package UI;
import android.content.Context;
import android.graphics.Typeface;
import android.util.AttributeSet;
import android.widget.TextView;
/**
* Created by GreendaMi on 2016/11/25.
*/
public class IconFontTextView extends TextView {
public IconFontTextView(Context context) {
super(context);
}
public IconFontTextView(Context context, AttributeSet attrs) {
super(context, attrs);
Typeface typeface = Typeface.createFromAsset(context.getAssets(), "iconfont/iconfont.ttf");
setTypeface(typeface);
}
}

我把需要用带的字体放在了Asset文件夹下面的/iconfont/iconfont.ttf。在初始化TextView的时候进行了加载。

       貌似有点跑题了,这个控件有一个背景和一个扇形,很简单,背景使用View原来的Background就可以了,扇形是我们自己画的。这个控件的名字叫做Seek。在有2个参数的构造方法中:读取了自定义参数active-扇形的颜色,并且初始化了画笔。

1
2
3
4
5
6
7
8
9
10
11
12
public Seek(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs , R.styleable.Seek);
active = a.getColor(R.styleable.Seek_active, Color.WHITE);
a.recycle();
mPain.setColor(active);
mPain.setAlpha(50);
mPain.setAntiAlias(false);
mPain.setStyle(Paint.Style.FILL);
}

       然后是控件的测量阶段,这里直接使用的布局文件中传过来的宽和高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
offset = (int)(Math.min(widthSpecSize,heightSpecSize) * 0.3);
int cCount = getChildCount();
for (int i = 0; i < cCount; i++) {
View child = getChildAt(i);
// 测量每一个child的宽和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
//这里直接设成来自父控件大小的一个正方形
setMeasuredDimension(widthSpecSize, heightSpecSize);
}

       offset = (int)(Math.min(widthSpecSize,heightSpecSize) 0.3);*这个offset是扇形所在的矩形,与控件的边界矩形的差。也就是说扇形所在的矩形要大于控件的边界矩形,这样才能保证扇形不会出现那条弧线。

       然后是子View的布局,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void onLayout(boolean b, int i0, int i1, int i2, int i3) {
int cCount = getChildCount();
int cWidth = 0;
int cHeight = 0;
LayoutParams cParams = null;
int cl = 0, ct = 0, cr = 0, cb = 0;
for (int i = 0; i < cCount; i++) {
View childView = getChildAt(i);
cWidth = childView.getMeasuredWidth();
cHeight = childView.getMeasuredHeight();
ct = (heightSpecSize - cHeight)/2;
cl = (widthSpecSize - cWidth)/2;
cr = cWidth + cl;
cb = cHeight + ct;
childView.layout(cl , ct , cr, cb);
}
}

       要在onMeasure中调用了measureChild(child, widthMeasureSpec, heightMeasureSpec);在onlayout中才会正确的得到cWidth = childView.getMeasuredWidth();和cHeight = childView.getMeasuredHeight();最后调用childView.layout(cl , ct , cr, cb);直接布局,这里没有什么特殊的地方,就是直接把子View居中放置。

       然后就是绘制了

1
2
3
4
5
6
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制活动区域
RectF rect = new RectF(-offset , -offset , widthSpecSize + offset , heightSpecSize + offset);
canvas.drawArc(rect , 0 , -current , true , mPain);
}

canvas.drawArc是画扇形的方法,current是当前的角度。我们要通过监听触摸事件,来改变current的值,然后触发重绘,就可以达到上面的效果了。

       铛铛铛,敲黑板!这里是重点啦!

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if(ev.getX() <= widthSpecSize && ev.getY() <= heightSpecSize){
return super.dispatchTouchEvent(ev);
}
return false;
}
Seek.this.setScaleX(1f);
Seek.this.setScaleY(1f);
return true;
}

       dispatchTouchEvent方法判断事件时候拦截。当手指在控件范围内移动的时候会拦截,当手指在控件范围外移动的时候不拦截。也就是说当你的手指从控件里面移动到控件外面的话,如果外面使用了ScollerView的话,是会响应ScollerView的触摸事件的,ScollerView会进行滚动。当手指一直在控件内移动的话,这个事件则不会被传递到ScollerView,ScollerView不会滚动。

       如果调用了super.dispatchTouchEvent(ev);的话,那么onTouchEvent(MotionEvent event)则会被调用,onTouchEvent主要是触摸事件的处理。

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
@Override
public boolean onTouchEvent(MotionEvent event) {
Point center = new Point(widthSpecSize / 2, heightSpecSize / 2);
Point now = new Point((int)(event.getX()),(int)(event.getY()));
int x=(now.x - center.x );
int y=(center.y - now.y);
setCurrent((int)(Math.toDegrees(Math.atan2(y , x))));
if(mOnSeekChangeLisner != null){
//此处有触屏优化
if(current > 300){
mOnSeekChangeLisner.onChange(0);
}else if(current > 180){
mOnSeekChangeLisner.onChange(100);
}else{
mOnSeekChangeLisner.onChange((int)(current/1.8));
}
}
Seek.this.setScaleX(0.95f);
Seek.this.setScaleY(0.95f);
return false;
}

center是控件的中心点,同时也是扇形的中心点。Math.toDegrees(Math.atan2(y , x)))算出的是手指触摸点的角度。因为一周是360度,而大于300度的时候也就是-60到0度之间用户经常会误操作,所以人为设置成0.180度到300度之间,显式的是180度。onChange是回调方法,为了让外部知道当前的进度,180度的时候是100%,0度的时候是0%。Seek.this.setScaleX(0.95f);这里是点击的效果(缩放)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 获取当前进度
* @return
*/
public int getCurrent() {
return current;
}
/**
* 设置进度,并请求重绘
* @param current
*/
public void setCurrent(int current) {
if(current > 360)
this.current = 360;
else if(current < 0)
this.current = current + 360 ;
else this.current = current;
if(this.current <= 180){
invalidate();
}
}

只有角度在0~180之间的时候才会触发重绘invalidate();前面的两句是为了控制角度在0~360之间,不会出现负的和超过360度的情况。

       到此这个控件就写完了,下面是完整代码:

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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
package UI;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.greendami.wellrelax.R;
import Listener.onSeekChangeLisner;
/**
* Created by GreendaMi on 2016/11/25.
*/
public class Seek extends ViewGroup {
private int active;//进度色
private int current = 0;//当前进度
int widthSpecSize;
int heightSpecSize;
int offset = 200;
public Paint mPain = new Paint();
public onSeekChangeLisner mOnSeekChangeLisner;
public Seek(Context context) {
super(context);
}
public Seek(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs , R.styleable.Seek);
active = a.getColor(R.styleable.Seek_active, Color.WHITE);
a.recycle();
mPain.setColor(active);
mPain.setAlpha(50);
mPain.setAntiAlias(false);
mPain.setStyle(Paint.Style.FILL);
}
public Seek(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
offset = (int)(Math.min(widthSpecSize,heightSpecSize) * 0.3);
int cCount = getChildCount();
for (int i = 0; i < cCount; i++) {
View child = getChildAt(i);
// 测量每一个child的宽和高
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
//这里直接设成来自父控件大小的一个正方形
setMeasuredDimension(widthSpecSize, heightSpecSize);
}
/**
* 子View布局
* @param b
* @param i0
* @param i1
* @param i2
* @param i3
*/
@Override
protected void onLayout(boolean b, int i0, int i1, int i2, int i3) {
int cCount = getChildCount();
int cWidth = 0;
int cHeight = 0;
LayoutParams cParams = null;
int cl = 0, ct = 0, cr = 0, cb = 0;
for (int i = 0; i < cCount; i++) {
View childView = getChildAt(i);
cWidth = childView.getMeasuredWidth();
cHeight = childView.getMeasuredHeight();
ct = (heightSpecSize - cHeight)/2;
cl = (widthSpecSize - cWidth)/2;
cr = cWidth + cl;
cb = cHeight + ct;
childView.layout(cl , ct , cr, cb);
}
}
/**
* 获取当前进度
* @return
*/
public int getCurrent() {
return current;
}
/**
* 设置进度,并请求重绘
* @param current
*/
public void setCurrent(int current) {
if(current > 360)
this.current = 360;
else if(current < 0)
this.current = current + 360 ;
else this.current = current;
if(this.current <= 180){
invalidate();
}
}
/**
* 扇形的外矩形,与背景矩形的差
* @return
*/
public int getOffset() {
return offset;
}
public void setOffset(int offset) {
this.offset = offset;
invalidate();
}
public onSeekChangeLisner getOnSeekChangeLisner() {
return mOnSeekChangeLisner;
}
public void setOnSeekChangeLisner(onSeekChangeLisner onSeekChangeLisner) {
mOnSeekChangeLisner = onSeekChangeLisner;
}
/**
* 绘制
* @param canvas
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//绘制活动区域
RectF rect = new RectF(-offset , -offset , widthSpecSize + offset , heightSpecSize + offset);
canvas.drawArc(rect , 0 , -current , true , mPain);
}
/**
* 通知父View,这个事件是不是你处理
* @param ev
* @return
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_MOVE:
if(ev.getX() <= widthSpecSize && ev.getY() <= heightSpecSize){
return super.dispatchTouchEvent(ev);
}
return false;
}
Seek.this.setScaleX(1f);
Seek.this.setScaleY(1f);
return true;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
Point center = new Point(widthSpecSize / 2, heightSpecSize / 2);
Point now = new Point((int)(event.getX()),(int)(event.getY()));
int x=(now.x - center.x );
int y=(center.y - now.y);
setCurrent((int)(Math.toDegrees(Math.atan2(y , x))));
if(mOnSeekChangeLisner != null){
//此处有触屏优化
if(current > 300){
mOnSeekChangeLisner.onChange(0);
}else if(current > 180){
mOnSeekChangeLisner.onChange(100);
}else{
mOnSeekChangeLisner.onChange((int)(current/1.8));
}
}
Seek.this.setScaleX(0.95f);
Seek.this.setScaleY(0.95f);
return false;
}
}

在布局文件中这样使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
xmlns:seek="http://schemas.android.com/apk/res-auto"
...
<UI.Seek
android:id="@+id/seek_favorites"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@color/fifteen"
seek:active="#ffffff"
>
<UI.IconFontTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:text="@string/favorites"
android:textColor="#ffffff"
android:textSize="@dimen/iconsize"/>
</UI.Seek>

attrs.xml中定义属性。

1
2
3
4
5
<resources>
<declare-styleable name="Seek">
<attr name="active" format="color"></attr>
</declare-styleable>
</resources>

java代码中设置回调

1
2
3
4
5
6
mSeekBar.setOnSeekChangeLisner(new onSeekChangeLisner() {
@Override
public void onChange(int per) {
...
}
});

       这个完整的例子是我自己做的一款白噪音App,有14中音效,简单的定时功能。App中没有使用图片,完全是IconFont完成的。其中还封装了定时任务功能。

20161128-1.png

20161128-2.png

APK安装包

完整代码–> github

代码未经过完全测试,如有Bug请联系:zpybless@163.com 同时寻找帝都工作机会!

文章目录
|