转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持!
开篇废话:
年前换了一个手机,SONY的Z3C。这个手机在解锁屏幕时有一个滑动动画,类似火花的粒子喷射,效果很炫。。。
于是尝试着模拟了一下,完成后效果如下图(还有很多细节没有实现):
SurfaceView:
因为surfaceview是使用的双缓冲机制,所以很适合绘制这种需要不停变换的画面。
下面我从网上copy了几条关于SurfaceView的一些特性(已经表明了出处),因为写这个Demo的一个主要目的就是熟悉了解Android的绘图机制。
SurfaceView和View最本质的区别:
摘录自http://www.cnblogs.com/lipeil/archive/2012/08/31/2666187.html
surfaceView是在一个新起的单独线程中可以重新绘制画面,而View必须在UI的主线程中更新画面。
那么在UI的主线程中更新画面 可能会引发问题,比如你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将无法响应按键,触屏等消息。
当使用surfaceView 由于是在新的线程中更新画面所以不会阻塞你的UI主线程。但这也带来了另外一个问题,就是事件同步。比如你触屏了一下,你需要surfaceView中 thread处理,一般就需要有一个event queue的设计来保存touch event,这会稍稍复杂一点,因为涉及到线程同步。
所以基于以上,根据游戏特点,一般分成两类:
1 被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。
2 主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制。
双缓冲:
摘录自http://blog.csdn.net/tobacco5648/article/details/8261749
Android中的SurfaceView在更新视图时,为了提高更新效率,加强用户体验,采用了双缓存机制。
Android的官方说明:
Note: On each pass you retrieve the Canvas from the SurfaceHolder, the previous state of the Canvas will be retained. In order to properly animate your graphics, you must re-paint the entire surface. For example, you can clear the previous state of the Canvas
by filling in a color with drawColor() or setting a background image with drawBitmap(). Otherwise, you will see traces of the drawings you previously performed.
简单理解:
在运用时可以理解为:SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布时,得到的实际上是backCanvas而不是正在显示的frontCanvas,之后你在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost(canvas)此视图,那么上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。例如,如果你已经先后两次绘制了视图A和B,那么你再调用lockCanvas()获取视图,获得的将是A而不是正在显示的B,之后你讲重绘的C视图上传,那么C将取代B作为新的frontCanvas显示在SurfaceView上,原来的B则转换为backCanvas。
Surface使用方法:
(摘录自http://www.cnblogs.com/devinzhang/archive/2012/02/03/2337559.html)
1)实现步骤
a.继承SurfaceView
b.实现SurfaceHolder.Callback接口
2)需要重写的方法
(1)public void surfaceChanged(SurfaceHolder holder,int format,int width,int height){} //在surface的大小发生改变时激发 (2)public void surfaceCreated(SurfaceHolder holder){} //在创建时激发,一般在这里调用画图的线程。 (3)public void surfaceDestroyed(SurfaceHolder holder) {} //销毁时激发,一般在这里将画图的线程停止、释放。</span>
3)SurfaceHolder
SurfaceHolder,surface的控制器,用来操纵surface。处理它的Canvas上画的效果和动画,控制表面,大小,像素等。几个需要注意的方法:
(1)、abstract void addCallback(SurfaceHolder.Callback callback); // 给SurfaceView当前的持有者一个回调对象。 (2)、abstract Canvas lockCanvas(); // 锁定画布,一般在锁定后就可以通过其返回的画布对象Canvas,在其上面画图等操作了。 (3)、abstract Canvas lockCanvas(Rect dirty); // 锁定画布的某个区域进行画图等..因为画完图后,会调用下面的unlockCanvasAndPost来改变显示内容。 // 相对部分内存要求比较高的游戏来说,可以不用重画dirty外的其它区域的像素,可以提高速度。 (4)、abstract void unlockCanvasAndPost(Canvas canvas); // 结束锁定画图,并提交改变。</span>
4)总结整个过程
继承SurfaceView并实现SurfaceHolder.Callback接口 ---->
SurfaceView.getHolder()获得SurfaceHolder对象 ---->
SurfaceHolder.addCallback(callback)添加回调函数---->
SurfaceHolder.lockCanvas()获得Canvas对象并锁定画布---->
Canvas绘画 ---->
SurfaceHolder.unlockCanvasAndPost(Canvas canvas)结束锁定画图,并提交改变,将图形显示。
代码实现:
介绍代码前先简单整理下思路:
1.在手指点击处会不停的喷射火花粒子
3.粒子喷射长度和密度不同,离触摸点越近越多,越远越少
3.粒子会从小变大,再从大到消失
4.粒子产生后会沿着某个方向随机运动
5.粒子会有淡淡的发光效果并且会变换颜色
下面来逐一进行讲解:
1.在手指点击处会不停的喷射火花粒子:要处理手指按下首先需要设置触摸点击监听,解决方法就是在自定义的surfaceview中重写onTouchEvent方法,从而获取到手指的点击位置。
知道了触摸点后就需要生成火花粒子,这时候就需要使用画笔(Paint)和画板(Canvas)来画出他们。
设置画笔方法:
private void setSparkPaint() { this.mSparkPaint = new Paint(); // 打开抗锯齿 this.mSparkPaint.setAntiAlias(true); /* * 设置画笔样式为填充 Paint.Style.STROKE:描边 Paint.Style.FILL_AND_STROKE:描边并填充 * Paint.Style.FILL:填充 */ this.mSparkPaint.setDither(true); this.mSparkPaint.setStyle(Paint.Style.FILL); // 设置外围模糊效果 this.mSparkPaint.setMaskFilter(new BlurMaskFilter(BLUR_SIZE, BlurMaskFilter.Blur.SOLID)); }
画笔设置好以后以后就用这个画笔在画布上画出这些粒子,这里为了简单都将粒子看作一个个小圆点。如下方法的作用就是在触摸点循环画出这些小圆
// 循环绘制所有火花 for (int[] n : sparks) { n = sparkManager.drawSpark(mCanvas, (int) X, (int) Y, n); }
经过一些列计算后调用canvas画圆的方法来画出粒子:
// 画花火 canvas.drawCircle(bezierPoint.x, bezierPoint.y, radius, mSparkPaint);
2.粒子喷射长度和密度不同,离触摸点越近越多,越远越少
长度,和密度这两个可以通过随即函数来解决。
mDistance = getRandom(SparkView.WIDTH / 4, mRandom.nextInt(15)) + 1;
3.粒子会从小变大,再从大到消失:
之前尝试过根据粒子存在时间改变透明度的方法,但效果不好。
所以直接调整粒子的大小来更好。但需要两个阶段:
第一阶段,粒子小圆的半径从0到最大。
第二阶段,粒子小圆的半径从最大到0。
只需要动态的计算出半径,最后将半径传递到canvas.drawCircle中就可以完成这个效果。
/** * 更新火花路径 */ private void updateSparkPath() { mCurDistance += PER_SPEED_SEC; // 前半段 if (mCurDistance < (mDistance / 2) && (mCurDistance != 0)) { radius = SPARK_RADIUS * (mCurDistance / (mDistance / 2)); } // 后半段 else if (mCurDistance > (mDistance / 2) && (mCurDistance < mDistance)) { radius = SPARK_RADIUS - SPARK_RADIUS * ((mCurDistance / (mDistance / 2)) - 1); } // 完成 else if (mCurDistance >= mDistance) { mCurDistance = 0; mDistance = 0; radius = 0; } }
4.粒子产生后会沿着某个方向随机运动
这个就简单了,相信大家肯定能想到就是——赛贝尔曲线(关于赛贝尔曲线推荐参考维基百科)。
粒子路径可以通过一条4点的赛贝尔曲线模拟,如下图:
我在上网找了一个函数可以求出赛贝尔曲线在某时间比下的点:
/** * 计算塞贝儿曲线 * * @param t 时间,范围0-1 * @param s 起始点 * @param c1 拐点1 * @param c2 拐点2 * @param e 终点 * @return 塞贝儿曲线在当前时间下的点 */ private Point CalculateBezierPoint( float t, Point s, Point c1, Point c2, Point e ) { float u = 1 - t; float tt = t * t; float uu = u * u; float uuu = uu * u; float ttt = tt * t; Point p = new Point((int) (s.x * uuu), (int) (s.y * uuu)); p.x += 3 * uu * t * c1.x; p.y += 3 * uu * t * c1.y; p.x += 3 * u * tt * c2.x; p.y += 3 * u * tt * c2.y; p.x += ttt * e.x; p.y += ttt * e.y; return p; }将计算后的点传入画圆函数中:
// 计算塞贝儿曲线的当前点 Point bezierPoint = CalculateBezierPoint(mCurDistance / mDistance, start, c1, c2, end); // 画花火 canvas.drawCircle(bezierPoint.x, bezierPoint.y, radius, mSparkPaint);
5.粒子会有淡淡的发光效果并且会变换颜色
淡淡的发光和变换颜色都是通过画笔设置的:
淡淡发光:
this.mSparkPaint.setMaskFilter(new BlurMaskFilter(BLUR_SIZE, BlurMaskFilter.Blur.SOLID));但需要关闭硬件加速:
// 关闭硬件加速 setLayerType(LAYER_TYPE_SOFTWARE, null);
设置随机变换颜色:
// 设置随机颜色 mSparkPaint.setColor(Color.argb(255, mRandom.nextInt(128) + 128, mRandom.nextInt(128) + 128, mRandom.nextInt(128) + 128));
总结:
大致逻辑已经介绍完毕,还有很多细节没有实现,并且发现有轻微的闪烁现象,但没有找到很好的解决办法(求大神指点)。。。
Github下载连接:
https://github.com/a396901990/SparkScreen/tree/master
最后推荐两篇关于SurfaceView和画图的文章:
如果通过SurfaceVIew做游戏Create a SurfaceView Game step-by-step
爱哥的Android自定义控件其实很简单系列