在最新的美拍增加了一个直播功能,看了一下其点赞的效果还是很酷炫的,就自己实现了一个类似的,效果如下:

Android 开发点赞 安卓手机点赞_Android 开发点赞


Android 开发点赞 安卓手机点赞_android_02


先说下实现该效果需要用到的知识点:

  • 属性动画,这里只是用到了基本的属性动画,对于属性动画的详细介绍,可以参考郭大神的博客: Android属性动画完全解析(上),初识属性动画的基本用法Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法 Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
  • 贝塞尔曲线, 贝塞尔曲线的学习可以参考自定义控件其实很简单5/12 Android防360水波进度

实现思路

先简单说下实现该效果的思路:

  • 定义一个Layout,每次点赞的时候,动态创建一个ImageView,添加到该布局中,然后使用属性动画来改变其透明度和大小
  • 是当前添加的ImageView沿着三阶贝塞尔曲线的路径滑动,当然,这里为了做到随机效果,每次随机改变两个控制点的坐标

##三阶贝塞尔的实现##

三阶贝塞尔曲线,有四个点,P0、P1、P2、P3 ,这里P0和P3表示起始点和终止点,P1和P2表示的是两个控制点,这两个控制主要决定了该曲线的路径。如下图:

Android 开发点赞 安卓手机点赞_动画_03


公式如下:

Android 开发点赞 安卓手机点赞_Android 开发点赞_04


###描绘曲线路径###

这里,我创建一个BezierCurveView.java用来绘制三阶贝塞尔曲线的路径

public class BezierCurveView extends View{
	
	private static final String TAG = "BezierCurveView";
		
	private Paint paint;
	
	private Path path;
	
	public BezierCurveView(Context context, AttributeSet attrs){
		super(context,attrs);
		init();
	}
	
	public BezierCurveView(Context context){
		super(context);
		init();
	}
	
	private void init(){		
		paint = new Paint();
		paint.setColor(Color.RED);
		paint.setAntiAlias(true);
		paint.setStyle(Style.STROKE);
		paint.setStrokeWidth(1);
		
		path = new Path();
	}
	
	public void onMeasure(int widthMeasureSpec, int heightMeasureSpec){
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		Log.v(TAG, "width = " + MeasureSpec.getSize(widthMeasureSpec) + "and height = " + MeasureSpec.getSize(heightMeasureSpec));
	}
		
	public void onDraw(Canvas canvas){
		canvas.drawColor(Color.WHITE);
		path.reset();
		path.moveTo(0, 0);
		// 可以看到这类的控制点的坐标:
		// p1 = getMeasuredWidth(), 0
		// p2 = 0, getMeasuredHeight()
		path.cubicTo(getMeasuredWidth(), 0, 0, getMeasuredHeight(),getMeasuredWidth(), getMeasuredHeight());
		canvas.drawPath(path, paint);
	}

}

代码比较简单,主要调用Path类的cubicTo方法绘制贝塞尔曲线的路劲,该类不是必须的,只是方便理解而添加的。
###自定义TypeEvaluator####

上面的BezierCurveView只是用来绘制一条三阶贝塞尔曲线的路径,而我们想要让某一个view滑动的路径是该曲线的话,就需要自定义一个TypeEvaluator,并且使用到了上面的公式。TypeEvaluator会接受第一步中算出来的比例因子,然后算出当前的属性的值,将其返回给ValuaAnimator。

由于需要使当前组件按照Bezier曲线的路径来滑动,这里需要的泛型类型就是PointF了。

class BezierEvaluator implements TypeEvaluator<PointF> {

        @Override
        public PointF evaluate(float fraction, PointF startValue,
                               PointF endValue) {
            final float t = fraction;
            float oneMinusT = 1.0f - t;
            PointF point = new PointF();
			// 起始点
            PointF point0 = (PointF)startValue;
			// 第一个控制点坐标
            PointF point1 = new PointF();
            point1.set(width, 0);
			// 第二个控制点坐标
            PointF point2 = new PointF();
            point2.set(0, height);
			// 终点坐标
            PointF point3 = (PointF)endValue;

            point.x = oneMinusT * oneMinusT * oneMinusT * (point0.x)
                    + 3 * oneMinusT * oneMinusT * t * (point1.x)
                    + 3 * oneMinusT * t * t * (point2.x)
                    + t * t * t * (point3.x);

            point.y = oneMinusT * oneMinusT * oneMinusT * (point0.y)
                    + 3 * oneMinusT * oneMinusT * t * (point1.y)
                    + 3 * oneMinusT * t * t * (point2.y)
                    + t * t * t * (point3.y);
            return point;
        }
    }

