使用属性动画实现直播送花效果
直播点赞送花,双击666,这种效果的使用场景越来越普遍。实现的方法可以使用自定义控件或者属性动画,这次本人使用后者来实现了送花效果,主要是考虑到属性动画的强大和简便的使用性。不多说,先上图看下效果。
demo.gif下面来分析一下本人实现的代码:
private PointF startPoint;
private PointF endPoint;
private Drawable[] drawables;
private Interpolator[] interpolators;
private FrameLayout rootView;
startPoint 作为屏幕下方的动画起始点,endPoint是屏幕最上方的动画终点,为了使点赞花朵样式能够多样化,生动化,我们定义了drawables作为存放所有花朵的Drawable图片数组,同理,为了使鲜花的运动轨迹更随机,我们定义了interpolators作为是一个存放所有插值器的数组。插值器在Android的属性动画中是一个非常重要的类,它控制了动画运动过程中的变化情况,比如加速,加速,先加速后减速等。父容器选用FrameLayout 而不是 LinearLayout,原因是在花运行到屏幕上方将其删除时,不会由于布局的变化而发生闪屏的现象。
下面问题来了,这个动画的重点是如何才能使每朵花按照随机的曲线轨迹进行运动,这里要利用估值器和贝塞尔曲线的知识。我们先定义一个估值器,然后在实现它的evaluate方法,在里面利用贝塞尔曲线的公式来计算出fraction时刻,曲线的运动坐标PointF 。
class MyTypeEvaluator implements TypeEvaluator<PointF> {
private PointF pointF1, pointF2;
public MyTypeEvaluator(PointF pointF1, PointF pointF2) {
this.pointF1 = pointF1;
this.pointF2 = pointF2;
}
@Override
public PointF evaluate(float fraction, PointF startValue, PointF endValue) {
//三阶贝塞尔曲线
//B(t) = P0 * (1-t)^3 + 3 * P1 * (1-t)^2 + 3 * P2 * t^2 * (1-t) + P3 * t^3 ,其中 0 <= t <= 1
float timeLeft = 1.0f - fraction;
PointF pointF = new PointF();//结果
pointF.x = timeLeft * timeLeft * timeLeft * (startValue.x)
+ 3 * timeLeft * timeLeft * fraction * (pointF1.x)
+ 3 * timeLeft * fraction * fraction * (pointF2.x)
+ fraction * fraction * fraction * (endValue.x);
pointF.y = timeLeft * timeLeft * timeLeft * (startValue.y)
+ 3 * timeLeft * timeLeft * fraction * (pointF1.y)
+ 3 * timeLeft * fraction * fraction * (pointF2.y)
+ fraction * fraction * fraction * (endValue.y);
return pointF;
}
}
evaluate方法中的参数fraction代表,物体做曲线运动时的某一时刻所占全部时间的百分比,例如刚开始运动时fraction等于0,运动到终点时fraction等于1。因此通过重写evaluate方法,我们就可以计算出某一时刻,物体在屏幕上的运动位置,并将该运动位置储存在pointF中。
然而光有运动路径上的点还是不够的,我们还需要将其变成动画,新手往往遇到一个复杂的动画时感觉无从下手,其实不用担心,任何复杂的动画都是可以通过分解成若干的基础动画,比如透明度渐变,缩放,旋转,平移等。下面我们来分析一下这个送花的动画由哪几部分动画组成的。
private void startAnin(final ImageView flower) {
final AnimatorSet animatorSet = new AnimatorSet();
ObjectAnimator alphaAnim = ObjectAnimator.ofFloat(flower, "alpha", 1.0f, 0.3f);
alphaAnim.setDuration(400);
ObjectAnimator scaleAnimX = ObjectAnimator.ofFloat(flower, "scaleX", 0.4f, 1.0f);
ObjectAnimator scaleAnimY = ObjectAnimator.ofFloat(flower, "scaleY", 0.4f, 1.0f);
scaleAnimX.setDuration(1800);
scaleAnimY.setDuration(1800);
final ValueAnimator animator = ValueAnimator.ofObject(new MyTypeEvaluator(getPoint(0), getPoint(1)), startPoint, endPoint);
animator.setDuration(4000);
animator.setInterpolator(new AccelerateDecelerateInterpolator());
animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
PointF pointF = (PointF) animation.getAnimatedValue();
flower.setX(pointF.x);
flower.setY(pointF.y);
}
});
// animator.start();
animatorSet.play(animator);
animatorSet.play(scaleAnimX).with(scaleAnimY).before(alphaAnim);
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
rootView.removeView(flower);
}
});
animatorSet.start();
}
1)花从不透明变成透明是第一个动画,我们定义为 alphaAnim
2)花从小变成大是第二个动画,我们定义为 scaleAnimX ,scaleAnimY
3)花本身的曲线位移是第三个动画,我们定义为 animator
以上是开启动画的代码,给 animator 设置监听事件,在 回调方法中onAnimationUpdate 中,我们将花的坐标进行变换。这里详细补充一点,属性动画的原理究竟是什么?通过阅读源码,属性动画本质上是通过反射来更改View的属性值,如TranslationX,alpha等,由于属性动画是实实在在得修改了View的属性,因此在动画过程中依然能够相应点击事件。在之后系统会不断去调用View的invalidate来重绘屏幕界面(16ms为间隔)形成动画。由于属性动画需要使用反射并且需要不断修改View的属性,在性能上比起补间动画和帧动画会有些许损耗。16ms是系统的刷新时间,因此如果在16ms内未完成View的重绘,就会造成丢帧,使画面显得十分卡顿。
@Override
public void onAnimationUpdate(ValueAnimator animation) {
PointF pointF = (PointF) animation.getAnimatedValue();
flower.setX(pointF.x);
flower.setY(pointF.y);
}
同时为了提高性能,在花运行到终点的时候,将其从父容器中删除。
animatorSet.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
rootView.removeView(flower);
}
});
在父容器中添加一朵花并开启动画,初始化花的位置
/**
* 添加花朵
*/
private void addFlower() {
ImageView flower = new ImageView(this);
flower.setLayoutParams(new ViewGroup.LayoutParams(100, 100));
flower.setBackground(drawables[new Random().nextInt(drawables.length)]);
flower.setX(startPoint.x);
flower.setY(startPoint.y);
rootView.addView(flower);
startAnin(flower);
}
最后需要在在按钮被点击时,在父容器增加一朵花,并开启动画,整个代码就完成了。
btn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
addFlower();
}
});