Android 四种常见的loading效果

1,757 阅读3分钟

一、前言

队列动画我们之前有篇文章专门写过,实际上每个动画结束后的状态处理是难点,队列动画的特定是

  1. 顺序执行 : 动画执行不存在超过前一帧的情况,但可以晚于前一顺序的时间(t >= 0)播放,也可以并行执行,但仍然保持顺序方法调用。

  2. 每个执行点位具备固定的时间: duration ,每一个点位执行的动画的时间应该是一样的

  3. 允许设置延迟间隔:可设置每一点位相对前一个位置的延迟开始时间

  4. 每个执行点位必须在duration之后才能发起下次动画

1.1 缓冲式效果

1.2 流体效果

1.3 扩散

二、原理

2.1 缓冲式动画原理

实际上,原理很简单,但是难点确实首尾平滑过渡问题,对于不需要平滑过渡的动画,在点位动画duration执行后,再次发起即可,但是对于需要首尾平滑过渡的动画,这个难点在于,必须保证 每个动画较前一个动画duration/n的时间间隔,同时满足下次执行动画必须在(n -1)* (duration/n) 时间之后才能执行,总结如下。

  • 开始时时间间隔:duration/n
  • 下次执行时间:(n -1)* (duration/n)
  • 下次无需考虑时间间隔,因为在(n -1)* (duration/n)时间后,就已经到到时间需要了开始动画了。

2.2 流体动画原理

流体动画相对简单,只不过需要控制粒子的的随机效果

boolean update() {    
    this.randomSpeed.x = getRandomRange(-0.001f, 0.001f);    
    this.randomSpeed.y = getRandomRange(0.01f, 0.02f);    
    this.speed.x += this.randomSpeed.x;    
    this.speed.y += this.randomSpeed.y;    
    this.x += this.speed.x;    
    this.y += this.speed.y;    
    if (this.radius >= 0.01f) {        
        this.radius -= 0.2f;        
        this.alpha -= 0.001f;        
        return true;    
    }    
    this.radius = 0f;    
    this.alpha = 0f;    
    return false;
}

2.3 粒子扩散动画原理

本质上,此种动画基于2.2实现,因此原理和2.2一致

三、实现

3.1 缓冲式动画实现

之前的动画使用自己写的动画,这次我们使用Animator实现,使用先加速后减速的插值器实现快慢变化。

我们这里实现2种预览图种的效果,通过LINE_STYLE_DOT进行控制样式。

public class LoadingView extends View {
    RectF arcBounds = new RectF();

    private static final long ANIMATION_TIMEOUT = 1500;
    private TextPaint mPaint;
    private int mMaxRadius;
    private int mDotWidth;
    private int mColor = Color.TRANSPARENT;
    private AnimatorSet mCircleDotAnimatorSet;

    private float[] mDotAngles = null;

    public final static int LINE_STYLE_DOT = 0;
    public final static int LINE_STYLE_ARC = 1;
    private int mLineStyle = LINE_STYLE_DOT;

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

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

