儿童节快乐!!!
前言
公司项目中使用了时间选择器,原先使用的时间选择器控件无法满足我的需求,于是选择自己造(抄)个轮子。
国际惯例 - 效果展示(GIF可能耗时比较长,请耐心观看——23S)
接下来肯定是挂源码了:Github
正文
绘制目标View的大致草图
绘制自定义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
26package com.yooking.tools.wheelview;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
/**
* 自定义WheelView
* Created by yooking on 2020/6/1.
* Copyright (c) 2020 yooking. All rights reserved.
*/
public class WheelView extends View {
public WheelView(Context context) {
super(context);
}
public WheelView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public WheelView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}界面绘制有两种方式:
- 根据用户定义的view宽高来确定每行数据的宽高
- 根据用户定义的单行宽高来绘制view的宽高
这里使用第二种方法 —— 因此需要重写
onMeasure
方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15private int measuredWidth;//View整体宽度
private int measuredHeight;//View整体高度
private float itemHeight = 50;//单项高度 - 默认50
private int showNum = 5;//展示在界面中的项目数量 - 默认5
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
measuredWidth = getMeasuredWidth();
if (measuredWidth != 0) {
measuredHeight = (int) (itemHeight * showNum);
setMeasuredDimension(measuredWidth, measuredHeight);
}
}绘制选中框 —— 重写
omMeasure
方法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
42private Paint linePaint;
private int lineColor = getResources().getColor(R.color.color_line);
private int lineWidth = 2;
private float topLineY;//上线条高度
private float bottomLineY;//下线条高度
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
//根据草图可知:绘制选区 选区可以用两条横线确定
//这里先new 下画笔
//onDraw会重复调用,画笔不需要重复new,且重复的new操作会造成内存抖动 - 严重可能造成内存溢出
initPaint();
drawLine(canvas);
}
private void initPaint() {
if (linePaint == null) {
linePaint = new Paint();
linePaint.setColor(lineColor);//设置线条颜色
linePaint.setStrokeWidth(lineWidth);//设置线宽
}
}
private void drawLine(Canvas canvas) {
//分析
//计算线条所在位置 - 上线条为中央选中区域的最上方-线宽 下线条所在位置为中央选中区域的最下方
//获取选中区域所在位置 - 如为奇数,选中区域为View正中心,如为偶数,选中区域为View中心上方
int selectedIndex;//选中项
// if (showNum % 2 == 1) {//取余法判断奇偶性
// selectedIndex = showNum / 2;//计算选中项为第几项
// } else {
// selectedIndex = showNum / 2 - 1;
// }
//改写上述if else
selectedIndex = showNum / 2 - 1 + showNum % 2;
topLineY = defSelectedIndex * itemHeight - lineWidth / 2;//线条中间和选中框重叠
bottomLineY = defSelectedIndex * itemHeight + itemHeight - lineWidth / 2;//线条中间和选中框重叠
canvas.drawLine(0,topLineY,measuredWidth,topLineY,linePaint);//绘制上线条
canvas.drawLine(0,bottomLineY,measuredWidth,bottomLineY,linePaint);//绘制上线条
}引入布局文件中预览下效果:
接下来就该绘制列表文字了:
创建文本类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24private class TextEntity {
int position; //当前文字的行数
String text; //文本内容
//float y; //文本所在的y值
public int getPosition() {
return position;
}
public void setPosition(int position) {
this.position = position;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public float getY() {//每个的原始y值和position相关
return position * itemHeight + itemHeight / 2;
}
}
在initPaint
中增加文字画笔:
1 | private void initPaint() { |
制造假数据:
1 | private List<TextEntity> textList;//展示用的数据 |
绘制文本:
1 | private void drawTextList(Canvas canvas) { |
看看效果
效果还是基本符合预期的
为自定义View添加滚动事件(手势监听)
重写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
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
54private static final int MOVE_INERTIA = 10086;//惯性运动
private static final int MOVE_ACTION = 10010;//匀速运动
private static final int MOVE_BACK = -1;//回弹
private float vInertia;//惯性滚动的移动速度
private float vAction;//点击滚动的移动速度
private float lastY = 0;//按下的位置
//move 偏移量 上方已定义
private float lastMove = 0;//记录手势抬起时的偏移量
private float nowMove = 0;//本次移动的值
private long lastDownTime = 0L;//上次按下的时间戳
private static final float MIN_MOVE = 10;//最小偏移量 当偏移量小于这个值时视为点击事件
"ClickableViewAccessibility") (
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//按下事件
nowMove = 0;//将本次移动距离置零
if (handler != null) {//如果handler存在 按下时阻止继续滚动
handler.removeMessages(MOVE_INERTIA);
handler.removeMessages(MOVE_ACTION);
handler.removeMessages(MOVE_INERTIA);
}
lastDownTime = new Date().getTime();
lastY = event.getRawY();
break;
case MotionEvent.ACTION_MOVE:
//移动事件
nowMove = event.getRawY() - lastY;
move = nowMove + lastMove;
invalidate();//刷新布局
break;
case MotionEvent.ACTION_UP:
long moveTime = new Date().getTime() - lastDownTime;//本次移动的时间间隔
float lastAbsMove = Math.abs(nowMove);
if (lastAbsMove <= MIN_MOVE) {//当移动距离小于最小移动距离,视为点击事件,跳转到目标位置
int clickNum = (int) (event.getY() / itemHeight);
int moveNum = clickNum - defSelectedIndex;//移动坐标数 每个坐标单位为itemHeight
vAction = itemHeight * moveNum / 20;//100毫秒内走完 移动单位为5毫秒
//匀速滚动
handler.sendEmptyMessage(MOVE_ACTION);
} else {
vInertia = nowMove * 5 / moveTime;//单位 5毫秒 初速度
//惯性滚动
handler.sendEmptyMessage(MOVE_INERTIA);
}
break;
}
return event.getAction() == MotionEvent.ACTION_DOWN ||
event.getAction() == MotionEvent.ACTION_MOVE ||
event.getAction() == MotionEvent.ACTION_UP;
}UI线程中添加Handler
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
78private Handler handler = new Handler(Looper.getMainLooper()) {
private float a = 1;//减速度 单位 5毫秒
private int duration = 100;//actionMove时间为300毫秒
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MOVE_INERTIA:
inertiaMove();
break;
case MOVE_ACTION:
actionMove();
break;
case MOVE_BACK:
springBack();
break;
}
}
private void inertiaMove() {
//惯性运动
//设定阻力 匀减速运动 减速度 a
vInertia = (Math.abs(vInertia) - a) * (vInertia > 0 ? 1 : -1);
float absV = Math.abs(vInertia);
if (absV >= a) {//当速度大于加速度时
move += vInertia;
lastMove = move;
invalidate();
handler.sendEmptyMessageDelayed(MOVE_INERTIA, 5);
} else {//当惯性速度小于加速度时停止
handler.sendEmptyMessageDelayed(MOVE_BACK, 5);
}
}
private void actionMove() {
//匀速运动
move -= vAction;
lastMove = move;
invalidate();
duration -= 5;
if (duration > 0) {
handler.sendEmptyMessageDelayed(MOVE_ACTION, 5);
} else {
duration = 100;
handler.sendEmptyMessageDelayed(MOVE_BACK, 5);
}
}
private void springBack() {
//回弹
//当文字不在选中项正中间时,移动到正中间
float deviation;
if (move > 0) {
//计算偏差值
deviation = move % itemHeight;
if (2 * deviation <= itemHeight) //偏差值 小于 itemHeight的一半,复原 否则移动到下一项
move -= deviation;
else
move = move - deviation + itemHeight;
} else {
deviation = -move % itemHeight;
if (2 * deviation <= itemHeight) //偏差值 小于 itemHeight的一半,复原 否则移动到下一项
move += deviation;
else
move = move + deviation - itemHeight;
}
//当选择器超出文字时,回弹
//defSelectedIndex 默认选中位置 即选中区所在的位置
float max = itemHeight * (textList.size() - 1 - defSelectedIndex);
if (move > defSelectedIndex * itemHeight) move = defSelectedIndex * itemHeight;
else if (-move > max) move = -max;
lastMove = move;
invalidate();
}
};在
attrs.xml
中自定义参数1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<resources>
<declare-styleable name="WheelView">
<!--基本信息-->
<attr name="itemHeight" format="dimension" />
<attr name="showNum" format="integer" />
<!--选中线条相关-->
<attr name="lineColor" format="color" />
<attr name="lineWidth" format="dimension" />
<!--选项分割线相关-->
<attr name="isShowDivider" format="boolean" />
<attr name="dividerColor" format="color" />
<attr name="dividerWidth" format="dimension" />
<!--选中项相关-->
<attr name="selectedColor" format="color" />
<attr name="selectedSize" format="dimension" />
<!--非选相关-->
<attr name="unSelectedColor" format="color" />
<attr name="unSelectedSize" format="dimension" />
</declare-styleable>
</resources>自定义View中添加
init(Context context, @Nullable AttributeSet attrs)
方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25private void init(Context context, @Nullable AttributeSet attrs) {
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.WheelView);
//基本信息
itemHeight = typedArray.getDimension(R.styleable.WheelView_itemHeight, itemHeight);
showNum = typedArray.getInteger(R.styleable.WheelView_showNum, showNum);
//选中线条相关
lineColor = typedArray.getColor(R.styleable.WheelView_lineColor, lineColor);
lineWidth = typedArray.getDimension(R.styleable.WheelView_lineWidth, lineWidth);
//分割线相关
isShowDivider = typedArray.getBoolean(R.styleable.WheelView_isShowDivider, isShowDivider);
dividerColor = typedArray.getColor(R.styleable.WheelView_dividerColor, dividerColor);
dividerWidth = typedArray.getDimension(R.styleable.WheelView_dividerWidth, dividerWidth);
//选中项相关
selectedColor = typedArray.getColor(R.styleable.WheelView_selectedColor, selectedColor);
selectedSize = typedArray.getDimension(R.styleable.WheelView_selectedSize, selectedSize);
//非选相关
unSelectedColor = typedArray.getColor(R.styleable.WheelView_unSelectedColor, unSelectedColor);
unSelectedSize = typedArray.getDimension(R.styleable.WheelView_unSelectedSize, unSelectedSize);
typedArray.recycle();
}调用:
1
2
3
4
5
6
7
8
9public WheelView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public WheelView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}布局中使用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<com.yooking.tools.wheelview.WheelView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/white"
app:itemHeight="@dimen/dp_50"
app:showNum="6"
app:selectedSize="@dimen/sp_30"
app:selectedColor="#e24223"
app:unSelectedSize="@dimen/sp_25"
app:unSelectedColor="#999999"
app:isShowDivider="false"
app:dividerColor="#0000ff"
app:dividerWidth="@dimen/dp_1"
app:lineWidth="@dimen/dp_1"
app:lineColor="@color/color_red_e24234"
/>
<!-- isShowDivider 丑,慎用-->使用代码控制布局
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
107private void initDefault() {//initDefault在实例化过程中加载
defSelectedIndex = showNum / 2 - 1 + showNum % 2;
index = defSelectedIndex;
}
public WheelView<T> setItemHeight(float itemHeight) {
this.itemHeight = dp2px(itemHeight);
measuredHeight = (int) (itemHeight * showNum);
return this;
}
public WheelView<T> setShowNum(int showNum) {
this.showNum = showNum;
measuredHeight = (int) (itemHeight * showNum);
initDefault();
return this;
}
public WheelView<T> setLineColor(@ColorInt int lineColor) {
this.lineColor = lineColor;
return this;
}
public WheelView<T> setLineWidth(float lineWidth) {
this.lineWidth = dp2px(lineWidth);
return this;
}
public WheelView<T> setShowDivider(boolean showDivider) {
isShowDivider = showDivider;
return this;
}
public WheelView<T> setDividerColor(@ColorInt int dividerColor) {
this.dividerColor = dividerColor;
return this;
}
public WheelView<T> setDividerWidth(float dividerWidth) {
this.dividerWidth = dp2px(dividerWidth);
return this;
}
public WheelView<T> setSelectedColor(@ColorInt int selectedColor) {
this.selectedColor = selectedColor;
return this;
}
public WheelView<T> setSelectedSize(float selectedSize) {
this.selectedSize = sp2px(selectedSize);
return this;
}
public WheelView<T> setUnSelectedColor(@ColorInt int unSelectedColor) {
this.unSelectedColor = unSelectedColor;
return this;
}
public WheelView<T> setUnSelectedSize(float unSelectedSize) {
this.unSelectedSize = sp2px(unSelectedSize);
return this;
}
/**
* 插入数据
*
* @param data 数据
*/
public WheelView<T> setData(List<T> data) {
this.data = data;
if (textList == null) {
textList = new ArrayList<>();
} else {
textList.clear();
}
for (int i = 0; i < data.size(); i++) {
TextEntity entity = new TextEntity();
entity.setPosition(i);
entity.setText(data.get(i).getName());
textList.add(entity);
}
return this;
}
/**
* 移动指针
*
* @param position 选中项
*/
public void change2Index(int position) {
//由于 defSelectedIndex 在界面未加载完成时为0,因此将defSelectedIndex提到initDefault中
this.index = position;//index为当前选中项
move = (defSelectedIndex - position) * itemHeight;
lastMove = move;
}
/**
* 滚动到指定位置
*
* @param position 选中项
*/
public void scroll2Index(int position) {
int moveNum = position - defSelectedIndex;//移动坐标数 每个坐标单位为itemHeight
vAction = itemHeight * moveNum / 20;//100毫秒内走完 移动单位为5毫秒
handler.sendEmptyMessage(MOVE_ACTION);
}其中T继承于
BaseEntity
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
28package com.yooking.tools.wheelview;
/**
* WheelView数据类
* Created by yooking on 2020/6/2.
* Copyright (c) 2020 yooking. All rights reserved.
*/
public class BaseEntity {
private String id;
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}回调监听
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
48private int index;//当前选中项
private OnScrollingListener onScrollingListener;
/**
* @param onScrollingListener 监听滚动事件
*/
public WheelView<T> setOnScrollingListener(OnScrollingListener onScrollingListener) {
this.onScrollingListener = onScrollingListener;
return this;
}
private OnSelectedListener<T> onSelectedListener;
/**
* @param onSelectedListener 监听选中事件
*/
public WheelView<T> setOnSelectedListener(OnSelectedListener<T> onSelectedListener) {
this.onSelectedListener = onSelectedListener;
return this;
}
public interface OnScrollingListener {
void onScrolling();
}
public interface OnSelectedListener<T> {
void onSelected(int index, T data);
}
/**
* 根据传入值获取目标项目
*
* @param index 传入的值
*/
public T getData(int index) {
if (data != null) {
return data.get(index);
}
return null;
}
/**
* 获取当前选中的项目
*/
public T getSelectedData() {
return getData(index);
}代码中调用
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
28WheelView<BaseEntity> wheelView = findViewById(R.id.wv_test);
List<BaseEntity> entityList = new ArrayList<>();
for (int i = 0; i < 20; i++) {
BaseEntity entity = new BaseEntity();
entity.setId("");
entity.setName("hhhhhh" + i);
entityList.add(entity);
}
wheelView.setData(entityList)
.setItemHeight(30)
.setShowNum(8)
.setLineColor(getResources().getColor(R.color.color_red_e24234))
.setLineWidth(1)
.setShowDivider(true)
.setDividerColor(getResources().getColor(R.color.color_line))
.setDividerWidth(1)
.setSelectedColor(getResources().getColor(R.color.text_3))
.setSelectedSize(30)
.setUnSelectedColor(getResources().getColor(R.color.text_9))
.setUnSelectedSize(28)
.change2Index(6);
wheelView.setOnScrollingListener(() -> Log.i("Test", "onScrolling: "))
.setOnSelectedListener((index, data) ->
Log.i("Test", "onSelected: " + index + "data:" + data.getName())
);
Log.i("Test", "onCreate: " + wheelView.getSelectedData().getName());