为SeekBar添加滑动跟随气泡
August 09, 2017
最近的项目需要做聊天语音消息,自然是用SeekBar
实现进度条,这个倒不难,播放拖动进度等功能。但是设计师要拖动进度的同时thumb
上方显示一个气泡显示秒数,效果如下
毕竟SeekBar
没提供这个功能,所以首先想到的是自定义View
,然后重写onTouch
滑动显示气泡,气泡也属于自定义View
里面,但是这样有个问题,由于气泡包在自定义View
里面,所以控件高度不会是设计师要的效果,所以想到了Window
。跟Dialog
、Toast
类似。
首先实现气泡,原理是在需要的时候向WindowManager
请求添加一个View
到窗口并及时更新气泡的位置
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 管制比较严,在有些小米手机会显示不了
并在适当的时候调用
windowManager.addView(bubbleView, layoutParams);
windowManager.updateViewLayout(bubbleView, layoutParams);
windowManager.removeViewImmediate(bubbleView);
最难的部分气泡的显示其实一点也不难,在适当的时候也就是onTouch
的事件处理时调用而已,这样气泡功能就实现了。
但是
哈哈,自定义View
不是我想要的,这样做侵入性很强,说不定以后设计师改了个样式就麻烦了,所以就要改到SeekBar
。既然这样干脆不要自定义View
,我就想到了SeekBar
本身提供的setOnSeekBarChangeListener
,里面有三个回调
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
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
管理即可
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);
}
});
项目地址
— Evil Mouth