    public LoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        initPaint();
    }


    public void setLineStyle(int mLineStyle) {
        this.mLineStyle = mLineStyle;
        if (mLineStyle == LINE_STYLE_DOT) {
            mDotWidth = (int) dip2px(8);
        } else {
            mDotWidth = (int) dip2px(3);
        }
    }

    public float dip2px(int dp) {
        return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
    }


    private void initPaint() {
        // 实例化画笔并打开抗锯齿
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setPathEffect(new CornerPathEffect(10)); //设置线条类型
        mPaint.setStrokeWidth(dip2px(1));
        mPaint.setTextSize(dip2px((12)));
        mPaint.setStyle(Paint.Style.STROKE);

        mDotWidth = (int) dip2px(8);
        mColor = argb((float) Math.random(), (float) Math.random(), (float) Math.random());

    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
        mDotAngles = null;
    }

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

        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);

        if (widthMode != MeasureSpec.EXACTLY) {
            width = (int) dip2px(100);
        }
        if (heightMode != MeasureSpec.EXACTLY) {
            height = (int) dip2px(100);
        }
        setMeasuredDimension(width, height);

    }

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

        int width = getWidth();
        int height = getHeight();

        if (width == 0 || height == 0) return;

        int diameter = Math.min(width, height) / 2;
        mMaxRadius = diameter - mDotWidth / 2;

        initPoints();

        int storeId = canvas.save();
        canvas.translate(width / 2F, height / 2F);

        if (mLineStyle == LINE_STYLE_DOT) {
            drawDotCircle(canvas);
        } else {
            drawArcCircle(canvas);
        }

        canvas.restoreToCount(storeId);

    }

    private void drawArcCircle(Canvas canvas) {

        if (mDotAngles == null || mDotAngles.length == 0) return;
        int firstPointY = (int) (Math.sin(Math.toRadians(mDotAngles[0])) * mMaxRadius);
        int firstPointX = (int) (Math.cos(Math.toRadians(mDotAngles[0])) * mMaxRadius);

        int lastPointY = (int) (Math.sin(Math.toRadians(mDotAngles[mDotAngles.length - 1])) * mMaxRadius);
        int lastPointX = (int) (Math.cos(Math.toRadians(mDotAngles[mDotAngles.length - 1])) * mMaxRadius);

        double lineLenghtPower = Math.pow(firstPointY - lastPointY, 2) + Math.pow(firstPointX - lastPointX, 2);
        double length = Math.sqrt(lineLenghtPower) / 2f;

        float degrees = (float) Math.toDegrees(Math.asin(length / mMaxRadius)) * 2;

        drawArcSport(canvas, mDotAngles[mDotAngles.length - 1], degrees);
    }

    private void drawDotCircle(Canvas canvas) {

        if (mDotAngles == null) return;

        int color = mPaint.getColor();
        float strokeWidth = mPaint.getStrokeWidth();
        Paint.Cap strokeCap = mPaint.getStrokeCap();


        mPaint.setStrokeWidth(mDotWidth);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setColor(mColor);

        for (int i = 0; i < mDotAngles.length; i++) {
            double dotAngle = mDotAngles[i];
            int pointY = (int) (Math.sin(Math.toRadians(dotAngle)) * mMaxRadius);
            int pointX = (int) (Math.cos(Math.toRadians(dotAngle)) * mMaxRadius);
            canvas.drawPoint(pointX, pointY, mPaint);
        }

        mPaint.setColor(color);
        mPaint.setStrokeWidth(strokeWidth);
        mPaint.setStrokeCap(strokeCap);
    }

    private void initPoints() {
        if (mDotAngles != null && mDotAngles.length == 5) return;
        mDotAngles = new float[5];

        float dotRadius = mDotWidth / 2f;

        float dotAngle = (float) Math.toDegrees(Math.atan(dotRadius / mMaxRadius) * 2);

        for (int i = 0; i < mDotAngles.length; i++) {
            float angle = 360f - (dotAngle + 8) * (i + 1);
            mDotAngles[i] = angle;
        }
        if (mCircleDotAnimatorSet != null) {
            mCircleDotAnimatorSet.cancel();
        }
        mCircleDotAnimatorSet = buildDotAnimationSet();
        mCircleDotAnimatorSet.start();
    }

    private AnimatorSet buildDotAnimationSet() {

        AnimatorSet animatorSet = new AnimatorSet();
        Animator[] animators = new Animator[mDotAngles.length];
        for (int i = 0; i < mDotAngles.length; i++) {
            Animator animator = buildDotAnimation(mDotAngles[i], i);
            animator.setStartDelay(100 * i);
            animators[i] = animator;
        }
        animatorSet.playTogether(animators);
        return animatorSet;
    }

    private Animator buildDotAnimation(final float startAngle, final int i) {

        ValueAnimator animatorTimer = ValueAnimator.ofFloat(0, 1);
        animatorTimer.setDuration(ANIMATION_TIMEOUT);
        animatorTimer.setRepeatCount(ValueAnimator.INFINITE);
        animatorTimer.setInterpolator(new AccelerateDecelerateInterpolator());
        animatorTimer.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float fraction = animation.getAnimatedFraction();
                float angle = (float) (startAngle + fraction * 360);
                if (mDotAngles != null) {
                    mDotAngles[i] = angle;
                }
                if (i == 0) {
                    postInvalidate();
                }
            }
        });
        return animatorTimer;

    }


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


    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();
        try {
            if (mCircleDotAnimatorSet != null) {
                mCircleDotAnimatorSet.cancel();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void drawArcSport(Canvas canvas, float currentAngle, float sweepAngle) {
        int oldColor = mPaint.getColor();
        Paint.Style style = mPaint.getStyle();
        float strokeWidth = mPaint.getStrokeWidth();

        mPaint.setStrokeWidth(mDotWidth);
        mPaint.setColor(mColor);
        mPaint.setStyle(Paint.Style.STROKE);
        arcBounds.set(-mMaxRadius, -mMaxRadius, mMaxRadius, mMaxRadius);
        canvas.drawArc(arcBounds, currentAngle, sweepAngle, false, mPaint);
        mPaint.setColor(oldColor);
        mPaint.setStyle(style);
        mPaint.setStrokeWidth(strokeWidth);
    }
    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);
    }

}