###为button添加属性动画###
下面使用属性动画,使当前button按照曲线路径滑动。

// 获取屏幕的宽度和高度
DisplayMetrics dm = new DisplayMetrics();       getWindowManager().getDefaultDisplay().getMetrics(dm);
width = dm.widthPixels;
height = dm.heightPixels;

final FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                valueAnimator.start();
            }
        });
// 创建属性动画,第一个参数就是我们创建的TypeEvaluator,后面两个分别表示起始点和终止点      
valueAnimator = ValueAnimator.ofObject(new BezierEvaluator(), new PointF(0,0),new PointF(width,height));
        valueAnimator.setDuration(2000);
        // 为当前动画添加监听,根据BezierEvaluator计算的值,实时更新当前button的位置,这里由于我们使用的是贝塞尔曲线的路径,所有button也会按照该路径来滑动
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF)animation.getAnimatedValue();
                fab.setX(pointF.x);
                fab.setY(pointF.y);
            }
        });
        valueAnimator.setTarget(fab);
        valueAnimator.setRepeatCount(1);
        valueAnimator.setRepeatMode(ValueAnimator.REVERSE);

此时效果如下:

Android 开发点赞 安卓手机点赞_动画_05

##实现点赞效果##
上面一个基本的demo,其实已经实现了一个基本的点赞效果了,我们现在需要做的有:

  • 自定义一个布局,每次点赞的时候,动态创建一个ImageView,并且添加到该布局中。
  • 当ImageView添加至该布局以后,使用属性动画,控制当前ImageView的滑动轨迹按照和上面类似的曲线来滑动,控制ImageView的大小和透明度

MeipaiLayout.java

每当点击一次赞按钮的时候添加一个ImageView到该MeipaiLayout中,下面看下其属性:

private int[] likeArray = new int[] {R.drawable.ic_praise_sm1,R.drawable.ic_praise_sm2,R.drawable.ic_praise_sm3,
            R.drawable.ic_praise_sm4,R.drawable.ic_praise_sm5,R.drawable.ic_praise_sm6,
        R.drawable.ic_praise_big3,R.drawable.ic_praise_big4,R.drawable.ic_praise_big5,R.drawable.ic_praise_big6,R.drawable.ic_praise_big7
            ,R.drawable.ic_praise_big8};
    private static final String TAG = MeipaiLayout.class.getSimpleName();
    // 布局的宽度和高度
    private int mLayoutWidth;
    private int mLayoutHeight;
    // 属性动画的时间
    private static final int DURATION = 3000;
    // 图片的宽度和高度
    private int mImageWidth;
    private int mImageHeight;
    // 大图片和小图片的大小
    private static final int BIG_SIZE = 128;
    private static final int SMALL_SIZE = 64;
    private Random random = new Random();
  • 添加addImageView方法
    addImageView方法就是动态构造一个ImageView并且添加到当前MeipaiLayout中
/**
     * 添加点赞效果,实际就是动态为该布局中添加一个ImageView,并且使用动画来显示
     */
    public void addImageView() {
        //随机选一个Image
        final ImageView imageView = new ImageView(getContext());
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(),likeArray[random.nextInt(likeArray.length)]);
        imageView.setImageBitmap(bitmap);
        // 获取当前图片的宽度和高度
        ViewTreeObserver vto = imageView.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                imageView.getViewTreeObserver().removeGlobalOnLayoutListener(this);
                // 获取当前图片的宽度和高度
                mImageWidth = imageView.getWidth();
                mImageHeight = imageView.getHeight();
                Log.d(TAG,"the mImageWidth is :"+mImageWidth+"===the mImageHeight is :"+mImageHeight);
            }
        });
        LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,ViewGroup.LayoutParams.WRAP_CONTENT);
        // 添加当前ImageView
        addView(imageView,params);

        // 下面定义三个动画,分别用来设置当前ImageView的透明度和大小
        ObjectAnimator alpha = ObjectAnimator.ofFloat(imageView,View.ALPHA, 1f, 0f);
        ObjectAnimator scaleX;
        ObjectAnimator scaleY;
        if (mImageWidth == BIG_SIZE) {
            scaleX = ObjectAnimator.ofFloat(imageView,View.SCALE_X, 0.5f, 1.5f);
            scaleY = ObjectAnimator.ofFloat(imageView,View.SCALE_Y, 0.5f, 1.5f);
        } else {
            scaleX = ObjectAnimator.ofFloat(imageView,View.SCALE_X, 0.5f, 1.2f);
            scaleY = ObjectAnimator.ofFloat(imageView,View.SCALE_Y, 0.5f, 1.2f);
        }
        // 使用三阶贝塞尔曲线来控制当前ImageView的位置
        ValueAnimator valueAnimator = ObjectAnimator.ofObject(new BezierEvaluator(), new PointF((mLayoutWidth - 64) / 2,mLayoutHeight - 64),new PointF(random.nextInt(mLayoutWidth - 64),random.nextInt(64)));
        valueAnimator.setDuration(5000);
        valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
                PointF pointF = (PointF)animation.getAnimatedValue();
                imageView.setX(pointF.x);
                imageView.setY(pointF.y);
            }
        });
        valueAnimator.setTarget(imageView);
        valueAnimator.setRepeatCount(1);
        valueAnimator.setRepeatMode(ValueAnimator.REVERSE);
        valueAnimator.start();

        // 控制当前ImageView的大小和透明度
        AnimatorSet animatorSet = new AnimatorSet();
        animatorSet.setDuration(5000);
        animatorSet.setInterpolator(new LinearInterpolator());
        animatorSet.playTogether(alpha,scaleX,scaleY);
        animatorSet.setTarget(imageView);
        animatorSet.addListener(new AnimatorListenerAdapter() {
            @Override
            public void onAnimationEnd(Animator animation) {
                super.onAnimationEnd(animation);
                // 在等动画结束时候移除该view
                removeView(imageView);
            }
        });
        animatorSet.start();
    }

