前语

现在为止,我数了下我的文章总数,已经70多篇了,写的根本都是实用性文章,并且根本都在最近3个月以内完结,为什么写这么多呢?主要原因是大环境比较差,期望多总结一下经历,其实我原本计划写open gl es系列的,由于当前的本职工作是音视频相关的,没想Canvas 2D的倒是越写越多了。

我在的榜首篇文章《Egl Context 与 open gl 纹路联系》和第二篇文章《Android系统中的坐标与矩阵系统》都是根底篇,假如有同学计划学习自界说View,建议熟悉下这两篇文章。

三月以前,我也写过《Android 焰火作用》,这篇我适当于做了个根底框架,在此根底上扩展和填充,就能扩展出许多作用。不过,其时,我在这篇文章中着重着重了一件事

要点:构建闭合空间

之所以着重这件事的原因是,只要闭合空间的图形才干填充颜色、图片纹路。咱们知道,Canvas 制作办法只是只要圆、弧、矩形、圆角矩形是能够闭合的,除此之外便是Path了。

想象一下,假如让你画一个三角形并填充上颜色,你可能的办法只要通过裁剪Path或许运用Path制作才行,而Path也有功能问题。

别的,闭合空间的填充也是件不容易的事。

所以,那篇文章中的焰火作用,本质上还不够完美,由于一些特殊的填充作用仍是很难完结。

新计划

Android 根据制作缓冲的焰火作用完结

现在我觉得可行的计划有两种

根据数学和Paint线宽突变

如:贝塞尔曲线函数 + strokeWidth渐渐增大 + Color 改变

这种办法是运用贝塞尔曲线计算出途径(不用Path,根据数学公式描绘),然后再规定的时刻内让Paint的strokeWidth跟着贝塞尔曲线 * time的偏移而增大,就能制作出作用不错的的焰火条。

根据制作缓冲

首先,要知道什么是缓冲,缓冲其实便是一般意义上存储数据的目标,比如byte数组、ByteBuffer等,但假如再聚焦Android 渠道,咱们还有FBO、VBO等。当然,最容易被疏忽的是Bitmap,Bitmap 其实也是FBO的一种,不过这儿我称之为“可视化缓冲”。

假如追寻的具体的目标上,除了Bitmap之外,Layer也是缓冲。

为什么运用缓冲能够优化焰火作用呢?

咱们先了解下缓冲的特性:

  • 占用空间较大,狭义上来说,这种数据不只是占用空间大,并且(虚拟)内存需求连续
  • 空间可复用性强,如享元模式的ByteBuffer、alpha离屏烘托buffer、inBitmap等
  • 会发生脏数据,比如上一次buffer中的数据,假如没有整理的话仍然会保存
  • 数据可复用性强,脏数据并不一定“脏”,有时还能复用

咱们终究运用的仍是空间可复用性和数据可复用性,假如咱们以每次都在前次的数据中制作,那么,意味着能够制作出更多作用,间接处理了闭合空间填充问题。

那么,本篇咱们选哪种呢?

终究计划

本篇,咱们就挑选根据缓冲的计划了,由于总的来说,榜首种办法可能需求很屡次的制作,适当考验CPU。而运用制作缓冲的的话,咱们还能够复用前次的数据,这就适当于将上一次的制作画面保存,然后再一次制作时,在之前的根底上进一步完善,这种明显是运用“空间交换时刻”的做法。

Android中有不少地方运用了“空间交换时刻”的制作,比如alpha叠加、SurfaceView双缓冲、TextureView缓冲等,当然,咱们在《Android 视频图像实时文字化》中仍是用了以Bitmap为缓冲的技术。

具体规划

本篇运用了制作缓冲,原则上运用Bitmap是能够的,可是在运用的进程中发现,Bitmap在xformode制作时功能仍是很差,明显提升流通度是必要原则。那么,你可能想到运用线程异步制作,是的,我也计划这么做,可是想到运用线程烘托,那为什么不运用TextureView、SurfaceView或许GLSurfaceView呢?于是,我就没有再运用Bitmap的想法了。