3.2 流体式动画效果实现

这部分主要是通过粒子效果来实现,基本是围绕圆运动的过程更新和产生新的粒子,不过这里要注意的一点是,粒子是y轴方向是向下运动,因此,速度必须小于0.

public class FluidLoadingView extends View {
    private TextPaint mPaint;
    public FluidLoadingView(Context context) {
        super(context);
    }
    public FluidLoadingView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public FluidLoadingView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    {
        initPaint();
    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setAntiAlias(true);
        mPaint.setDither(true);
        mPaint.setStrokeCap(Paint.Cap.ROUND);
        mPaint.setStyle(Paint.Style.FILL);
        mPaint.setTextSize(sp2px(14));
        mPaint.setStrokeWidth(10);
    }


    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

    List<Particle> particles = new ArrayList<>();
    float angle = (float) (45);

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        int width = getWidth();
        int height = getHeight();
        int count = canvas.save();
        final float radius = Math.min(width,height) / 5f ;

        canvas.translate(width/2f,height/2f);
        if(particles.size() < 100){
            Particle particle = createParticle(angle,radius);
            particles.add(particle);
        }
        for (int j = 0; j < particles.size(); j++) {
            Particle p = particles.get(j);
            boolean update = p.update();
            p.draw(canvas,mPaint);
            if(!update){
                double radians = Math.toRadians(angle);
                float x = (float) (radius * Math.sin(radians));
                float y = (float) (radius * Math.cos(radians));
                p.init(x,y);
            }
        }
        canvas.restoreToCount(count);
        if (angle <=  360f) {
            angle += 2.5f;
        } else {
            angle = angle - 360f;
        }
        postInvalidate();

    }

    private Particle createParticle(float angle,float radius) {

        double radians = Math.toRadians(angle);
        float x = (float) (radius * Math.sin(radians));
        float y = (float) (radius * Math.cos(radians));
        Particle p = new Particle();
        p.init(x,y);
        return p;

    }

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

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

    static float getRandomRange(float min, float max) {
        return (float) (min + Math.floor(Math.random() * (max - min + 1)));
    }


    static class Particle {
        float radius;
        float x;
        float y;
        PointF speed = null;
        PointF randomSpeed = null;
        float alpha = 1f;

        Particle(){
            this.speed = new PointF();
            this.randomSpeed = new PointF();
        }

        private void init(float x, float y) {
            this.x = x;
            this.y = y;
            this.radius = getRandomRange(10, 16);
            this.speed.x = 0;
            this.speed.y = 0;
            this.randomSpeed.x = 0;
            this.randomSpeed.y = 0;
            this.alpha = 1f;
        }

        boolean update() {
            this.randomSpeed.x = getRandomRange(-0.001f, 0.001f);
            this.randomSpeed.y = getRandomRange(0.01f, 0.02f);
            this.speed.x += this.randomSpeed.x;
            this.speed.y += this.randomSpeed.y;
            this.x += this.speed.x;
            this.y += this.speed.y;

            if (this.radius >= 0.01f) {
                this.radius -= 0.2f;
                this.alpha -= 0.001f;
                return true;
            }
            this.radius = 0f;
            this.alpha = 0f;
            return false;
        }

        void draw(Canvas canvas,Paint paint) {
            paint.setColor(argb(1,1,1,this.alpha));
            canvas.drawCircle(this.x, this.y, this.radius, paint);
        }
    }

    public static int argb(float alpha, float red, float green, float blue) {
        return ((int) (alpha * 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);
    }

}

3.3 粒子扩散动画

扩散动画主要核心逻辑和3.2类似,但是不同点四,这里使用2000多个粒子,同时使用Shader来渲染 

