炸裂的点赞动画

4,322 阅读3分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第13天,点击查看活动详情

前言

之前偶然间看到某APP点赞有个炸裂的效果,觉得有点意思,就尝试了下,轻微还原,效果图如下

GIF.gif

封装粒子

从动画效果中我们可以看到,当动画开始的时候,会有一组粒子从四面八方散射出去,然后逐渐消失,于是可以定义一个粒子类包含以下属性

public class Particle {
    public float x, y;
    public float startXV;
    public float startYV;
    public float angle;
    public float alpha;
    public Bitmap bitmap;
    public int width, height;
}
  • x,y是粒子的位置信息
  • startXV,startYV是X方向和Y方向的速度
  • angle是发散出去的角度
  • alpha是粒子的透明度
  • bitmap, width, height即粒子图片信息 我们在构造函数中初始化这些信息,给定一些默认值
public Particle(Bitmap originalBitmap) {
    alpha = 1;
    float scale = (float) Math.random() * 0.3f + 0.7f;
    width = (int) (originalBitmap.getWidth() * scale);
    height = (int) (originalBitmap.getHeight() * scale);
    bitmap = Bitmap.createScaledBitmap(originalBitmap, width, height, true);

    startXV = new Random().nextInt(150) * (new Random().nextBoolean() ? 1 : -1);
    startYV = new Random().nextInt(170) * (new Random().nextBoolean() ? 1 : -1);
    int i = new Random().nextInt(360);
    angle = (float) (i * Math.PI / 180);

    float rotate = (float) Math.random() * 180 - 90;
    Matrix matrix = new Matrix();
    matrix.setRotate(rotate);
    bitmap = Bitmap.createBitmap(bitmap, 0, 0, width, height, matrix, false);
    originalBitmap.recycle();
}

仔细看效果动画,会发现同一个图片每次出来的旋转角度会有不同,于是,在创建bitmap的时候我们随机旋转下图片。

绘制粒子

有了粒子之后,我们需要将其绘制在View上,定义一个ParticleView,重写onDraw()方法,完成绘制

public class ParticleView extends View {
    Paint paint;
    List<Particle> particles = new ArrayList<>();
    //.....省略构造函数
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for (Particle particle : particles) {
            paint.setAlpha((int) (particle.alpha * 255));
            canvas.drawBitmap(particle.bitmap, particle.x - particle.width / 2, particle.y - particle.height / 2, paint);
        }
    }
    public void setParticles(List<Particle> particles) {
        this.particles = particles;
    }
}

管理粒子

绘制的时候我们发现需要不断改变粒子的x,y值,才能使它动起来,所以我们需要一个ValueAnimator,然后通过监听动画执行情况,不断绘制粒子。

private void startAnimator(View emiter) {
    ValueAnimator valueAnimator = ObjectAnimator.ofInt(0, 1).setDuration(1000);
    valueAnimator.addUpdateListener(animation -> {
        for (Particle particle : particles) {
            particle.alpha = 1 - animation.getAnimatedFraction();
            float time = animation.getAnimatedFraction();
            time *= 10;
            particle.x = startX - (float) (particle.startXV * time * Math.cos(particle.angle));
            particle.y = startY - (float) (particle.startYV * time * Math.sin(particle.angle) - 9.8 * time * time / 2);
        }
        particleView.invalidate();
    });
    valueAnimator.start();
}

由于我们的点赞按钮经常出现在RecyclerView的item里面,而点赞动画又是全屏的,所以不可能写在item的xml文件里面,而且我们需要做到0侵入,在不改变原来的逻辑下添加动画效果。

我们可以通过activity.findViewById(android.R.id.content)获取FrameLayout然后向他添加子View

public ParticleManager(Activity activity, int[] drawableIds) {
    particles = new ArrayList<>();
    for (int drawableId : drawableIds) {
        particles.add(new Particle(BitmapFactory.decodeResource(activity.getResources(), drawableId)));
    }
    topView = activity.findViewById(android.R.id.content);
    topView.getLocationInWindow(parentLocation);
}

首先我们通过构造函数传入当前Activity以及我们需要的图片资源,然后将图片资源都解析成Particle对象,保存在particles中,然后获取topView以及他的位置信息。

然后需要知道动画从什么位置开始,传入一个view作为锚点

public void start(View emiter) {
    int[] location = new int[2];
    emiter.getLocationInWindow(location);
    startX = location[0] + emiter.getWidth() / 2 - parentLocation[0];
    startY = location[1] - parentLocation[1];
    particleView = new ParticleView(topView.getContext());
    topView.addView(particleView);
    particleView.setParticles(particles);
    startAnimator(emiter);
}

通过传入一个emiter,计算出起始位置信息并初始化particleView中的粒子信息,最后开启动画。

使用

val ids = ArrayList<Int>()
for (index in 1..18) {
    val id = resources.getIdentifier("img_like_$index", "mipmap", packageName);
    ids.add(id)
}
collectImage.setOnClickListener {
    ParticleManager(this, ids.toIntArray())
        .start(collectImage)
}

运行之后会发现基本和效果图一致,但是其实有个潜在的问题,我们只是向topView添加了view,并没有移除,虽然界面上看不到,其实只是因为我们的粒子在最后透明度都是0了,将粒子透明度最小值设置为0.1后运行会发现,动画结束之后粒子没有消失,会越堆积越多,所以我们还需要移除view。

valueAnimator.addListener(new AnimatorListenerAdapter() {
    @Override
    public void onAnimationStart(Animator animation, boolean isReverse) {
    }
    @Override
    public void onAnimationEnd(Animator animation) {
        topView.removeView(particleView);
        topView.postInvalidate();
    }
    @Override
    public void onAnimationCancel(Animator animation) {
        topView.removeView(particleView);
        topView.postInvalidate();
    }
});

移除的时机放在动画执行完成,所以继续使用之前的valueAnimator,监听他的完成事件,移除view,当然,如果动画取消了也需要移除。