首页 Android 正文
  • 本文约3905字,阅读需20分钟
  • 65
  • 0

Android 烟花效果

摘要

一、前言 本篇和《Android 粒子喷泉效果》一样,通过 Canvas 2D 坐标系实现粒子效果。上一篇我们着重讲了粒子效果的三个要素:起始点、矢量速度、符合运动学方程。当然有人会疑惑,终点不重要么 ?实际上大部分情况下,只有在防止跑出边界的情况下才会计算终点,本篇也会计算终点,防止跑出边界。 效果预览 无中心版本 有中心版本 二、实现 2.1 均匀分布 ...

一、前言

本篇和《Android 粒子喷泉效果》一样,通过 Canvas 2D 坐标系实现粒子效果。上一篇我们着重讲了粒子效果的三个要素:起始点、矢量速度、符合运动学方程。当然有人会疑惑,终点不重要么 ?实际上大部分情况下,只有在防止跑出边界的情况下才会计算终点,本篇也会计算终点,防止跑出边界。

效果预览

Android 烟花效果

无中心版本
Android 烟花效果

有中心版本
Android 烟花效果

二、实现

2.1 均匀分布

我们首要解决的问题是计算出粒子运动方向,保证粒子能正常扩散到目标范围和区域,另外还有保证粒子尽可能随机和均匀分布在任意方向。

Android 烟花效果

方法是:

粒子扩散的范围是一个圆的范围内,我们要尽可能利用圆的旋转半径和夹角之间的关系,属于高中数学知识。另外也要控制粒子的数量,防止堆叠过多的问题。

int t = i % 12;  
double degree = random.nextFloat() * 30 + t * 30;  // 12 等分圆,没等分都保证产生粒子 
// 360 /12 = 30 ,意味着每等分 30 度区域内需要产生一定的粒子

2.2 速度计算

我们上一篇说过,计算出速度是最难的,要结合场景,这里我们采样计算终点的方式,目的有 2 个,限制粒子运动出大圆,限制时间。

float minRadius = maxRadius * 1f / 2f;
double radians = Math.toRadians(degree);
int radius = (int) (random.nextFloat() * maxRadius / 2f);
float x = (float) (Math.cos(radians) * (radius + minRadius));
float y = (float) (Math.sin(radians) * (radius + minRadius));
float speedX = (x - 0) / dt;
float speedY = (y - 0) / dt;

2.3 颜色

颜色选择自己喜欢的就可以,我喜欢五彩缤纷,所以随机生成

int color = argb(random.nextFloat(), random.nextFloat(), random.nextFloat());

2.4 定义粒子对象

static class Star {
        private final boolean fromCenter;
        private final int color;
        private double radians;
        private float r;
        float speedX;
        float speedY;
        long startTime;
        Path path = new Path();
        int type = TYPE_QUAD;

        public Star(float speedX, float speedY, long clockTime, float r, double radians, int color, boolean fromCenter, int type) {
            this.speedX = speedX;
            this.speedY = speedY;
            this.startTime = clockTime;
            this.r = r;
            this.radians = radians;
            this.fromCenter = fromCenter;
            this.color = color;
            this.type = type;
        }
        public void draw(Canvas canvas,Paint paint,long clockTime){

      }
}

2.4 基础骨架

 public void drawBase(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);

            paint.setColor(color);

            if (currentRadius > 0) {
                double asin = Math.asin(r / currentRadius);
                //利用反三角函数计算出切线与圆的夹角
                int t = 1;
                for (int i = 0; i < 2; i++) {
                    double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);  //切线长度
                    float ax = (float) (aspectRadius * Math.cos(radians + asin * t));
                    float ay = (float) (aspectRadius * Math.sin(radians + asin * t));
                    if (fromCenter) {
                        canvas.drawLine(0, 0, ax, ay, paint);
                    } else {
                        canvas.drawLine(dx / 3, dy / 3, ax, ay, paint);
                    }
                    t = -1;
                }

            }
            canvas.drawCircle(dx, dy, r, paint);
        }

