Android 龙年烈焰燃烧红红火火

前言

时间过的很快,不知不觉大家年龄又要长一岁了,也就是这几年,很多人才意识到自己的渺小,无形的手和有形手不断提醒你和指导你,做人应该逆来顺受,犹如进入了一个崭新的时代,而我们仅仅是时代中的一粒尘埃,风一吹就会到处乱漂。经济数据比想象的差,裁员潮一波接着一波,股市也是跌跌不休,因此大家都在卷,并且很多人思想上保守了,生活上也保守了,也变得不听"话"了。

但是,我们不能因为过去的不快而影响新的一年,我们反思过去的问题,重新制定新年计划,重新启航吧。客观因素上大家都面临不确定性,选择保守是本能使然,但并不意味着沉沦甚至躺平。我个人观点,只要还活着,就应该力所能及的让寒冬来得晚一些,走得早一些。

下面是本篇的效果实现,期待新的一年大家的工作生活红红火火。

火焰,象征一种祝福,希望大家在今年取得新的成就和收益。

预览效果

本篇主要是火焰效果实现,至于效果图中的龙是gif图片。途中的🔥看起来有些差,主要原因是GIF录制不够饱和。

火焰原理

实际上火焰的实现是通过粒子叠加效果实现的。

也就是说,本篇会用到Xfermode对图像进行叠加,但是我们知道,叠加是有一定要求的,比如颜色值不能相同的,因此,我们对火焰的定义需要尽可能让颜色相似火焰,但同时也要有一定的随机性,当然,这里还有个问题,纯色叠加容易有明显的切割感,比如我们在 《Android 粒子喷泉动效》中使用的LIGHTEN 叠加,我们需要的是融合效果,因此,我们还需要使用RadialGradient来增强融合效果。

现有方案

在前段时间,掘金上也有大佬实现火焰动效的效果,但是存在一些问题,就是没有使用RadialGradient + Blend,而是使用了Blur,这就导致火焰比较模糊且融合的不够好,其次Blur往往是修改像素实现,因此要尽可能让图片小,否则也有一定的性能问题。

我们先来看看普通的叠加效果

下面,我们使用了圆来构成粒子,其实火焰如果使用椭圆或许会更好,不过android Canvas 椭圆绘制还是比较费事的,同一家公司设计的产品,html5的Canvas api总是比Android的Canvas api好用的多。

下面是LIGHTEN 模式的叠加效果

BlendMode模式

在Android 9开始,Paint开始支持BLEND模式了,但是为了兼容低版本,使用Xfermode去兼容,当然有些效果是不支持的,兼容的API是andriodx中提供的PaintCompat

java 复制代码
public static boolean setBlendMode(@NonNull Paint paint, @Nullable BlendModeCompat blendMode) {
    if (Build.VERSION.SDK_INT >= 29) {
        Object blendModePlatform = blendMode != null
                ? BlendModeUtils.Api29Impl.obtainBlendModeFromCompat(blendMode) : null;
        Api29Impl.setBlendMode(paint, blendModePlatform);
        // All blend modes supported in Q
        return true;
    } else if (blendMode != null) {
        PorterDuff.Mode mode = obtainPorterDuffFromCompat(blendMode);
        paint.setXfermode(mode != null ? new PorterDuffXfermode(mode) : null);
        // If the BlendMode has an equivalent PorterDuff mode, return true,
        // otherwise return false
        return mode != null;
    } else {
        // Configuration of a null BlendMode falls back to the default which is supported in
        // all platform levels
        paint.setXfermode(null);
        return true;
    }
}

不过目前最好用的是PLUS和LIGHTEN效果,前者叠加后亮度更高,后者稍差一些。

关于粒子动效

粒子运动比较重要的是三个方面

  • 起始点
  • 矢量速度
  • 符合运动学方程

在绘制粒子时,一定要使用容器管理粒子,我们前面一些文章中实际上定义过粒子,粒子需要被管理起来,因此,使用数据结构是必须的。

火焰实现

我们前面说过,火焰是粒子实现,因此我们首先应该实现粒子对象,一般情况下,Canvas是2D坐标系,因此我们只需要管理好x,y坐标即可。