可是,根据做音视频的经历,我选了个兼容性最好功能最差的TextureView,其实我这儿本计划选GLSurfaceView的,由于其功能和兼容性都是居中水平,不过涉及到极点、纹路的一套东西,计划后续在音视频专栏写这类文章,因而本篇就选TexureView了。

简单说下SurfaceView的问题,功能最好,但其不适合在滑动的页面调用,由于有些设备会呈现画面漂移和缩放的问题,别的不支持clipchildren等,理论上也是适合本篇的,可是假如app回到后台,其Surface会自动毁掉,因而,操控线程的逻辑就会有些杂乱。

在这儿咱们看下TextureView源码,其创建的SurfaceTexture并不是单缓冲模式,可是TexureLayer却是当之无愧的缓冲。

mLayer = mAttachInfo.mThreadedRenderer.createTextureLayer();
boolean createNewSurface = (mSurface == null);
if (createNewSurface) {
    // Create a new SurfaceTexture for the layer.
    mSurface = new SurfaceTexture(false);
    nCreateNativeWindow(mSurface);
}
mLayer.setSurfaceTexture(mSurface);

下面是咱们的具体流程。

完结焰火逻辑

下面是咱们本篇的完结流程。

界说FireExploreView

咱们本篇根据TextureView完结制作逻辑,而TextureView必须要开启硬件加速,其次咱们要完结TextureView.SurfaceTextureListener,用于监听SurfaceTexture的创建和毁掉。理论上,TextureView的SurfaceTexture能够复用的,其次,假如onSurfaceTextureDestroyed回来false,那么SurfaceTexture的毁掉是由你自己操控的,TextureView不会自动毁掉。

@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    return false;
}

别的,咱们要知道,默认情况下TextureView运用的是TextureLayer,制作完结之后,需求在RenderThread上运用gl去组成,这也是功能较差的主要原因。尤其是低配设备,运用TextureView也做不到功能优化,终究仍是得运用SurfaceView或许GLTextureView或许GLSurfaceView,当然我比较推荐GL系列,主要是离屏烘托能够避免MediaCodec切换Surface引发黑屏和卡住的问题。

当然,这儿咱们必定也要运用到线程和Surface了,相关代码如下

@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
    drawThread = new Thread(this);
    this.surfaceTexture = surfaceTexture;
    this.surface = new Surface(this.surfaceTexture);
    this.isRunning = true;
    this.drawThread.start();
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
    isRunning = false;
    if (drawThread != null) {
        try {
            drawThread.interrupt();
        }catch (Throwable e){
            e.printStackTrace();
        }
    }
    drawThread = null;
    //不让TextureView 毁掉SurfaceTexture,这儿回来false
    return false; 
}

界说粒子

无论任何时分,不要把粒子不当目标,一些开发者对粒子目标不以为然,这明显是不对的,不受办理的粒子凭什么听你的指挥。

当然,任何粒子的运动需求契合运动学方程,而二维平面的运动是能够拆分为X轴和Y轴单方向的运动的。


static final float gravity = 0.0f;
static final float fraction = 0.88f;
static final float speed = 50f; //最大速度
static class Particle {
    private float opacity;  //透明度
    private float dy; // y 轴速度
    private float dx; // x 轴速度
    private int color; //此颜色
    private float radius; //半径
    private float y; // y坐标
    private float x; // x坐标
    Particle(float x, float y, float r, int color, float speedX, float speedY) {
        this.x = x;
        this.y = y;
        this.radius = r;
        this.color = color;
        this.dx = speedX;
        this.dy = speedY;
        this.opacity = 1f;
    }
    void draw(Canvas canvas, Paint paint) {
        int save = canvas.save();
        paint.setAlpha((int) (this.opacity * 255));
        paint.setColor(this.color);
        canvas.drawCircle(this.x, this.y, this.radius, paint);
        canvas.restoreToCount(save);
    }
    void update() {
        this.dy += gravity; 
        //加上重力因子,那么就会呈现粒子重力现象,这儿咱们不运用时刻了,这样简单点
        this.dx *= fraction;  // fraction 是小于1的,用于下降速度
        this.dy *= fraction;  // fraction 是小于1的,用于下降速度
        this.x += this.dx;
        this.y += this.dy;
        this.opacity -= 0.03; //透明度递减
    }
}

