之前玩淘宝误入它的直播频道,发现它的直播界面的点赞效果挺好看,然后发现QQ控件点赞有类似动画,于是趁有空花了点时间玩玩。

先上个效果图:

直播点赞控件Android 直播点赞神器_自定义View


添加了一个按钮模拟点赞,点击多少次就出现多个水果,他们的运动轨迹和速度是不一样的,而且带有淡入淡出效果。这是淘宝直播的效果,qq空间是点击一次就出现好多个的,修改一点逻辑也能实现对应的效果。

gif图看起来有点不流畅,因为录制时锁定的帧率避免超2M不能上传,实际运行是流畅的。


因为不是做成一个通用控件,所以我也就实现了效果,大家如果要用,可以自己加更多自定义内容。实现起来挺简单的,也就不啰嗦了

用到的知识点:贝塞尔公式(三阶)、属性动画、动画集合、自动义估值器

先丢代码再说实现过程:

代码:

package cn.small_qi.transitiontest.diyview;

import android.animation.Animator;
import android.animation.AnimatorSet;
import android.animation.TypeEvaluator;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.Interpolator;
import android.view.animation.LinearInterpolator;
import android.widget.ImageView;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import cn.small_qi.transitiontest.R;
public class PressLikeView extends ViewGroup {
    private List<Integer> images;//图片
    private List<Interpolator> inters;//插值器
    private Random random;
    private int defaultSize = 150;//图片默认尺寸(px)
    public PressLikeView(Context context) {
        super(context);
    }
    public PressLikeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        initData();
    }
