在最新的美拍增加了一个直播功能,看了一下其点赞的效果还是很酷炫的,就自己实现了一个类似的,效果如下:
先说下实现该效果需要用到的知识点:
- 属性动画,这里只是用到了基本的属性动画,对于属性动画的详细介绍,可以参考郭大神的博客: Android属性动画完全解析(上),初识属性动画的基本用法Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法 Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
- 贝塞尔曲线, 贝塞尔曲线的学习可以参考自定义控件其实很简单5/12 Android防360水波进度
实现思路
先简单说下实现该效果的思路:
- 定义一个Layout,每次点赞的时候,动态创建一个ImageView,添加到该布局中,然后使用属性动画来改变其透明度和大小
- 是当前添加的ImageView沿着三阶贝塞尔曲线的路径滑动,当然,这里为了做到随机效果,每次随机改变两个控制点的坐标
##三阶贝塞尔的实现##
三阶贝塞尔曲线,有四个点,P0、P1、P2、P3 ,这里P0和P3表示起始点和终止点,P1和P2表示的是两个控制点,这两个控制主要决定了该曲线的路径。如下图:
公式如下:
###描绘曲线路径###
这里,我创建一个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);
此时效果如下:
##实现点赞效果##
上面一个基本的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)]);