上面是粒子以及更新办法、制作逻辑。

办理粒子

咱们运用List办理粒子

static final int maxParticleCount = 300;
List<Particle> particles = new ArrayList<>(maxParticleCount);

初始化粒子

粒子的初始化是非常重要的,初始化位置的正确与否会影响粒子的整体作用,明显,这儿咱们需求留意。

float angleIncrement = (float) ((Math.PI * 2) / maxParticleCount); //平分 360度
float[] hsl = new float[3];
for (int i = 0; i < maxParticleCount; i++) {
    hsl[0] = (float) (Math.random() * 360);
    hsl[1] = 0.5f;
    hsl[2] = 0.5f;
    int hslToColor = HSLToColor(hsl);
    Particle p = new Particle(x, y,
            2.5f,
            hslToColor,
            (float) (Math.cos(angleIncrement * i) * Math.random() * speed),
            (float) (Math.sin(angleIncrement * i) * Math.random() * speed)
    );
    particles.add(p);
}

不过,在这儿咱们还需求留意的是,这儿咱们运用HLS,这是一种色彩空间,和RGB不一样的是,他有Hue(色调)、饱和度、亮度为基准,因而,有利于亮色的表示,因而适合获取着重亮度的色彩。

与rgb的转化逻辑如下

public static int HSLToColor(@NonNull float[] hsl) {
    final float h = hsl[0];
    final float s = hsl[1];
    final float l = hsl[2];
    final float c = (1f - Math.abs(2 * l - 1f)) * s;
    final float m = l - 0.5f * c;
    final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
    final int hueSegment = (int) h / 60;
    int r = 0, g = 0, b = 0;
    switch (hueSegment) {
        case 0:
            r = Math.round(255 * (c + m));
            g = Math.round(255 * (x + m));
            b = Math.round(255 * m);
            break;
        case 1:
            r = Math.round(255 * (x + m));
            g = Math.round(255 * (c + m));
            b = Math.round(255 * m);
            break;
        case 2:
            r = Math.round(255 * m);
            g = Math.round(255 * (c + m));
            b = Math.round(255 * (x + m));
            break;
        case 3:
            r = Math.round(255 * m);
            g = Math.round(255 * (x + m));
            b = Math.round(255 * (c + m));
            break;
        case 4:
            r = Math.round(255 * (x + m));
            g = Math.round(255 * m);
            b = Math.round(255 * (c + m));
            break;
        case 5:
        case 6:
            r = Math.round(255 * (c + m));
            g = Math.round(255 * m);
            b = Math.round(255 * (x + m));
            break;
    }
    r = constrain(r, 0, 255);
    g = constrain(g, 0, 255);
    b = constrain(b, 0, 255);
    return Color.rgb(r, g, b);
}
private static int constrain(int amount, int low, int high) {
    return amount < low ? low : Math.min(amount, high);
}

粒子制作

制作当然简单了,办法完结不是很杂乱,调用如下逻辑即可,当然,opacity<=0 的粒子咱们并没有移除,原因是由于remove 时, 可能引发ArrayList内存重整,这个是适当耗费功能的,因而,还不如遍历效率高。

protected void drawParticles(Canvas canvas) {
    canvas.drawColor(0x10000000); //为了让焰火削弱作用,每次加深制作
    for (int i = 0; i < particles.size(); i++) {
        Particle particle = particles.get(i);
        if (particle.opacity > 0) {
            particle.draw(canvas, mPaint);
            particle.update();
        }
    }
}

