Android 萤火虫飞舞粒子效果

4,485 阅读3分钟
原文链接: ailoli.me

GitHub地址

原创文章,转载请注明出处

萤火虫飞舞粒子效果

本项目中我提供了两种方案,最终呈现的效果如下:

先奉上GitHub地址戳这里,有兴趣的同鞋star一下咯

实现原理

Android的粒子效果、粒子动画,已经有很多开源的轮子了。作为一个坚定的轮子主义者,我google了大半天,却没有找到这种类似于萤火虫飞舞的效果。只好自己来实现这种效果。


相比较普通的View,SurfaceView更加适合这种不断变化的画面,所以选择SurfaceView来实现。现在把思路再重新梳理一下:

  • 大小不同的粒子在区域内随机分布
  • 粒子做无规则运动,然后消失
粒子区域内随机分布

这个简单,我们在callBack的方法内直接循环生成一个粒子的数组即可。方位的话使用Random即可。

if (mCircles.size() == 0) {
    for (int i = 0; i < MAX_NUM; i++) {
        FloatParticleLine f = new FloatParticleLine(getF() * mMeasuredWidth, getF() * mMeasuredHeight, mMeasuredWidth, mMeasuredHeight);
        f.setRadius(mRandom.nextInt(2) + 1.2f);
        mCircles.add(f);
    }
}
private float getF() {
        float v = mRandom.nextFloat();
        if (v < 0.2f) {
            return v + 0.2f;
        } else if (v >= 0.85f) {
            return v - 0.2f;
        } else {
            return v;
        }
    }

getF()方法是限制在区域内取值,mMeasuredWidth、mMeasuredHeight为SurfaceView的宽和高。

这里的宽和高在粒子对象FloatParticleLine,内会用到。

然后我们在创建一个线程,在run()方法内做无线循环的绘制即可,为了避免无意义的绘制,可以使用Thread.sleep方法来控制帧数。

while (isRun) {
    try {
        mCanvas = mHolder.lockCanvas(null);
        if (mCanvas != null) {
            synchronized (mHolder) {
                // 清屏
                mCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);

                for (FloatParticleLine circle : mCircles) {
                    circle.drawItem(mCanvas);
                }
                // 控制帧数
                Thread.sleep(25);
            }
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (mCanvas != null) {
            mHolder.unlockCanvasAndPost(mCanvas);
        }
    }
}

isRun的变量我们会在SurfaceView内callBack的surfaceDestroyed方法中置为false

粒子做无规则运动
  • 方案一

其实看到这种粒子效果,首先应该想到的就是Canvas了。

在SurfaceView里就是通过不断地循环调用FloatParticleLine类的drawItem()方法来实现粒子的运动。我第一种方案的实现,就是每一个粒子在被创建出来的时候,就随机选择一个方向开始运动,滑过一定的轨迹之后让其消失就好了。

至于怎么选择随机方向,我这里的做法是,分别随机生成一个x和y轴上的递增或者递减的数值,然后每次在前一次绘制的基础上,x和y分别递增递减,直到运动到屏幕边缘或者是规定的运动距离满足了再消失即可。

//随机生成参数
private void setRandomParm() {
    // 2017/5/2-上午10:47 x和y的方向
    mIsAddX = mRandom.nextBoolean();
    mIsAddY = mRandom.nextBoolean();

    // 2017/5/2-上午10:47 x和y的取值
    mDisX = mRandom.nextInt(2) + 0.2f;
    mDisY = mRandom.nextInt(2) + 0.3f;

    // 2017/5/2-上午10:47 内部区域的运动最远距离
    mDistance = mRandom.nextInt((int) (0.25f * mWidth)) + (0.125f * mWidth);
}

绘制图形:

public void drawItem(Canvas canvas) {
    if (mX == mStartX) {
        mPaint.setAlpha(ALPHA_MAX);
    }
    //绘制
    canvas.drawCircle(mX += getPNValue(mIsAddX, mDisX), mY += getPNValue(mIsAddY, mDisY), mRadius, mPaint);
    //内部区域运动到一定距离消失
    if (judgeInner()) {
        float gapX = Math.abs(mX - mStartX);
        float ratio = 1 - (gapX / mDistance);
        mPaint.setAlpha((int) (255 * ratio));
        mRadius = mStartRadius * ratio;
        if (gapX >= mDistance || mY - mStartY >= mDistance) {
            resetDisXY();
            return;
        }
        return;
    }
    //外部区域运动到屏幕边缘消失
    if (judgeOutline()) {
        resetDisXY();
    }
}

private void resetDisXY() {
        setRandomParm();

        mPaint.setAlpha(0);
        mX = mStartX;
        mY = mStartY;
        mRadius = mStartRadius;
    }

judgeInner()和judgeOutline()是判断区域的方法,内部区域的点和外部区域的店消失时机不同

在透明度为0也就是粒子消失时,让粒子回到原点,再重新选择一个方向,进行下一步运动轨迹。

  • 方案二

方案二粒子做的运动是贝塞尔曲线,函数实在网上找到的一个函数。每当粒子做完一次曲线运动后,再随机生成一段新的贝塞尔曲线即可。

思路和方案一的思路都是一样的,无非就是运动的轨迹不同而已。

总结

做完之后回头再看,发现这个项目的原理其实并不难,可以说是简单了。但刚开始起步的时候真的还是比较懵的,原因就是没有思路。

所以做任何效果,思路最重要。