首页 > 代码库 > Android好奇宝宝_番外篇_看脸的世界_05

Android好奇宝宝_番外篇_看脸的世界_05

上一篇番外篇讲了一个炒鸡炒鸡简单的自定义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下载

Android好奇宝宝_番外篇_看脸的世界_05