参考链接:
属性动画简介(一)属性动画的实现和常用方法
属性动画简介(二)属性动画的源码分析
属性动画简介(三)ObjectAnimator中属性的自动设置解析
老张我最近疯狂迷恋黄龄,因为偶然听了她唱的《牵丝戏》,真的好听,单曲循环一天后,忍不住为黄龄女士疯狂打call,怎么打call呢? 写个动效送给她吧
翻看抖音看了一下抖音直播的动效,感觉效果还行,可以参考抖音的动效加以变换写一下,
抖音动效如下图:
抖音点赞.jpg
首先构思一下动效的一些关键元素
- 四张图片从10个图片资源中随机获取,按照不同的轨迹向上飘动
- 三种不同的轨迹,分别是向左边画一个圆弧,向右边画一个圆弧,画一个S形
- 四张图片的飘动在点赞事件发生后0-500ms内随机开始
- 最终飘动到同一位置消失
- 终点和飘动范围可控
- 动画运行在点赞控件上方,点赞后消失
我们尝试使用属性动画实现这一需求
先做一下分析:
1.四张图片要在500ms内随机启动,因此需要四个动画
2.因为四个动画互不干扰,因此需要一个数据结构保存这四个动画的属性
3.半圆弧(C形弧)和S形弧分别是办个Sin函数周期和一个Sin函数周期
4.动画控件的消失要在四个动画全部执行完成后
接着看如何实现:
首先定义一个PraiseView继承于View
PraiseView的主要元素:
四个动画我们用List<ValueAnimator>来保存,500ms随机启动,可以使用Handler的延时消失
private List<ValueAnimator> animatorList;\
定义一个类AnimDrawable来保存每个动画的属性
private class AnimDrawable {
Bitmap bitmap; // 需要向上飘动的Bitmap
Rect srcRest; // Bitmap sourceRect
Rect dstRect; // Bitmap 位置rect
int originLeft; // 起始位置
int originRight; // 起始位置
int originTop; // 起始位置
int originBottom; // 起始位置
Paint paint; // 起始位置
public AnimDrawable(Bitmap bitmap, Rect srcRest, Rect dstRect, Paint paint) {
this.bitmap = bitmap;
this.srcRest = srcRest;
this.dstRect = dstRect;
originLeft = dstRect.left;
originRight = dstRect.right;
originTop = dstRect.top;
originBottom = dstRect.bottom;
this.paint = paint;
}
}
初始化数据:
private void init() {
bitmapSourceArray = new int[10];
bitmapSourceArray[0] = R.drawable.ic_live_room_like1;
bitmapSourceArray[1] = R.drawable.ic_live_room_like2;
bitmapSourceArray[2] = R.drawable.ic_live_room_like3;
bitmapSourceArray[3] = R.drawable.ic_live_room_like4;
bitmapSourceArray[4] = R.drawable.ic_live_room_like5;
bitmapSourceArray[5] = R.drawable.ic_live_room_like6;
bitmapSourceArray[6] = R.drawable.ic_live_room_like7;
bitmapSourceArray[7] = R.drawable.ic_live_room_like8;
bitmapSourceArray[8] = R.drawable.ic_live_room_like9;
bitmapSourceArray[9] = R.drawable.ic_live_room_like10;
drawableList = new ArrayList<>();
animatorList = new ArrayList<>();
handler = new MyHandler(this);
}
从外部设置View的宽高:
public void setPraiseViewSize(int width, int height) {
this.width = width;
this.height = height;
}
重写onMeasure保持View的宽高:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthSpec = MeasureSpec.getMode(widthMeasureSpec);
int heightSpec = MeasureSpec.getMode(heightMeasureSpec);
super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthSpec), MeasureSpec.makeMeasureSpec(height, heightSpec));
}
定义一个Handler来启动动画:
/***
* Handler 用于启动动画
*/
private static class MyHandler extends Handler {
private WeakReference<PraiseView> holder;
public MyHandler(PraiseView view) {
holder = new WeakReference<>(view);
}
@Override
public void handleMessage(Message msg) {
if (holder.get() != null) {
int heartIndex = msg.getData().getInt("heartIndex");
int way = msg.getData().getInt("way");
holder.get().startAnim(heartIndex, way);
}
}
}
startAnim方法的实现
/****
* 启动动画
* @param heartIndex 图片元素的位置
* @param way 圆弧的路径 左弧 右弧 S弧
*/
private void startAnim(int heartIndex, int way) {
Bitmap bitmap = BitmapFactory.decodeResource(this.getContext().getResources(), bitmapSourceArray[heartIndex]);
int bitmapHeight = bitmap.getHeight();
int bitmapWidth = bitmap.getWidth();
AnimDrawable animDrawable;
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
if (way == WAY_CENTER) {
// 生成S弧的初始位置保存在AnimDrawable中
animDrawable = new AnimDrawable(bitmap, new Rect(0, 0, bitmapWidth, bitmapHeight), new Rect((width - ScreenUtils.dp2px(getContext(),22.5f)) / 2
, height - ScreenUtils.dp2px(getContext(),22.5f), (width + ScreenUtils.dp2px(getContext(),22.5f)) / 2, height), paint);
} else {
// // 生成C弧的初始位置保存在AnimDrawable中
animDrawable = new AnimDrawable(bitmap, new Rect(0, 0, bitmapWidth, bitmapHeight), new Rect((width) / 2, height - ScreenUtils.dp2px(getContext(),22.5f)
, (width) / 2 + ScreenUtils.dp2px(getContext(),22.5f), height), paint);
}
drawableList.add(animDrawable);
// 这个值代表不同动画宽度变化的最大宽度
int temMaxWidth = width / 2;
// 这个值代表不同动画高度变化的最大值
int maxHeight = height - ScreenUtils.dp2px(getContext(),22.5f);
float value = 360f;
if (way == WAY_CENTER) {
// S弧Sin函数走一个完整的圆
value = 360f;
temMaxWidth = (width - ScreenUtils.dp2px(getContext(),22.5f)) / 2;
} else if (way == WAY_LEFT) {
// 左C弧Sin函数走一个半圆
value = 180f;
temMaxWidth = width / 2;
} else if (way == WAY_RIGHT) {
// 右C弧Sin函数走一个半圆
value = 180f;
temMaxWidth = width / 2 - ScreenUtils.dp2px(getContext(),22.5f);
}
int maxWidth = temMaxWidth;
float animValue = value;
ValueAnimator animator = ValueAnimator.ofFloat(animValue);
animator.setDuration(1000);
animator.addUpdateListener(animation -> {
float tempValue = (float) animation.getAnimatedValue();
// 动画图片的展示纵坐标由初始位置减移动的height决定,当 tempValue = animValue时,正好为animDrawable.originTop - maxHeight
animDrawable.dstRect.top = animDrawable.originTop - (int) (tempValue * (maxHeight / animValue));
animDrawable.dstRect.bottom = animDrawable.originBottom - (int) (tempValue * (maxHeight / animValue));
if (way == WAY_RIGHT) {
//右弧动画图片的展示横坐标由 初始位置加sin(Math.toRadians(tempValue) 决定
animDrawable.dstRect.left = animDrawable.originLeft + (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
animDrawable.dstRect.right = animDrawable.originRight + (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
} else {
// 左弧动画图片的展示横坐标由 初始位置减sin(Math.toRadians(tempValue) 决定
animDrawable.dstRect.left = animDrawable.originLeft - (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
animDrawable.dstRect.right = animDrawable.originRight - (int) (maxWidth * Math.sin(Math.toRadians(tempValue)));
}
// 透明度从 255 到 0
animDrawable.paint.setAlpha(255 - (int)(255 * tempValue / animValue));
invalidate();
});
animator.addListener(new Animator.AnimatorListener() {
@Override
public void onAnimationStart(Animator animation) {
}
@Override
public void onAnimationEnd(Animator animation) {
animCount++;
if (animCount >= COUNT) {
if (animEndListener != null) {
// 当 COUNT 数的动画全部执行完成,执行动画完成的回调
animEndListener.onAnimEnd();
}
}
}
@Override
public void onAnimationCancel(Animator animation) {
animCount++;
}
@Override
public void onAnimationRepeat(Animator animation) {
}
});
animator.start();
}
定义一个PraiseLayout, 宽高均为MathParent 作为承载动画的容器
public class PraiseLayout extends RelativeLayout {
private PraiseView praiseView;
private int width;
private int height;
public PraiseLayout(Context context) {
super(context);
init();
}
public PraiseLayout(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public PraiseLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
LayoutInflater.from(getContext()).inflate(R.layout.layout_praise, this, true);
praiseView = findViewById(R.id.praiseView);
// 动画完成事件的监听
praiseView.setOnAnimEndListener(() -> hide());
}
/***
* 从外部获取点赞View的宽高
* @param width
* @param height
*/
public void setPraiseViewSize(int width, int height) {
this.width = width;
this.height = height;
praiseView.setPraiseViewSize(width, height);
}
public void show(View anchor) {
// 定义一个int数组用来记录点赞View的位置
int[] location = new int[2];
// 获取点赞View的位置
anchor.getLocationInWindow(location);
// 获取点赞View的LayoutParams
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) praiseView.getLayoutParams();
// 设置praiseView的位置
params.leftMargin = location[0] - (width - anchor.getWidth()) / 2;
params.topMargin = location[1] - height;
praiseView.setLayoutParams(params);
// 将本View添加到DecoreView中
((ViewGroup)((Activity)getContext()).getWindow().getDecorView()).addView(PraiseLayout.this);
praiseView.show();
}
private void hide() {
// 将本View从DecoreView中移除
((ViewGroup)((Activity)getContext()).getWindow().getDecorView()).removeView(this);
}
}
使用方式:
在Activity中设置点赞按钮的监听:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button praiseBtn = findViewById(R.id.praiseBtn);
praiseBtn.setOnClickListener(v -> {
PraiseLayout layout = new PraiseLayout(this);
layout.setPraiseViewSize(ScreenUtils.dp2px(this, 60), ScreenUtils.dp2px(this, 200));
layout.show(praiseBtn);
});
}
效果:
yellow_zero_call[00_00_04--00_00_24].gif
为龄龄子女士疯狂打call任务完成
源码:
网友评论