Android 烟花效果

2.4 进一步优化

public void drawCircleLineUsePath(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            path.reset();

            if (currentRadius > 0) {

                if (fromCenter) {
                    path.moveTo(0, 0);
                } else {
                    path.moveTo(dx / 3, dy / 3);
                }

                //1、利用反三角函数计算出小圆切线与所有小圆原点与(0,0)点的夹角
                double asin = Math.asin(r / currentRadius);

                //2、计算出切线长度
                double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);

                float axLeft = (float) (aspectRadius * Math.cos(radians - asin));
                float ayLeft = (float) (aspectRadius * Math.sin(radians - asin));
                path.lineTo(axLeft, ayLeft);

                float axRight = (float) (aspectRadius * Math.cos(radians + asin));
                float ayRight = (float) (aspectRadius * Math.sin(radians + asin));
                path.lineTo(axRight, ayRight);
                path.addCircle(dx, dy, r, Path.Direction.CCW);

            }
            path.close();
            paint.setColor(color);
            canvas.drawPath(path, paint);
        }

Android 烟花效果

有点样子了,但是问题是,Path 动画并没有和粒子圆点闭合,这样就会有问题,后续如果要使用 Shader 着色 (为啥要用 Shader 着色,主要是火焰效果很难画出来,还得借助一些其他工具),必然产生不均匀问题。为了实现开头的效果,最初是计算切线和小圆的夹角让 Path 闭合,但是计算量和难度太大了,直接使用贝塞尔曲线更省事。

public void drawFillZoneUsePath(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            path.reset();

            if (currentRadius > 0) {
                if (fromCenter) {
                    path.moveTo(0, 0);
                } else {
                    path.moveTo(dx / 3, dy / 3);
                }

                //1、利用反三角函数计算出小圆切线与所有小圆原点与(0,0)点的夹角
                double asin = Math.asin(r / currentRadius);

                //2、计算出切线长度
                double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);

                float axLeft = (float) (aspectRadius * Math.cos(radians - asin));
                float ayLeft = (float) (aspectRadius * Math.sin(radians - asin));
                path.lineTo(axLeft, ayLeft);

                float axRight = (float) (aspectRadius * Math.cos(radians + asin));
                float ayRight = (float) (aspectRadius * Math.sin(radians + asin));

                float cx = (float) (Math.cos(radians) * (currentRadius + 2 * r));
                float cy = (float) (Math.sin(radians) * (currentRadius + 2 * r));
                //如果使用三角函数计算切线可能很复杂,这里使用贝塞尔曲线简化逻辑
                path.quadTo(cx, cy, axRight, ayRight);
                path.lineTo(axRight, ayRight);

            }
            path.close();
            paint.setColor(color);
            canvas.drawPath(path, paint);
        }

2.6 性能优化

由于 Path 是路径转换公式,而不是坐标定点,因此,随着粒子数量的增多,其绘制性能会显著下降。

Android 烟花效果

我们要达到两个目的:

有具备封闭区域:有封闭区域,一是为了好看,二是为了可以使用线性 shader 着色。如果不需要封闭区域,当然也可使用图片,做好旋转矩阵和平移矩阵的运算即可。

提升性能:显然 Path 是不行的,因此我们可以绘制矩形、arc 闭合弧形 (可利用 Canvas Matrix 旋转减少计算难度)、或者 Line 即可,这里为了简单,我们使用 drawLine 方式。

 public void drawFillZone(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;
            float dx = 0 + speedX * costTime;
            float dy = 0 + speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            double tail = currentRadius - 20;
            double cosM = Math.cos(radians);
            double sinM = Math.sin(radians);

            float cx;
            float cy;

            if (fromCenter || tail < 0) {
                cx = 0f;
                cy = 0f;
            } else {
                cx = (float) (tail * cosM);
                cy = (float) (tail * sinM);
            }

            paint.setColor(color);
            paint.setStrokeWidth(r);
            canvas.drawLine(cx,cy,dx,dy,paint);
        }