缓冲复用

那么,以上便是完好的制作逻辑了,至于Surface调用逻辑呢,其实也很简单。

不过这儿要留意的是,只要接受到command=true的时分,咱们才整理画布,否则,咱们要保存缓冲区中的数据。咱们知道,一般View在onDraw的时分,RenderNode给你的Canvas都是整理过的,而这儿,咱们每次通过lockCanvas拿到的Canvas是带有前次缓冲数据的。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
    canvas = surface.lockHardwareCanvas();
} else {
    canvas = surface.lockCanvas(null);
}
if(isCommand){
    particles.clear();
    canvas.drawColor(0x99000000, PorterDuff.Mode.CLEAR); //整理画布
    explode(getWidth() / 2f, getHeight() / 2f);
    isCommand = false;
}
//制作粒子
drawParticles(canvas);
surface.unlockCanvasAndPost(canvas);

Android 根据制作缓冲的焰火作用完结

明显,咱们能得到两条经历:

  • lockCanvas获取到的Canvas是带有前次制作数据的
  • 运用缓冲制作不仅着重成果,并且还着重进程,一般的Canvas制作只是着重成果

Blend作用增强

实际上面的作用还有点差,便是尖端亮度太低,为此,咱们能够运用Blend进行增强,咱们设置BlendMode为PLUS,别的上面咱们的重力是0,现在咱们调整一下gravity=0.25f。

PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS);

作用

Android 根据制作缓冲的焰火作用完结

多线程制作

总的来说,TextureView能够在一些情况下明显提升功能,当然,条件是你的主线程流通。

这儿的逻辑便是TextureView的用法了,咱们就不持续深入了,本篇末尾供给源码。

总结

以上是本篇的内容,也是咱们要掌握的技巧,许多时分,咱们对Canvas的制作,过于着重成果,成果规划了许多杂乱的算法,其实,根据进程的制作明显更加简单和优化。

到这儿本篇就完毕了,期望本篇对你有所协助。

源码