定义粒子对象

下面我们来定义一个FireParticle来描述粒子,当然,有2个时长,第一个比较好理解,而remaining_life 的作用是为了计算透明度。

另外,我们会实现reset方法,重置粒子状态,方便粒子复用。

java 复制代码
static class FireParticle {

    private final float initX;  //初始值x
    private final float initY; //初始值y
    public float opacity;  //透明度
    private PointF location; // 当前位置
    private int r;  // red 色值
    private int g;  // green 色值
    private int b;  // blue 色值
    private float radius;  // 粒子半径
    private float life;  // 粒子生存时长
    private float remaining_life; //粒子剩余生存时长
    private PointF speed;  //x,y方向的矢量速度

    FireParticle(float x,float y) {
        this.initX = x;
        this.initY = y;
        init();
    }

    public void reset() {
        init();  
    }

    private void init() {
        //计算X,Y、轴速度
        this.speed = new PointF(-4.5f + (float) (Math.random() * 8f), -20f + (float) (Math.random() * 10f));
        //初始位置
        this.location = new PointF(initX, initY);
        //随机半径
        this.radius = (float) (30 + Math.random() * 55);
        //duration - 生存时间
        this.life = (float) (20 + Math.random() * 20);
        //剩余生存时间
        this.remaining_life = this.life;
        //colors
        this.r = 255;
        this.g = 0;
        this.b = 0;
    }
}

融合

下面我们来简单看下纯色效果

是不是已经很像火焰了,但是更像在水中"吐泡泡",为了让火焰更加逼真,我们调整rgb的色值,让其趋近火焰效果,红绿蓝中,红绿用来叠加黄色效果,这里我们将色值调整如下。

颜色修改

java 复制代码
this.r = 200 + (int) Math.round(Math.random() * 55);
this.g = (int) Math.round(Math.random() * 155);
this.b = (int) Math.round(Math.random() * 100);

为什么需要随机呢,我们前面说过,不同的颜色才能在Xformode合成时进行叠加。

设置BlendMode

为了让颜色能叠加,我们使用PaintCompat

java 复制代码
PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS);

图像边缘融合

我们前面提到过,纯色融合边缘会很明显出现"交集",因此,我们不是用纯色,而是使用Shader中的RadialGrendient

java 复制代码
int[] colors = new int[3];

float[] colorStops = new float[]{
        0,
        0.5f,
        1f
};

colors[0] = argb((int) (p.opacity * 255f), p.r, p.g, p.b);
colors[1] = argb((int) (p.opacity * 255f), p.r, p.g, p.b);
colors[2] = argb(0, p.r, p.g, p.b);

RadialGradient radialGradient = new RadialGradient(p.location.x, p.location.y, p.radius, colors, colorStops, Shader.TileMode.CLAMP);
paint.setShader(radialGradient);
//   mPaint.setAlpha((int) (p.opacity * 255));
//   mPaint.setColor(Color.RED);
canvas.drawCircle(p.location.x, p.location.y, p.radius, paint);

接下来我们看看效果

是不是好看多了

火焰范围控制

上面我们只是实现了单个火焰,而我们想要的是一大团火焰效果,能不能控制火焰的面积呢?

大范围控制火焰也是可以的,不过,如果想让火焰效果更具可扩展性,所以变更火焰位置,是不是更好呢?当然,本篇就是这么写的。

定义粒子发射器

下面是单个粒子绘制的完整逻辑,我们通过改变初始位置来调整起始点,这样我们就能决定粒子的范围了

java 复制代码
static class FireParticleShooter {
    public void init(float x, float y) {
        if (particles.isEmpty()) {
            for (int i = 0; i < count; i++) {
                particles.add(new FireParticle(x,y));
            }
        }
    }

  // 省略绘制逻辑
}

以上是粒子控制的核心逻辑

View实现

下面我们来实现View承载火焰效果,当然Drawable也是可以的,这里我们通过一些手段,控制绘制逻辑。

java 复制代码
FireParticleShooter[] fireParticleShooters = null; //发射器管理容器
private int fireNum = -1;  // 火焰数量, -1 表示平铺整个宽度
private boolean isStart = true;  //是否开始绘制
final float fireCircleRange = 100; //单个火焰范围

