为SeekBar添加滑动跟随气泡

最近的项目需要做聊天语音消息,自然是用SeekBar实现进度条,这个倒不难,播放拖动进度等功能。但是设计师要拖动进度的同时thumb上方显示一个气泡显示秒数,效果如下

毕竟SeekBar没提供这个功能,所以首先想到的是自定义View,然后重写onTouch滑动显示气泡,气泡也属于自定义View里面,但是这样有个问题,由于气泡包在自定义View里面,所以控件高度不会是设计师要的效果,所以想到了Window。跟DialogToast类似。

首先实现气泡,原理是在需要的时候向WindowManager请求添加一个View到窗口并及时更新气泡的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams = new WindowManager.LayoutParams();
layoutParams.gravity = Gravity.START | Gravity.TOP;
layoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
layoutParams.format = PixelFormat.TRANSLUCENT;
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
if (XiaoMiUtils.isMIUI() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1){
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
}

FLAG_NOT_TOUCH_MODAL : 当前Window区域以外的单击事件传递给底层Window,不拦截,一般需要开启此标记
FLAG_NOT_FOCUSABLE : 不需要获取焦点
FLAG_SHOW_WHEN_LOCKED : 显示在锁屏上
XiaoMiUtils.isMIUI() : 由于小米对TYPE_TOAST管制比较严,在有些小米手机会显示不了

并在适当的时候调用

1
2
3
windowManager.addView(bubbleView, layoutParams);
windowManager.updateViewLayout(bubbleView, layoutParams);
windowManager.removeViewImmediate(bubbleView);

最难的部分气泡的显示其实一点也不难,在适当的时候也就是onTouch的事件处理时调用而已,这样气泡功能就实现了。

但是

哈哈,自定义View不是我想要的,这样做侵入性很强,说不定以后设计师改了个样式就麻烦了,所以就要改到SeekBar。既然这样干脆不要自定义View,我就想到了SeekBar本身提供的setOnSeekBarChangeListener,里面有三个回调

1
2
3
4
5
6
7
public interface OnSeekBarChangeListener {
void onProgressChanged(SeekBar var1, int var2, boolean var3);
void onStartTrackingTouch(SeekBar var1);
void onStopTrackingTouch(SeekBar var1);
}

简直完美符合我的思路,在onStartTrackingTouch的时候addView,在onStopTrackingTouch的时候removeViewImmediate,在滑动过程中,也就是onProgressChanged的时候updateViewLayout更新气泡位置,这样做完全不会影响到项目原有的代码,只需要注入气泡显示的代码即可。

于是有了下面的Delegate

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
public class SeekBarBubbleDelegate implements SeekBar.OnSeekBarChangeListener {
/**
* 气泡
*/
private View mBubble;
private boolean mIsDragging;
private WindowManager mWindowManager;
private WindowManager.LayoutParams mLayoutParams;
/**
* 气泡移动范围
*/
private Rect mRect;
/**
* 状态栏高度
*/
private int mStatusBarHeight;
private List<SeekBar.OnSeekBarChangeListener> mListeners;
public SeekBarBubbleDelegate(Context context, View bubble) {
mBubble = bubble;
mBubble.setVisibility(View.INVISIBLE);
mIsDragging = false;
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
mLayoutParams = new WindowManager.LayoutParams();
mLayoutParams.gravity = Gravity.START | Gravity.TOP;
mLayoutParams.width = ViewGroup.LayoutParams.WRAP_CONTENT;
mLayoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT;
mLayoutParams.format = PixelFormat.TRANSLUCENT;
mLayoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED;
if (XiaoMiUtils.isMIUI() || Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
mLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
} else {
mLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST;
}
mRect = new Rect();
mStatusBarHeight = getStatusBarHeight();
mListeners = new ArrayList<>();
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int bubbleWidth = mBubble.getWidth();
if (mIsDragging && bubbleWidth > 0) {
float x = mRect.left + ((float) mRect.width() / seekBar.getMax() * progress) - (bubbleWidth / 2);
mLayoutParams.x = (int) x;
mLayoutParams.y = mRect.top - mStatusBarHeight - mBubble.getHeight();
//更新气泡位置
mWindowManager.updateViewLayout(mBubble, mLayoutParams);
mBubble.setVisibility(View.VISIBLE);
}
for (SeekBar.OnSeekBarChangeListener listener : mListeners) {
listener.onProgressChanged(seekBar, progress, fromUser);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
mIsDragging = true;
//获取整个SeekBar在屏幕的位置
seekBar.getGlobalVisibleRect(mRect);
//重复赋值left right为气泡移动范围
int offset = seekBar.getThumb().getIntrinsicWidth() / 2 - seekBar.getThumbOffset();
mRect.left = mRect.left + seekBar.getPaddingLeft() + offset;
mRect.right = mRect.right - seekBar.getPaddingRight() - offset;
//将气泡加入window
mWindowManager.addView(mBubble, mLayoutParams);
for (SeekBar.OnSeekBarChangeListener listener : mListeners) {
listener.onStartTrackingTouch(seekBar);
}
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
mIsDragging = false;
removeBubble();
for (SeekBar.OnSeekBarChangeListener listener : mListeners) {
listener.onStopTrackingTouch(seekBar);
}
}
public View getBubble() {
return mBubble;
}
public boolean isDragging() {
return mIsDragging;
}
private int getStatusBarHeight() {
int height = 0;
try {
Resources resources = Resources.getSystem();
height = resources.getDimensionPixelSize(resources.getIdentifier("status_bar_height", "dimen", "android"));
} catch (Exception e) {
e.printStackTrace();
}
return height;
}
public void addOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener l) {
mListeners.add(l);
}
public void removeOnSeekBarChangeListener(SeekBar.OnSeekBarChangeListener l) {
mListeners.remove(l);
}
public void clearOnSeekBarChangeListener() {
mListeners.clear();
}
public void removeBubble() {
try {
mWindowManager.removeViewImmediate(mBubble);
} catch (Exception e) {
//do nothing
}
}
}

onProgressChanged下拿到progress进度去计算从而更新气泡位置

使用方法极其简单,给SeekBar设置监听并交给delegate管理即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SeekBarBubbleDelegate delegate = new SeekBarBubbleDelegate(context, bubbleView);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
delegate.onProgressChanged(seekBar, progress, fromUser);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
delegate.onStartTrackingTouch(seekBar);
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
delegate.onStopTrackingTouch(seekBar);
}
});

项目地址

https://github.com/izyhang/SeekBarBubble