public class FireExploreView extends TextureView implements TextureView.SurfaceTextureListener, Runnable {
    private TextPaint mPaint;
    private SurfaceTexture surfaceTexture;
    private Surface surface;
    {
        initPaint();
    }
    public FireExploreView(Context context) {
        this(context, null);
    }
    public FireExploreView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
        setSurfaceTextureListener(this);
    }
    private void initPaint() {
        //否则供给给外部纹路制作
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setStyle(Paint.Style.FILL_AND_STROKE);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStyle(Paint.Style.FILL);
        PaintCompat.setBlendMode(mPaint, BlendModeCompat.PLUS);
    }
    static final float gravity = 0.21f;
    static final float fraction = 0.88f;
    static final int maxParticleCount = 300;
    List<Particle> particles = new ArrayList<>(maxParticleCount);
    float[] hsl = new float[3];
    volatile boolean isCommand = false;
    static final float speed = 50f;
    Thread drawThread = null;
    public void startExplore() {
        isCommand = true;
    }
    void explode(float x, float y) {
        float angleIncrement = (float) ((Math.PI * 2) / maxParticleCount);
        for (int i = 0; i < maxParticleCount; i++) {
            hsl[0] = (float) (Math.random() * 360);
            hsl[1] = 0.5f;
            hsl[2] = 0.5f;
            int hslToColor = HSLToColor(hsl);
            Particle p = new Particle(x, y,
                    3f,
                    hslToColor,
                    (float) (Math.cos(angleIncrement * i) * Math.random() * speed),
                    (float) (Math.sin(angleIncrement * i) * Math.random() * speed)
            );
            particles.add(p);
        }
    }
    protected void drawParticles(Canvas canvas) {
        canvas.drawColor(0x10000000);
        for (int i = 0; i < particles.size(); i++) {
            Particle particle = particles.get(i);
            if (particle.opacity > 0) {
                particle.draw(canvas, mPaint);
                particle.update();
            }
        }
    }
    static class Particle {
        private float opacity;
        private float dy;
        private float dx;
        private int color;
        private float radius;
        private float y;
        private float x;
        Particle(float x, float y, float r, int color, float speedX, float speedY) {
            this.x = x;
            this.y = y;
            this.radius = r;
            this.color = color;
            this.dx = speedX;
            this.dy = speedY;
            this.opacity = 1f;
        }
        void draw(Canvas canvas, Paint paint) {
            int save = canvas.save();
            paint.setAlpha((int) (this.opacity * 255));
            paint.setColor(this.color);
            canvas.drawCircle(this.x, this.y, this.radius, paint);
            canvas.restoreToCount(save);
        }
        void update() {
            this.dy += gravity;
            this.dx *= fraction;
            this.dy *= fraction;
            this.x += this.dx;
            this.y += this.dy;
            this.opacity -= 0.03;
        }
    }
    private volatile boolean isRunning = false;
    private final Object lockSurface = new Object();
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                try {
                    this.wait(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            if (!isRunning || Thread.currentThread().isInterrupted()) {
                synchronized (lockSurface) {
                    if (surface != null && surface.isValid()) {
                        surface.release();
                    }
                    surface = null;
                }
                break;
            }
            Canvas canvas = null;
            synchronized (lockSurface) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    canvas = surface.lockHardwareCanvas();
                } else {
                    canvas = surface.lockCanvas(null);
                }
                if(isCommand){
                    particles.clear();
                    canvas.drawColor(0x99000000, PorterDuff.Mode.CLEAR);
                    explode(getWidth() / 2f, getHeight() / 2f);
                    isCommand = false;
                }
                drawParticles(canvas);
                surface.unlockCanvasAndPost(canvas);
            }
        }
    }
    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
        drawThread = new Thread(this);
        this.surfaceTexture = surfaceTexture;
        this.surface = new Surface(this.surfaceTexture);
        this.isRunning = true;
        this.drawThread.start();
    }
    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
    }
    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        isRunning = false;
        if (drawThread != null) {
            try {
                drawThread.interrupt();
            }catch (Throwable e){
                e.printStackTrace();
            }
        }
        drawThread = null;
        return false;
    }
    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {
    }
    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);
    }
    @ColorInt
    public static int HSLToColor(@NonNull float[] hsl) {
        final float h = hsl[0];
        final float s = hsl[1];
        final float l = hsl[2];
        final float c = (1f - Math.abs(2 * l - 1f)) * s;
        final float m = l - 0.5f * c;
        final float x = c * (1f - Math.abs((h / 60f % 2f) - 1f));
        final int hueSegment = (int) h / 60;
        int r = 0, g = 0, b = 0;
        switch (hueSegment) {
            case 0:
                r = Math.round(255 * (c + m));
                g = Math.round(255 * (x + m));
                b = Math.round(255 * m);
                break;
            case 1:
                r = Math.round(255 * (x + m));
                g = Math.round(255 * (c + m));
                b = Math.round(255 * m);
                break;
            case 2:
                r = Math.round(255 * m);
                g = Math.round(255 * (c + m));
                b = Math.round(255 * (x + m));
                break;
            case 3:
                r = Math.round(255 * m);
                g = Math.round(255 * (x + m));
                b = Math.round(255 * (c + m));
                break;
            case 4:
                r = Math.round(255 * (x + m));
                g = Math.round(255 * m);
                b = Math.round(255 * (c + m));
                break;
            case 5:
            case 6:
                r = Math.round(255 * (c + m));
                g = Math.round(255 * m);
                b = Math.round(255 * (x + m));
                break;
        }
        r = constrain(r, 0, 255);
        g = constrain(g, 0, 255);
        b = constrain(b, 0, 255);
        return Color.rgb(r, g, b);
    }
    private static int constrain(int amount, int low, int high) {
        return amount < low ? low : Math.min(amount, high);
    }
}