public class ParticleShaderView extends View {
    private TextPaint mPaint;
    private TextPaint mParticlePaint;
    private final List<Particle> particles = new ArrayList<>();
    private final List<Particle> recycleParticles = new ArrayList<>();
    private final List<Particle> aliveParticles = new ArrayList<>();
    private final Matrix matrix = new Matrix();
    private float angle = 45;
    private float imageAngle = 45;
    private final float particleCircleRadius = 4.5f;
    private final int MAX_NUM = 2048;

    private boolean isResume = true;
    private int primaryColor = Color.TRANSPARENT;
    private float particleTrackWidth;
    private boolean isShouldCreate = false;

    private final String TAG = "DeepMusicPlayingView";
    private TextPaint mBitmapPaint;
    private int secondaryColor;

    private Matrix particleMatrix = new Matrix();
    private int particleColor = 0xFFFF1493;

    public ParticleShaderView(Context context) {
        super(context);
    }

    public ParticleShaderView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public ParticleShaderView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }


    private Bitmap primaryImageBitmap;
    private Shader primaryBitmapShader;

    private Shader particleShader;


    private final int threshold;

    {
        initPaint();
        threshold = MAX_NUM - 50;

    }

    private void initPaint() {
        //否则提供给外部纹理绘制
        this.mPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        this.mPaint.setAntiAlias(true);
        this.mPaint.setDither(true);
        this.mPaint.setStrokeCap(Paint.Cap.ROUND);
        this.mPaint.setStyle(Paint.Style.FILL);
        this.mPaint.setTextSize(sp2px(14));
        this.mPaint.setStrokeWidth(5);

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

        this.mBitmapPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
        this.mBitmapPaint.setAntiAlias(true);
        this.mBitmapPaint.setDither(true);
        this.mBitmapPaint.setStyle(Paint.Style.FILL);

        this.particleTrackWidth = dp2px(30);
    }


    public void setPrimaryColor(int primaryColor) {
        this.primaryColor = primaryColor;
        this.primaryBitmapShader = null;
        postInvalidate();
    }

    public void pause() {
        this.isResume = false;
        invalidate();
    }

    public void resume() {
        this.isResume = true;
        invalidate();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
        int size = Math.min(width, height);
        setMeasuredDimension(size, size);
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

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

        boolean isResume = this.isResume;

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

        int save = canvas.save();
        canvas.translate(width / 2f, height / 2f);
        float distance = radius - particleTrackWidth *2/3f ;
        float textureRadius = radius - particleTrackWidth;

        if(this.particleShader == null && this.particleColor != Color.TRANSPARENT){
            this.mParticlePaint.setColor(this.particleColor);
            this.particleShader = new RadialGradient(0, 0, particleCircleRadius, new int[]{this.particleColor, Color.TRANSPARENT}, new float[]{
                0, 0.85f
            }, Shader.TileMode.CLAMP);
            this.mParticlePaint.setShader(this.particleShader);
        }

        mPaint.setColor(this.primaryColor);
        canvas.drawCircle(0, 0, textureRadius, mPaint);


        if (this.secondaryColor != Color.TRANSPARENT) {
            mPaint.setColor(this.secondaryColor);
            canvas.drawCircle(0, 0, radius * 3 / 5f, mPaint);
        }

        drawPrimaryBitmap(canvas, radius * 3 / 5f - this.particleTrackWidth/2f);

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


        mPaint.setColor(this.primaryColor);
        canvas.drawCircle(lx, ly, particleCircleRadius, mPaint);

        aliveParticles.clear();

        for (int i = 0; i < particles.size(); i++) {
            Particle next = particles.get(i);
            next.draw(canvas, mParticlePaint, particleMatrix);
            if(isResume) {
                next.update();
                if (next.alpha >= 0.05f) {
                    aliveParticles.add(next);
                } else {
                    recycleParticles.add(next);
                }
            }
        }
        canvas.restoreToCount(save);
        if (isResume) {

            particles.clear();
            particles.addAll(aliveParticles);

            int particleCount = particles.size();

            if (!isShouldCreate && particleCount < threshold) {
                isShouldCreate = true;
            }

            if (isShouldCreate) {
                final float speed = 1.5f;
                if (MAX_NUM - particleCount >= 20) {
                    int count = (int) (1 + Math.random() * 19);
                    for (int i = 0; i < count; i++) {
                        Particle particle = createParticle(lx, ly, 1, speed, speed);
                        particles.add(particle);
                    }
                } else {
                    Particle particle = createParticle(lx, ly, 1, speed, speed);
                    particles.add(particle);
                }
            }
            if (particles.size() >= MAX_NUM - 50) {
                isShouldCreate = false;
            }

            if (angle <= 360f) {
                angle += 1.5f;
            } else {
                angle = angle - 360f;
            }
            if (imageAngle <= 360f) {
                imageAngle += 0.5f;
            } else {
                imageAngle = imageAngle - 360f;
            }
            postInvalidateDelayed(16);
        }

    }

    private void drawPrimaryBitmap(Canvas canvas, float textureRadius) {
        if (this.primaryImageBitmap != null && !this.primaryImageBitmap.isRecycled()) {
            if (primaryBitmapShader == null) {
                primaryBitmapShader = new BitmapShader(this.primaryImageBitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP);
            }
            drawRotateBitmap(canvas, this.primaryImageBitmap, textureRadius, primaryBitmapShader, mBitmapPaint);
        }
    }

    private void drawRotateBitmap(Canvas canvas, Bitmap primaryImageBitmap, float textureRadius, Shader primaryBitmapShader, Paint bitmapPaint) {
        matrix.reset();
        int bitmapWidth = primaryImageBitmap.getWidth();
        matrix.postTranslate(-bitmapWidth / 2f, -bitmapWidth / 2f);
        matrix.postScale(textureRadius * 2 / bitmapWidth, textureRadius * 2 / bitmapWidth);
        matrix.postRotate(imageAngle);
        primaryBitmapShader.setLocalMatrix(matrix);
        bitmapPaint.setShader(primaryBitmapShader);
        canvas.drawCircle(0, 0, textureRadius, bitmapPaint);
    }


    private Particle createParticle(float lx, float ly, int alpha, float spx, float spy) {
        if(!recycleParticles.isEmpty()){
            int i = recycleParticles.size() - 1;
            Particle particle = recycleParticles.remove(i);
            particle.reset(lx,ly,particleCircleRadius,alpha,spx,spy);
            return particle;
        }
        return new Particle(lx, ly, particleCircleRadius, alpha, spx, spy);
    }


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

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

    static float getRandomRange(float min, float max) {
        return (float) (min + Math.floor(Math.random() * (max - min + 1)));
    }

    public void setSecondaryColor(int secondaryColor) {
        this.secondaryColor = secondaryColor;
        postInvalidate();
    }

    public void setPrimaryImage(Bitmap bitmap) {
        this.primaryImageBitmap = bitmap;
        postInvalidate();
    }

    public void setParticleColor(int particleColor) {
        this.particleColor = particleColor;
        this.particleShader = null;
        if(this.mParticlePaint != null){
            this.mParticlePaint.setShader(null);
        }
        postInvalidate();
    }

    static class Particle {

        private float alpha;
        private float radius;
        private int color = Color.TRANSPARENT;
        private float x;
        private float y;
        private float speedx;
        private float speedy;
        private double alphaTransformer;

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

            init(x, y, radius, alpha, spx, spy);
        }

        private void init(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.alphaTransformer = 0.005 + 0.005 * Math.random();
           // this.color = argb(1f, (float) Math.random(), (float) Math.random(), (float) Math.random());
        }

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

        void draw(Canvas canvas, Paint paint, Matrix matrix) {
            Shader shader = paint.getShader();
            if(shader == null){
                paint.setColor(color);
            }else {
                if(matrix == null){
                    matrix = new Matrix();
                }else{
                    matrix.reset();
                }
                matrix.postTranslate(this.x,this.y);
                shader.setLocalMatrix(matrix);
            }
            paint.setAlpha((int) (this.alpha * 255));
            canvas.drawCircle(this.x, this.y, this.radius, paint);
        }

        public void reset(float x, float y, float radius, float alpha, float spx, float spy) {
            init(x, y, radius, alpha, spx, spy);
        }
    }

}

四、总结

之前我们的一篇是  《Android 自定义队列动画》,专门从细节点和公式上处理了循环问题,这次使用Animator就不需要考虑这种问题了,相比来说更加简单直观。