效果如下:

Android 烟花效果

三、全部代码

最终版本我们微调了代码,使得效果更加逼真一些。

public class FireworksView extends View {

    private static final long V_SYNC_TIME = 16;
    private final DisplayMetrics mDM;
    private TextPaint mArcPaint;
    private long displayTime = 500L;
    private long clockTime = 0;
    private TextPaint mDrawerPaint = null;
    private Random random;

    final int maxStartNum = 200;
    Star[] stars = new Star[maxStartNum];
    private boolean isRefresh = true;

    public static final int TYPE_BASE = 1;
    public static final int TYPE_FILL_ARC = 2;
    public static final int TYPE_RECT = 3;
    public static final int TYPE_CIRCLE_LINE = 4;
    public static final int TYPE_FILL_ZONE_ON_PATH = 5;
    public static final int TYPE_FILL_LINE = 6;

    public FireworksView(Context context) {
        this(context, null);
    }

    public FireworksView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public FireworksView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        mDM = getResources().getDisplayMetrics();
        initPaint();

        setOnClickListener(new OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
            @Override
            public void onClick(View v) {
                startPlay();
            }
        });
    }

    public static int argb(float red, float green, float blue) {
        return ((int) (1 * 255.0f + 0.5f) << 24) |
                ((int) (red * 255.0f + 0.5f) << 16) |
                ((int) (green * 255.0f + 0.5f) << 8) |
                (int) (blue * 255.0f + 0.5f);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int widthSize = MeasureSpec.getSize(widthMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            widthSize = mDM.widthPixels / 2;
        }

        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        if (heightMode != MeasureSpec.EXACTLY) {
            heightSize = widthSize / 2;
        }
        random = new Random(SystemClock.uptimeMillis());

        setMeasuredDimension(widthSize, heightSize);
    }

    public float dp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, mDM);
    }

    public float sp2px(float dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, dp, mDM);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        int width = getWidth();
        int height = getHeight();
        if (width <= 10 || height <= 10) {
            return;
        }
        int saveCount = canvas.save();

        int maxRadius = Math.min(width, height) / 2;
        canvas.translate(width / 2, height / 2);

        long clockTime = getClockTime();
        if (isRefresh) {
            finalfloat dt = 1000;
            finalfloat r = 4;

            for (int i = 0; i < maxStartNum; i++) {

                int t = i % 12;
                double degree = random.nextFloat() * 30 + t * 30;  // 12 等分圆

                float minRadius = maxRadius * 1f / 4f;

                double radians = Math.toRadians(degree);
                int radius = (int) (random.nextFloat() * maxRadius *3f/ 4f);

                float x = (float) (Math.cos(radians) * (radius + minRadius));
                float y = (float) (Math.sin(radians) * (radius + minRadius));

                float speedX = (x - 0) / dt;
                float speedY = (y - 0) / dt;

                int color = argb(random.nextFloat(), random.nextFloat(), random.nextFloat());
                stars[i] = new Star(speedX, speedY, clockTime, r, radians, color, false, TYPE_FILL_ARC);
            }
            isRefresh = false;
        }

        for (int i = 0; i < maxStartNum; i++) {
            Star star = stars[i];
            star.draw(canvas, mDrawerPaint, clockTime);
        }
        canvas.restoreToCount(saveCount);
    }

    private long getClockTime() {
        return clockTime;
    }

    ValueAnimator animator;

    @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB)
    public void startPlay() {
        clockTime = 0;
        isRefresh = true;
        postInvalidate();
        if(animator != null){
            animator.cancel();
        }
        ValueAnimator valueAnimator = ValueAnimator.ofFloat(0, 1);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(@NonNull ValueAnimator animation) {
                clockTime =  animation.getCurrentPlayTime();
                postInvalidate();
            }
        });
        valueAnimator.setDuration(displayTime);
        animator = valueAnimator;
        valueAnimator.start();
    }

    private void initPaint() {
        // 实例化画笔并打开抗锯齿
        mArcPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mArcPaint.setAntiAlias(true);
        mArcPaint.setStyle(Paint.Style.STROKE);
        mArcPaint.setStrokeCap(Paint.Cap.ROUND);

        mDrawerPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mDrawerPaint.setAntiAlias(true);
        mDrawerPaint.setStyle(Paint.Style.FILL);
        mDrawerPaint.setStrokeCap(Paint.Cap.ROUND);

    }

    static class Star {
        private final boolean fromCenter;
        private final int color;
        private double radians;
        private float r;
        float speedX;
        float speedY;
        long startTime;
        int type = TYPE_FILL_ARC;
        private Path path = new Path();

        public Star(float speedX, float speedY, long clockTime, float r, double radians, int color, boolean fromCenter, int type) {
            this.speedX = speedX;
            this.speedY = speedY;
            this.startTime = clockTime;
            this.r = r;
            this.radians = radians;
            this.fromCenter = fromCenter;
            this.color = color;
            this.type = type;
        }

        public void draw(Canvas canvas, Paint paint, long clockTime) {

            switch (type) {
                case TYPE_BASE:
                    drawBase(canvas, paint, clockTime);
                    break;
                case TYPE_RECT:
                    drawRect(canvas, paint, clockTime);
                    break;
                case TYPE_CIRCLE_LINE:
                    drawCircleLine(canvas, paint, clockTime);
                    break;
                case TYPE_FILL_LINE:
                    drawFillZone(canvas, paint, clockTime);
                    break;
                case TYPE_FILL_ARC:
                    drawArcFillZone(canvas, paint, clockTime);
                    break;
                case TYPE_FILL_ZONE_ON_PATH:
                    drawFillZoneUsePath(canvas, paint, clockTime);
                    break;
            }

        }

        public void drawFillZone(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;
            float dx = 0 + speedX * costTime;
            float dy = 0 + speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            double tail = currentRadius - 20;
            double cosM = Math.cos(radians);
            double sinM = Math.sin(radians);

            float cx;
            float cy;

            if (fromCenter || tail < 0) {
                cx = 0f;
                cy = 0f;
            } else {
                cx = (float) (tail * cosM);
                cy = (float) (tail * sinM);
            }

            paint.setColor(color);
            paint.setStrokeWidth(r);
            canvas.drawLine(cx,cy,dx,dy,paint);
        }

        public void drawArcFillZone(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;
            if(costTime == 0){
                return;
            }
            float dx = 0 + speedX * costTime;
            float dy = 0 + speedY * costTime;

            float currentRadius = (float) Math.sqrt(dx * dx + dy * dy);

            paint.setColor(color);

            int save = canvas.save();

            float degrees = (float) Math.toDegrees(radians);
            canvas.rotate(degrees);
           // canvas.drawRect(new RectF(currentRadius - 20,-r/2,currentRadius,r/2),paint);
           canvas.drawArc(new RectF(currentRadius - 50,-2*r,currentRadius,2*r),-15,30,true,paint);
           canvas.restoreToCount(save);

        }

        public void drawFillZoneUsePath(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            path.reset();

            if (currentRadius > 0) {
                if (fromCenter) {
                    path.moveTo(0, 0);
                } else {
                    path.moveTo(dx / 3, dy / 3);
                }

                //1、利用反三角函数计算出小圆切线与所有小圆原点与(0,0)点的夹角
                double asin = Math.asin(r / currentRadius);

                //2、计算出切线长度
                double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);

                float axLeft = (float) (aspectRadius * Math.cos(radians - asin));
                float ayLeft = (float) (aspectRadius * Math.sin(radians - asin));
                path.lineTo(axLeft, ayLeft);

                float axRight = (float) (aspectRadius * Math.cos(radians + asin));
                float ayRight = (float) (aspectRadius * Math.sin(radians + asin));

                float cx = (float) (Math.cos(radians) * (currentRadius + 2 * r));
                float cy = (float) (Math.sin(radians) * (currentRadius + 2 * r));
                //如果使用三角函数计算切线可能很复杂,这里使用贝塞尔曲线简化逻辑
                path.quadTo(cx, cy, axRight, ayRight);
                path.lineTo(axRight, ayRight);

            }
            path.close();
            paint.setColor(color);
            canvas.drawPath(path, paint);
        }

        public void drawCircleLine(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            if(costTime == 0){
                return;
            }

            float dx = speedX * costTime;
            float dy = speedY * costTime;
            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            paint.setColor(color);
            if (currentRadius > 0) {
                float cx;
                float cy;
                if (fromCenter) {
                    cx = 0;
                    cy = 0;
                } else {
                    cx = dx / 3;
                    cy = dy / 3;
                }

                //1、利用反三角函数计算出小圆切线与所有小圆原点与(0,0)点的夹角
                double asin = Math.asin(r / currentRadius);

                //2、计算出切线长度
                double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);
                //左侧坐标
                float axLeft = (float) (aspectRadius * Math.cos(radians - asin));
                float ayLeft = (float) (aspectRadius * Math.sin(radians - asin));
                canvas.drawLine(cx,cy,axLeft,ayLeft,paint);
                float axRight = (float) (aspectRadius * Math.cos(radians + asin));
                float ayRight = (float) (aspectRadius * Math.sin(radians + asin));
                canvas.drawLine(cx,cy,axRight,ayRight,paint);

            }
            canvas.drawCircle(dx,dy,r,paint);
        }

        public void drawBase(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);

            paint.setColor(color);

            if (currentRadius > 0) {
                double asin = Math.asin(r / currentRadius);
                //利用反三角函数计算出切线与圆的夹角
                int t = 1;
                for (int i = 0; i < 2; i++) {
                    double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);  //切线长度
                    float ax = (float) (aspectRadius * Math.cos(radians + asin * t));
                    float ay = (float) (aspectRadius * Math.sin(radians + asin * t));
                    if (fromCenter) {
                        canvas.drawLine(0, 0, ax, ay, paint);
                    } else {
                        canvas.drawLine(dx / 3, dy / 3, ax, ay, paint);
                    }
                    t = -1;
                }

            }
            canvas.drawCircle(dx, dy, r, paint);
        }

        //使用 Path 如果粒子多的化性能会极速下降
        public void drawCircleLineUsePath(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            double currentRadius = Math.sqrt(dx * dx + dy * dy);
            path.reset();

            if (currentRadius > 0) {

                if (fromCenter) {
                    path.moveTo(0, 0);
                } else {
                    path.moveTo(dx / 3, dy / 3);
                }

                //1、利用反三角函数计算出小圆切线与所有小圆原点与(0,0)点的夹角
                double asin = Math.asin(r / currentRadius);

                //2、计算出切线长度
                double aspectRadius = Math.abs(Math.cos(asin) * currentRadius);

                float axLeft = (float) (aspectRadius * Math.cos(radians - asin));
                float ayLeft = (float) (aspectRadius * Math.sin(radians - asin));
                path.lineTo(axLeft, ayLeft);

                float axRight = (float) (aspectRadius * Math.cos(radians + asin));
                float ayRight = (float) (aspectRadius * Math.sin(radians + asin));
                path.lineTo(axRight, ayRight);
                path.addCircle(dx, dy, r, Path.Direction.CCW);

            }
            path.close();
            paint.setColor(color);
            canvas.drawPath(path, paint);
        }

        public void drawRect(Canvas canvas, Paint paint, long clockTime) {
            long costTime = clockTime - startTime;

            float dx = speedX * costTime;
            float dy = speedY * costTime;

            paint.setColor(color);
            RectF rectF = new RectF(dx - r, dy - r, dx + r, dy + r);
            canvas.drawRect(rectF, paint);
         //   canvas.drawCircle(dx,dy,r,paint);

        }
    }

}

四、总结

本篇我们大量使用了三角函数、反三角函数,因此一定要掌握好数学基础。


扫描二维码,在手机上阅读


    评论