那么怎么初始化呢

其实很简单,我们通过数量来控制位置,计算第一把火的位置,公式如下 float startX = - fireCircleRange * NUM /2 + fireCircleRange/2;

那么,其他火把的位置就是

(startX + fireCircleRange * i) 下面是火焰发射器初始化和调度逻辑

java 复制代码
int NUM = fireNum; // 火焰数量
if(NUM < 0){
    NUM = (int) (width / fireCircleRange) ; // 负数表示平铺
}
if(fireParticleShooters == null){
    fireParticleShooters = new FireParticleShooter[NUM];
}
//第一把🔥的位置
float startX =  - fireCircleRange * NUM /2 + fireCircleRange/2;
for (int i = 0; i < NUM; i++) {
    FireParticleShooter fireParticleShooter = fireParticleShooters[i];
    if(fireParticleShooter == null){
        fireParticleShooter = new FireParticleShooter();
        fireParticleShooters[i] = fireParticleShooter;
        //初始化不位置
        fireParticleShooter.init((int) (startX + fireCircleRange * i), 0);
    }
    //绘制
    fireParticleShooter.drawAndUpdate(canvas, mPaint);
}

我们现在的效果就完成了

画龙

我们这里的龙使用gif,毕竟涉及复杂的解码操作,因此就从简了,资源如下。

这里我们使用Glide + ImageView实现Gif图片的展示,因为Glide又一个强大的Gif解码引擎。当然,Android 9提供的AnimatedDrawable应该也是可以的,但是不兼容低版本,这里就不使用了

java 复制代码
Glide.with(imageView).asGif().load(R.mipmap.pic_long).into(imageView);

效果

最终效果就是我们开头的效果了,当然,开头部分是有特效的,我们这里展示一下。

以上整个绘制流程就到此结束了

总结

本篇核心就是定义火焰特效,一直以来很多类似的效果是通过专业软件来做的,实际上专业软件的很多东西也是基于系统api实现,他们能实现的,我们理论上也可以。

附录

这里我们继续延续以前的风格,提供代码供大家参考。

火焰效果源码

下面是火焰效果源码,当然也做了很多封装逻辑

