高仿马蜂窝旅游头像泡泡动画
2020-06-08 本文已影响0人
像程序一样思考
/ 前言 /
一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。
当pm制定完下一版本需求打开马蜂窝旅游app准备出去嗨一圈的时候 看到了马蜂窝旅游app的一个用户头像动画后。。。(=@__@=) 先看看效果图
效果分析:
- 涉及到有多个view在做动画操作 这里需要继承FrameLayout来左父布局 供图片做动画操作
- 每个子view的动画路径类似于S型 我这里采用的是三阶贝塞尔曲线和PathMeasure来完成动画运动路径的封装
- 每个子view动画执行完后 是移除添加新的view进来 还是回收重新利用 本案例是直接移除再添加新的(回收重新利用还没来得及去考虑该怎么写)
- 动画是循环不停的播放 我采用的是RxJava timer()操作符 不断的发送随机延迟消息去通知动画的执行
- 最后就剩下一些停止动画操作的开关设定
/ 实现步骤 /
1. 一些基本的初始化工作
public class HeadBubbleView extends FrameLayout {
//这个position很重要 不断的取出图片资源 靠它累加完成的
private int position = 0;
public HeadBubbleView(@NonNull Context context) {
this(context,null);
}
public HeadBubbleView(Context context, AttributeSet attrs) {
super(context, attrs);
mContext = context;
setFocusable(false);
//三阶贝塞尔曲线控制点一
controlPointOne = new Point();
//三阶贝塞尔曲线控制点二
controlPointTwo = new Point();
//每个子view的宽高是固定的
viewWidth = viewHeight = SizeUtils.dp2px(context, 22);
marginLeft = SizeUtils.dp2px(context, 15);
marginBot = SizeUtils.dp2px(context, 21);
//父View的高度也是固定的
height = SizeUtils.dp2px(context, 130);
//用于从PathMeasure 中不断的取出 曲线的路径值
pos = new float[2];
tan = new float[2];
initView();
}
2. 初始化的时候数据的加载状态
private void initView() {
//这个ImageView将不执行动画 用于底部不断切换图片展示
tempImageView = getImageView();
textView = getTextView();
initData(tempImageView);
}
//创建执行动画的具体角色
private ImageView getImageView() {
LayoutParams layoutParams = new LayoutParams(viewWidth, viewHeight);
ImageView roundedImageView = new ImageView(getContext());
roundedImageView.setScaleType(ImageView.ScaleType.FIT_XY);
layoutParams.gravity = Gravity.BOTTOM | Gravity.END;
layoutParams.setMargins(0, 0, marginLeft, marginBot);
addView(roundedImageView, layoutParams);
return roundedImageView;
}
//创建用于显示坐标xx来过的TextView
private TextView getTextView() {
int bottom = SizeUtils.dp2px(mContext, 23);
int right = SizeUtils.dp2px(mContext, 41);
LayoutParams layoutParams = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
layoutParams.gravity = Gravity.END | Gravity.BOTTOM;
layoutParams.setMargins(0, 0, right, bottom);
TextView tv_name = new TextView(mContext);
tv_name.setTextSize(12);
tv_name.setTextColor(Color.WHITE);
addView(tv_name, layoutParams);
return tv_name;
}
//第一次加载数据
private void initData(ImageView roundedImageView) {
if (null != browseEntities && browseEntities.size() > 0) {
//第一次去第0个数据
BrowseEntity browseEntity = browseEntities.get(position);
if (null != browseEntity) {
roundedImageView.setBackgroundResource(browseEntity.drawableId);
String username = browseEntity.name;
if (!TextUtils.isEmpty(username)) {
textView.setText(username + "来过");
}
}
}
}
由上面的操作就完成基础显示
image.gif
3. 接下来完成第一阶段动画 由最小缩放到最大
private boolean createAnimView() {
if (!isStop) {
return true;
}
ImageView imageView = getImageView();
//创建好后 设置缩放到最小
imageView.setScaleX(0);
imageView.setScaleY(0);
initData(imageView);
startScaleAnim(imageView);
return false;
}
//执行缩放动画
private void startScaleAnim(final ImageView imageView) {
ValueAnimator valueAnimator = ValueAnimator.ofFloat(0.0f, 1.0f);
valueAnimator.setDuration(800);
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
imageView.setScaleX(0.1f + animatedValue);
imageView.setScaleY(0.1f + animatedValue);
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
if (position == browseEntities.size() - 1) {
position = 0;
} else {
position++;
}
BrowseEntity browseEntity = browseEntities.get(position);
//动画执行完后要立马取出下一个图片 把底部的图片显示更新
tempImageView.setBackgroundResource(browseEntity.drawableId);
//动画执行完执行平移动画
startTranslationAnimator(imageView);
}
});
valueAnimator.start();
}
image.gif
4. 第二阶段的曲线运动缩小动画
private void startTranslationAnimator(final ImageView imageView) {
Path path;
int seed = (int) (Math.random() * 100);
//根据随机数来确定是走左边曲线还是右边曲线
if (seed % 2 == 0) {
//曲线路径的封装
path = createRightPath();
} else {
//曲线路径的封装
path = createLeftPath();
}
//通过PathMeasure 和ValueAnimator结合 在不同的阶段取出运动路径的x,y值
final PathMeasure pathMeasure = new PathMeasure(path, false);
final ValueAnimator valueAnimator = ValueAnimator.ofFloat(1.0f, 0.0f);
valueAnimator.setDuration(riseDuration);
valueAnimator.setInterpolator(new LinearInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float animatedValue = (float) animation.getAnimatedValue();
int length = (int) (pathMeasure.getLength() * animatedValue);
//在不同的阶段取出运动路径的x,y值
pathMeasure.getPosTan(length, pos, tan);
imageView.setTranslationX(pos[0]);
imageView.setTranslationY(pos[1]);
//同时做透明度动画
imageView.setAlpha(animatedValue);
if (animatedValue >= 0.5f) {
imageView.setScaleX(0.2f + animatedValue);
imageView.setScaleY(0.2f + animatedValue);
}
}
});
valueAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//动画执行完就移除View
removeView(imageView);
}
});
valueAnimator.start();
}
5. 三阶赛贝尔曲线的计算下面以左边的为例
这里我也没有更好的办法去计算 是通过不断预估尝试出来的 如果有大佬在这里有很好的计算方法 请务必告知下
private Path createLeftPath() {
Path path = new Path();
float nextFloat = new Random().nextFloat();
path.moveTo(nextFloat, -height * 1.0f / 1.8f);
//曲线控制点一
controlPointOne.x = -(viewWidth);
controlPointOne.y = -height / 5;
//曲线控制点二
controlPointTwo.x = -(viewWidth + marginLeft / 2);
controlPointTwo.y = (int) (-height * 0.15);
//生成三阶贝塞尔曲线
path.cubicTo(controlPointOne.x, controlPointOne.y, controlPointTwo.x, controlPointTwo.y, 0, 0);
return path;
}
最后连贯起来看看效
6. 最后使用RxJava 的timer()操作符 发延迟消息来让整个动画循环执行起来这里也可以用 handler 来发消息处理
public void startAnimation(int innerDelay) {
subscribe = Observable.timer(innerDelay, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new Consumer<Long>() {
@Override
public void accept(Long aLong) throws Exception {
if (createAnimView()) return;
int duration = (int) (1500 * Math.random());
if (duration < 500) {
duration = 500;
}
//循环调用
startAnimation(500 + duration);
}
});
}
//动画执行的一些开关操作
public void stopAnimator() {
isStop = false;
if (null != subscribe) {
subscribe.dispose();
}
}
到这里整个动画流程到这里就结束了,当然在内存的管理上还没有做到极致 大家可以去自由发挥, 希望这篇水文能帮助到那些有类似需求的同学,我们应该把时间拿去做一些更有用的事情,不过截止到目前 马蜂窝最新版 已经没有该头像的泡泡动画,想必他们也改了吧!