上面最重要的就是使用到的BezierEvaluator,它就是控制当前ImageView滚动的轨迹,这类我们设置的是三阶贝塞尔曲线,并且每一个ImageView对应的两个控制点:p1和p2都是不同的,所以就会有随机滑动的效果。

BezierEvaluator.java

class BezierEvaluator implements TypeEvaluator<PointF> {

        private PointF point1 = new PointF();
        private PointF point2 = new PointF();

        public BezierEvaluator() {
            // 这里由于需要每一个新创建的ImageView按照不同的曲线来运动,所以通过random随机生成,这里的范围可以自己定义
            point1.set(random.nextInt((mLayoutWidth - SMALL_SIZE) / 2), random.nextInt(SMALL_SIZE));
            point2.set(random.nextInt((mLayoutWidth + SMALL_SIZE) / 2), random.nextInt(mLayoutHeight - SMALL_SIZE));
        }

        @Override
        public PointF evaluate(float fraction, PointF startValue,
                               PointF endValue) {
            final float t = fraction;
            float oneMinusT = 1.0f - t;
            PointF point = new PointF();

            // p0表示起始点
            PointF point0 = (PointF)startValue;
            // p3表示终止点
            PointF point3 = (PointF)endValue;

            point.x = oneMinusT * oneMinusT * oneMinusT * (point0.x)
                    + 3 * oneMinusT * oneMinusT * t * (point1.x)
                    + 3 * oneMinusT * t * t * (point2.x)
                    + t * t * t * (point3.x);

            point.y = oneMinusT * oneMinusT * oneMinusT * (point0.y)
                    + 3 * oneMinusT * oneMinusT * t * (point1.y)
                    + 3 * oneMinusT * t * t * (point2.y)
                    + t * t * t * (point3.y);
            return point;
        }
    }

ok,此时就实现了我们想要的效果了,可是目前每一个都动画效果的速度都是一样的,我们可以添加不同的插值器。

##设置不同的插值器##
常见的差之器有如下九个:

  • AccelerateDecelerateInterpolator
    在动画开始与介绍的地方速率改变比较慢,在中间的时候加速
  • AccelerateInterpolator
    在动画开始的地方速率改变比较慢,然后开始加速
  • AnticipateInterpolator
    开始的时候向后然后向前甩
  • AnticipateOvershootInterpolator
    开始的时候向后然后向前甩一定值后返回最后的值
  • BounceInterpolator
    动画结束的时候弹起
  • CycleInterpolator
    动画循环播放特定的次数,速率改变沿着正弦曲线
  • DecelerateInterpolator
    在动画开始的地方快然后慢
  • LinearInterpolator
    匀速
  • OvershootInterpolator
    向前甩一定值后再回到原来位置

我们随机选择一个差之器,应用到当前的动画中。

private Interpolator[] inteceptors = new Interpolator[]{new DecelerateInterpolator(),new LinearInterpolator()
                                        ,new OvershootInterpolator()};

valueAnimator.setInterpolator(inteceptors[random.nextInt(inteceptors.length)]);