ini 复制代码
public class FireView extends View {
    private TextPaint mPaint;
    private DisplayMetrics mDM;
    public FireView(Context context) {
        this(context, null);
    }
    public FireView(Context context, AttributeSet attrs) {
        super(context, attrs);
        mDM = getResources().getDisplayMetrics();
        initPaint();
    }
    private void initPaint() {
        //否则提供给外部纹理绘制
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeCap(Paint.Cap.BUTT);
        mPaint.setStyle(Paint.Style.FILL);
        PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS);
    }
    @Override
    protected void onMeasure(int widthMeasureSpec, int 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;
        }
        setMeasuredDimension(widthSize, heightSize);

    }


    FireParticleShooter[] fireParticleShooters = null;
    private int fireNum = -1;
    private boolean isStart = true;
    final float fireCircleRange = 100; //单个火焰范围

    public void startFire(int fireNum){
        this.fireNum = fireNum;
        this.isStart = true;
        invalidate();
    }
    public void startFireFillWidth(){
        startFire(-1);
    }

    public void stopFire(){
        isStart = false;
        fireParticleShooters = null;
    }

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

        if (!isStart || fireNum == 0) {
            return;
        }
        int width = getWidth();
        int height = getHeight();

        canvas.drawColor(Color.BLACK);
        int save = canvas.save();
        canvas.translate(width / 2f, height - getPaddingBottom());

        int NUM = fireNum; // 火焰数量
        if(NUM < 0){
            NUM = (int) (width / fireCircleRange) ; // 负数表示平铺
        }
        if(fireParticleShooters == null){
            fireParticleShooters = new FireParticleShooter[NUM];
        }
        //第一把🔥的位置
        float startX =  - fireCircleRange * NUM /2 + fireCircleRange/2;
        for (int i = 0; i < NUM; i++) {
            FireParticleShooter fireParticleShooter = fireParticleShooters[i];
            if(fireParticleShooter == null){
                fireParticleShooter = new FireParticleShooter();
                fireParticleShooters[i] = fireParticleShooter;
                //初始化不位置
                fireParticleShooter.init((int) (startX + fireCircleRange * i), 0);
            }
            //绘制
            fireParticleShooter.drawAndUpdate(canvas, mPaint);
        }
        canvas.restoreToCount(save);
        postInvalidate();

    }

    public static int argb(
            @IntRange(from = 0, to = 255) int alpha,
            @IntRange(from = 0, to = 255) int red,
            @IntRange(from = 0, to = 255) int green,
            @IntRange(from = 0, to = 255) int blue) {
        return (alpha << 24) | (red << 16) | (green << 8) | blue;
    }

    static class FireParticleShooter {
        int count = 50; //粒子数量
        List<FireParticle> particles = new ArrayList<>(); //粒子管理
        int[] colors = new int[3];  //radial gradient范围
        float[] colorStops = new float[]{
                0,
                0.5f,
                1f
        };

        public void init(float x, float y) {
            if (particles.isEmpty()) {
                for (int i = 0; i < count; i++) {
                    particles.add(new FireParticle(x,y));
                }
            }
        }

        public void drawAndUpdate(Canvas canvas, Paint paint) {

            for (int i = 0; i < particles.size(); i++) {
                FireParticle p = particles.get(i);
                p.opacity = Math.round(p.remaining_life / p.life * 100) / 100f;
                //RadialGradient 火焰模糊
                if (p.radius > 0) {
                    colors[0] = argb((int) (p.opacity * 255f), p.r, p.g, p.b);
                    colors[1] = argb((int) (p.opacity * 255f), p.r, p.g, p.b);
                    colors[2] = argb(0, p.r, p.g, p.b);

                    RadialGradient radialGradient = new RadialGradient(p.location.x, p.location.y, p.radius, colors, colorStops, Shader.TileMode.CLAMP);
                    paint.setShader(radialGradient);
                    //   mPaint.setAlpha((int) (p.opacity * 255));
                    //   mPaint.setColor(Color.RED);
                    canvas.drawCircle(p.location.x, p.location.y, p.radius, paint);
                }
                p.remaining_life--;
                p.radius--;
                p.location.x += p.speed.x;
                p.location.y += p.speed.y;

                // 对不符合规则的进行重置
                if (p.remaining_life < 0 || p.radius <= 0) {
                    p.reset();
                }
            }

        }
    }

    static class FireParticle {

        private final float initX;
        private final float initY;
        public float opacity;
        private PointF location;
        private int r;
        private int g;
        private int b;
        private float radius;
        private float life;
        private float remaining_life;
        private PointF speed;

        FireParticle(float x,float y) {
            this.initX = x;
            this.initY = y;
            init();
        }

        public void reset() {
            init();
        }

        private void init() {
            //计算X,Y、轴速度
            this.speed = new PointF(-4.5f + (float) (Math.random() * 8f), -20f + (float) (Math.random() * 10f));
            //初始位置
            this.location = new PointF(initX, initY);
            //随机半径
            this.radius = (float) (30 + Math.random() * 55);
            //duration - 生存时间
            this.life = (float) (20 + Math.random() * 20);
            //剩余生存时间
            this.remaining_life = this.life;
            //colors
            this.r = 200 + (int) Math.round(Math.random() * 55);
            this.g = (int) Math.round(Math.random() * 155);
            this.b = (int) Math.round(Math.random() * 100);
        }
    }
}

双发射粒子绕环效果

本篇开头的粒子动画,实际上这种动画很多,实现起来也很简单,主要源码如下

ini 复制代码
public class CircleFireView extends View {
    private TextPaint mPaint;
    private DisplayMetrics mDM;

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

