上一篇番外篇讲了一个炒鸡炒鸡简单的自定义ProgressBar,这一篇基于上一篇的基础扩展为SeekBar,没看过上一篇的,请先看一遍:传送门
先上效果图(2G内存的机子运行模拟器,所以有点卡):
这个效果之前不知道在哪里看到过,我也忘了。
下面进入正题:
测量大小和绘制部分沿用上一篇ProgressBar的,不清楚的请走上面的传送门。
对比上一篇的扩展:
(1)SeekBar能通过触摸改变刻度
(2)SeekBar上方添加一个显示当前刻度的浮动View(后面用FloatView表示)
(1)通过触摸改变刻度:
这个很容易实现,只要处理触摸事件,然后根据触摸坐标修改刻度并请求重绘就行了。
需要注意的一点就是要处理好临界状态,不然可能出现刻度为负值或者最大只能为99的情况。
因为这个很简单,就不再说了,代码会在等一下跟FloatView的实现一起贴出来,因为FloatView也需要触摸事件。
(2)浮动View的实现:
之前在网上看到这个效果时就在思考怎么实现。
刚开始时我想的是通过监听触摸事件,可以计算出FloatView的位置,然后在onDraw把它给画出来。
但是还没开始写我就发现问题了,就是这样的话FloatView就作为SeekBar的一部分,并且它的位置位于SeekBar的范围之外,这样的话FloatView是显示不出来的。
既然有问题,那么就思考解决的办法,我想到三个方法:
(1)动态改变SeekBar的高度,让FloatView可以显示出来。但会引发另一个问题,相邻的View会被挤压,所以该方法不可行。
(2)我记得ViewGroup有个属性可以让子view超过自身的显示范围,我可以在代码中类似这样设置:
((ViewGroup) getParent()).setClipChildren(true);
但这个方法问题更多,首先,你让你的直接父View允许你超过显示范围,但可能你的FloatView显示在你的直接父View的范围外了,这样你必须循环父View的父View,设置所有父View的clipChildren属性,这样可能会影响到页面内其它的View。所以这个方法也不建议采用,我们应该只显示FloatView并且避免在任何布局中对其它View造成影响。
(3)不在onDraw里画,用WindowManager来添加FloatView:
Bingo,就是这个了。用这个方法FloatView不属于SeekBar,甚至不属于这个页面,可以说是属于这个屏幕的,所以不会对页面内的任何View造成影响。更妙的是我们可以显示任何的View,不像在onDraw方法里能画的东西有限(其实在onDraw方法里也是可以画其它的View的,不过处理起来比较麻烦)。
其实我们常用的PopupWindow和现在很多应用都有的桌面悬浮窗都是这种方法。
好了,下面开始讲这种方法的实现,不清楚如何用WindowManager添加FloatView的参考我另一篇博文:传送门
(1)创建一个FloatView:
// 创建FloatView floatView = new TextView(getContext()); floatView.setGravity(Gravity.CENTER); floatView.setBackgroundResource(R.drawable.shape_circle_blue); floatView.setTextColor(0XFFFFFFFF); floatViewWidth = (int) dp2px(40); floatViewHeight = floatViewWidth;
(2)初始化FloatView的LayoutParams:
// FloatView添加到Window的参数初始化 floatLP = new LayoutParams(); floatLP.width = floatViewWidth; floatLP.height = floatViewHeight; floatLP.gravity = Gravity.LEFT | Gravity.TOP; floatLP.format = PixelFormat.RGBA_8888; floatLP.windowAnimations = R.style.pppanim;
这里注意看windowAnimations属性,就是为FloatView添加入场和出场的动画效果的,添加的方式跟PopupWindow是一样的(它们都是通过WindowManager添加的),可以看下动画样式:
<style name="pppanim"> <item name="android:windowEnterAnimation">@anim/ppp</item> <item name="android:windowExitAnimation">@anim/pppout</item> </style>
具体的动画可以自己随便定义,Enter代表添加时的动画,Exit代表移除。
(3)开始显示FloatView:
FloatView的显示跟触摸事件挂钩。我们应该在Down事件时往Window中添加FloatView,Move事件时更新FloatView的位置和显示的刻度,在Up事件时从Window中移除FloatView。
注意:往Window中添加两次相同的View和试图移除未添加进Window的View都会产生异常。
@Override public boolean onTouchEvent(MotionEvent event) { switch (event.getAction()) { case MotionEvent.ACTION_DOWN: // 获得状态栏高度 statusHeight = getStatusHeight(mContext); // 修改刻度 fontRectF.right = event.getX(); changeProgress(); // 修改FloatView显示文字 floatView.setText(mProgress + ""); // 修改FloatView的X和Y坐标 // X坐标=当前触摸的X-FloatView的宽度/2+该ProgressBar的左边坐标 floatLP.x = (int) event.getRawX() - floatViewWidth / 2; // Y坐标=相对屏幕触摸X坐标-前面根据屏幕密度计算出来的垂直间隔-状态栏高度-FloatView的高度 floatLP.y = (int) event.getRawY() - mFloatVerticalSpacing - statusHeight - floatViewHeight; // 将FloatView添加进Window wm.addView(floatView, floatLP); break; case MotionEvent.ACTION_MOVE: float newX = event.getX(); // 临界处理 if (newX < 0) { newX = 0; } else if (newX > mWidth) { newX = mWidth; } // 修改刻度 fontRectF.right = newX; changeProgress(); // 修改FloatView显示文字 floatView.setText(mProgress + ""); // 修改FloatView的X坐标 // 临界处理,只有在触摸在Bar范围内才去更新 if (event.getRawX() >= getLeft() && event.getRawX() <= getRight()) { floatLP.x = (int) event.getRawX() - floatViewWidth / 2; // 更新FloatView在window中的位置 wm.updateViewLayout(floatView, floatLP); } break; case MotionEvent.ACTION_CANCEL: case MotionEvent.ACTION_UP: // 从window中移除FloatView // 在2.3版本模拟器中报IllegalArgumentException,尚未查明原因 wm.removeView(floatView); break; } // 我们想处理触摸事件,所以这里要返回true,对触摸事件不清楚的,找我另一篇博文 return true; }
具体看注释吧,我写得很详细。这里比较复杂的是计算FloatView的位置和临界处理。
这里要注意getX()和getRawX()的区别:
getX()是相对于这个SeekBar的坐标,getRawX()是相对于屏幕的坐标。
所以当我们计算刻度时,应该用getX()。而计算FloatView的位置时,我们的FloatView是添加进屏幕的而不是添加到SeekBar的,应该使用getRawX(),基本上用WindowManager添加View时,大多数情况下都应该用getRawX()。
发现也没什么好解释的,不懂的话下载Demo自己再研究研究,有点基础的建议直接自己试下实现,原理也很简单。
当然这个控件还有很多地方可以优化,比如:很多属性可以写成通过外部动态设置的,像画笔颜色,FloatView的外观,FloatView的动画,FloatView的垂直间隔。还有2.3模拟器在移除FloatView时不知道为什么报错了,又找不到真机测试,有知道原因的跪求评论指点。
Demo下载