//初始化数据
    private void initData() {
        random =new Random();
        images = new ArrayList<>();
        inters = new ArrayList<>();
        images.add(R.drawable.a510209);
        images.add(R.drawable.a510213);
        images.add(R.drawable.a510216);
        images.add(R.drawable.a510222);
        images.add(R.drawable.a510225);
        images.add(R.drawable.a510234);
        //....
        inters.add(new LinearInterpolator());
        inters.add(new AccelerateInterpolator());
        inters.add(new AccelerateDecelerateInterpolator());
        inters.add(new DecelerateInterpolator());
        //....
    }

    public PressLikeView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  }
    //使用预置的随机图片
    public void  show(){//这个方法是开放出去的,就是按钮点击时调用,出现一个水果动画
        ImageView view = new ImageView(getContext());
        view.setImageResource(images.get(random.nextInt(images.size())));//随机设置一张图片
        view.setLayoutParams(new LayoutParams(defaultSize,defaultSize));//设置大小
        addView(view);//添加到容器中
        view.layout(getWidth()/2-defaultSize, (int) (getHeight()-defaultSize*1.5),getWidth()/2, (int) (getHeight()-0.5*defaultSize));//计算位置
        startAnim(view);//开始动画
    }
    //使用自定义的图片 -- 也可以修改成传入一个ImageView
    public void  show(Drawable drawable){
        ImageView view = new ImageView(getContext());
        view.setImageDrawable(drawable);
        view.setLayoutParams(new LayoutParams(defaultSize,defaultSize));
        addView(view);
        view.layout(getWidth()/2-defaultSize, (int) (getHeight()-defaultSize*1.5),getWidth()/2, (int) (getHeight()-0.5*defaultSize));
        startAnim(view);
    }

    private void startAnim(final ImageView view) {
        AnimatorSet animatorSet = new AnimatorSet();
        //淡入动画
        ValueAnimator inAnim = ValueAnimator.ofFloat(0.5f,1f);
        inAnim.setDuration(500);
        inAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                float value = (float) animation.getAnimatedValue();
                view.setAlpha(value);
                view.setScaleX(value);
                view.setY(value);
            }
        });
        //淡出动画
        ValueAnimator outAnim = ValueAnimator.ofFloat(1,0);
        outAnim.setDuration(1500);
        outAnim.setStartDelay(1500);//延迟启动,保证水果飞到一大半再淡出
        outAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                view.setAlpha((float) animation.getAnimatedValue());
            }
        });
        //位移动画
        ValueAnimator transAnim = ValueAnimator.ofObject(new BezierValue(),new Point(getWidth()/2,getHeight()),new Point(new Random().nextInt(getWidth()),0));
        transAnim.setDuration(3000);
        transAnim.setInterpolator(inters.get(random.nextInt(inters.size())));//随机设置插值器
        transAnim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                Point point = (Point) animation.getAnimatedValue();
                view.setX(point.x);
                view.setY(point.y);
            }
        });
        //组合动画
        //三个动画同时执行
        animatorSet.playTogether(inAnim,transAnim,outAnim);
        //前面两个动画延迟执行,最后一个同时执行
        /*animatorSet.playSequentially(inAnim,transAnim);
        animatorSet.play(outAnim);*/
        animatorSet.start();
        animatorSet.addListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) { }
            @Override
            public void onAnimationEnd(Animator animation) {
                removeView(view);//动画结束移除ImageView
            }
            @Override
            public void onAnimationCancel(Animator animation) { }
            @Override
            public void onAnimationRepeat(Animator animation) { }
        });
    }
    //自定义插值器
    class BezierValue implements TypeEvaluator<Point>{
        private Random random =new Random();
        private int ctrlPX1, ctrlPX2,ctrlPY1, ctrlPY2;
        private boolean isInit;//只需要初始化一次
        @Override
        public Point evaluate(float fraction, Point startValue, Point endValue) {
            Point point = new Point();
            point.x = (int) cubicPointX(fraction,startValue.x,endValue.x);
            point.y = (int) cubicPointY(fraction,startValue.y,endValue.y);
            return point;
        }
        //贝塞尔计算x
        private double cubicPointX(float fraction, int start, int end){
            if (!isInit){
                //初始化控制点y左边
                ctrlPY1 = random.nextInt(start+end/2);
                ctrlPY2 = random.nextInt(start+end/2)+(start+end/2);
                //初始化控制点x坐标
                if (random.nextBoolean()){//先左后右
                    ctrlPX1 = (int) (random.nextInt(start)-start/4f);//减去start/4 是为了运动曲线更明显
                    ctrlPX2 = (int) (random.nextInt(start)+start*1.25f);//start是宽度的一半,为了保证后面往右运动,应该是随机数加上start。现在乘1.25是为了让曲线更明显
                }else{//先右后左
                    ctrlPX1 = (int) (random.nextInt(start)+start*1.25f);
                    ctrlPX2 = (int) (random.nextInt(start)-start/4f);
                }
                isInit =true;
            }
           return start*Math.pow((1-fraction),3)+3* ctrlPX1 *fraction*Math.pow((1-fraction),2)
                    +3* ctrlPX2 *Math.pow(fraction,2)*(1-fraction)+end*Math.pow(fraction,3);
        }
        //贝塞尔计算y
        private double cubicPointY(float fraction, int start, int end){
            return start*Math.pow((1-fraction),3)+3* ctrlPY1 *fraction*Math.pow((1-fraction),2)
                    +3* ctrlPY2 *Math.pow(fraction,2)*(1-fraction)+end*Math.pow(fraction,3);
        }
    }
}



其实就是

1.自定义一个ViewGroup,然后每点击一次就往里面添加一个ImageView,然后设置好它的位置和大小

2.为每一个ImageView设置随机的图片,然后对其执行一些列动画

3.为了某些效果加入了淡入淡出动画,相信大家都熟悉,根据自己需求来决定怎么写就行了

4.主要是运动轨迹动画,为了路径不是单纯的直线或者简单的曲线,所以用了自定义估值器配合三阶的贝塞尔实现运动路径

5.为了实现运动速度的不规则,内置了4种插值器,然后每次随机取一个,用在运动轨迹的动画上


可能有点难看懂的是我位置的计算和贝塞尔控制点的计算,我这里简要说明一下。

初始位置我本来是是要底部居中,但是稍微做了点偏移,计算位置如图所示:

直播点赞控件Android 直播点赞神器_自定义View_02


而贝塞尔两个控制点和终点的选择,我是这么计算的,大家结合代码看就看得懂了:

直播点赞控件Android 直播点赞神器_动画_03


计算好动画之后,接下来就不难了!执行动画就好了,这样就实现的这种效果拉!


--本文结束,谢谢大家阅读