android实现防美拍点赞效果
在最新的美拍增加了一个直播功能,看了一下其点赞的效果还是很酷炫的,就自己实现了一个类似的,效果如下:
先说下实现该效果需要用到的知识点:
属性动画,这里只是用到了基本的属性动画,对于属性动画的详细介绍,可以参考郭大神的博客:
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)]);
ok,到这里完整的效果都已经实现,下面附上源码连接
点赞效果源码
更多相关文章
- Android-自定义滑动菜单(抽屉效果)
- Android的Activity切换动画特效库SwitchLayout,视图切换动画库,媲
- Android应用程序资源——Animation动画资源
- 使用点九图在Android Studio中实现与Axure设计图一致的阴影效果
- android仿网易云音乐引导页、仿书旗小说Flutter版、ViewPager切
- 重置Android中的帧动画播放
- Android中给listview/gridview设置动画(逐条加载条目动画)
- Android旋转动画
- android之Tween动画