在最新的美拍增加了一个直播功能,看了一下其点赞的效果还是很酷炫的,就自己实现了一个类似的,效果如下:
先说下实现该效果需要用到的知识点:
属性动画,这里只是用到了基本的属性动画,对于属性动画的详细介绍,可以参考郭大神的博客:
Android属性动画完全解析(上),初识属性动画的基本用法
Android属性动画完全解析(中),ValueAnimator和ObjectAnimator的高级用法
Android属性动画完全解析(下),Interpolator和ViewPropertyAnimator的用法
贝塞尔曲线, 贝塞尔曲线的学习可以参考
自定义控件其实很简单5/12
Android防360水波进度
先简单说下实现该效果的思路:
三阶贝塞尔曲线,有四个点,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方法绘制贝塞尔曲线的路劲,该类不是必须的,只是方便理解而添加的。
上面的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按照曲线路径滑动。
// 获取屏幕的宽度和高度
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,其实已经实现了一个基本的点赞效果了,我们现在需要做的有:
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();
/** * 添加点赞效果,实际就是动态为该布局中添加一个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,此时就实现了我们想要的效果了,可是目前每一个都动画效果的速度都是一样的,我们可以添加不同的插值器。
常见的差之器有如下九个:
我们随机选择一个差之器,应用到当前的动画中。
private Interpolator[] inteceptors = new Interpolator[]{new DecelerateInterpolator(),new LinearInterpolator()
,new OvershootInterpolator()};
valueAnimator.setInterpolator(inteceptors[random.nextInt(inteceptors.length)]);
ok,到这里完整的效果都已经实现,下面附上源码连接
点赞效果源码