ANDROID模拟火花粒子的滑动喷溅效果

ANDROID模拟火花粒子的滑动喷射效果

转载请注明本文出自大苞米的博客(http://blog.csdn.net/a396901990),谢谢支持!


开篇废话:


年前换了一个手机,SONY的Z3C。这个手机在解锁屏幕时有一个滑动动画,类似火花的粒子喷射,效果很炫。。。

于是尝试着模拟了一下,完成后效果如下图(还有很多细节没有实现):

ANDROID模拟火花粒子的滑动喷溅效果  ANDROID模拟火花粒子的滑动喷溅效果



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点的赛贝尔曲线模拟,如下图:

ANDROID模拟火花粒子的滑动喷溅效果


我在上网找了一个函数可以求出赛贝尔曲线在某时间比下的点:

    /**
     * 计算塞贝儿曲线
     * 
     * @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自定义控件其实很简单系列


2楼txfyteen前天 20:06
想问下你第4点赛贝尔曲线那个图是用什么软件画出来的
Re: a396901990前天 20:51
回复txfyteenn*中复制过来的。。。哈哈
Re: txfyteen昨天 13:40
回复a396901990n好吧,谢谢博主了,辛苦
1楼sjyhehe3天前 09:34
请问那个第一个动图怎么做的?
Re: a396901990前天 14:27
回复sjyhehengif录制工具,百度一搜一堆