    public CircleFireView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setClickable(true);
        mDM = getResources().getDisplayMetrics();
        initPaint();
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setStrokeCap(Paint.Cap.BUTT);
        mPaint.setStyle(Paint.Style.FILL);
        PaintCompat.setBlendMode(mPaint, BlendModeCompat.LIGHTEN);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int 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;
        }
        setMeasuredDimension(widthSize, heightSize);

    }

    List<Particle> particles = new ArrayList<>();

    float angle = 0;
    float circleRadius = 20;
    float speed = 3.5f;
    final int NUM = 300;

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();

        float radius = Math.min(width, height) / 2f;

        canvas.drawColor(Color.BLACK);
        int save = canvas.save();
        canvas.translate(width / 2f, height / 2f);
        float distance = radius / 2f;

        double preRadian = Math.toRadians(angle);
        float lx = (float) (distance * Math.cos(preRadian));
        float ly = (float) (distance * Math.sin(preRadian));

        mPaint.setColor(Color.CYAN);
        canvas.drawCircle(lx , ly, circleRadius, mPaint);

        if (particles.size() < NUM) {
            particles.add(new Particle(lx, ly, circleRadius, 1, speed, speed));
        }

        double rightRadian = Math.toRadians(angle - 180);
        float rx = (float) (distance * Math.cos(rightRadian));
        float ry = (float) (distance * Math.sin(rightRadian));

        mPaint.setColor(Color.CYAN);
        canvas.drawCircle(rx , ry, circleRadius, mPaint);

        if (particles.size() < NUM) {
            particles.add(new Particle(rx, ry, circleRadius, 1, speed, speed));
        }

        Iterator<Particle> iterator = particles.iterator();
        while (iterator.hasNext()) {
            Particle next = iterator.next();
            next.draw(canvas, mPaint);
            if (next.alpha < 0.05f) {
                iterator.remove();
            }
        }

        canvas.restoreToCount(save);
        postInvalidate();

        angle -= 3.0f;
        if (angle <-360f) {
            angle = 0;
        }
    }

    static class Particle {

        private float alpha;
        private float radius;
        private int color;
        private float x;
        private float y;
        private float speedx;
        private float speedy;

        Particle(float x, float y, float radius, float alpha, float spx, float spy) {

            this.x = x;
            this.y = y;
            this.speedx = (float) ((Math.random() - 0.5) * spx);
            this.speedy = (float) ((Math.random() - 0.5) * spy);
            this.radius = radius;
            this.alpha = alpha;
            this.color = argb((float) Math.random(), (float) Math.random(), (float) Math.random());
        }

        void update() {
            this.alpha *= 0.97;
            this.x += this.speedx;
            this.y += this.speedy;
        }

        void draw(Canvas canvas, Paint paint) {
            paint.setColor(color);
            paint.setAlpha((int) (this.alpha * 255));
            canvas.drawCircle(this.x, this.y, this.radius, paint);
            this.update();
        }
    }

    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);
    }

}

好了,以上是本篇的核心内容

龙年,祝所有人都能腾飞。

相关推荐
~甲壳虫几秒前
说说webpack proxy工作原理?为什么能解决跨域
前端·webpack·node.js
Cwhat2 分钟前
前端性能优化2
前端
&岁月不待人&3 分钟前
Kotlin by lazy和lateinit的使用及区别
android·开发语言·kotlin
爱吃生蚝的于勒18 分钟前
深入学习指针(5)!!!!!!!!!!!!!!!
c语言·开发语言·数据结构·学习·计算机网络·算法
羊小猪~~21 分钟前
数据结构C语言描述2(图文结合)--有头单链表,无头单链表(两种方法),链表反转、有序链表构建、排序等操作,考研可看
c语言·数据结构·c++·考研·算法·链表·visual studio
王哈哈^_^1 小时前
【数据集】【YOLO】【VOC】目标检测数据集,查找数据集,yolo目标检测算法详细实战训练步骤!
人工智能·深度学习·算法·yolo·目标检测·计算机视觉·pyqt
星沁城1 小时前
240. 搜索二维矩阵 II
java·线性代数·算法·leetcode·矩阵
熊的猫1 小时前
JS 中的类型 & 类型判断 & 类型转换
前端·javascript·vue.js·chrome·react.js·前端框架·node.js
脉牛杂德1 小时前
多项式加法——C语言
数据结构·c++·算法
legend_jz1 小时前
STL--哈希
c++·算法·哈希算法