高仿马蜂窝旅游头像泡泡动画

共 7461字,需浏览 15分钟

 ·

2020-06-21 23:49

72dd8ac3cfb320f94bde6949095f276e.webp

本篇文章转自 明朗__ 的博客,分享了一个炫酷的动画效果,希望对大家有所帮助!

原文地址:https://www.jianshu.com/p/dd29f1ae5239

前言

当 pm 制定完下一版本需求打开马蜂窝旅游 app 准备出去嗨一圈的时候看到了马蜂窝旅游 app 的一个用户头像动画后。。。(=@__@=) 先看看效果图

45d4ff83390212fa422dade4722ae16d.webp

效果分析:

39d9f2b13457fb837e92ccdf44ff8c84.webp
  1. 涉及到有多个 view 在做动画操作 这里需要继承 FrameLayout 来左父布局 供图片做动画操作
  2. 每个子 view 的动画路径类似于 S 型 我这里采用的是三阶贝塞尔曲线和 PathMeasure 来完成动画运动路径的封装
  3. 每个子 view 动画执行完后 是移除添加新的 view 进来 还是回收重新利用 本案例是直接移除再添加新的(回收重新利用还没来得及去考虑该怎么写)
  4. 动画是循环不停的播放 我采用的是 RxJava timer()操作符 不断的发送随机延迟消息去通知动画的执行
  5. 最后就剩下一些停止动画操作的开关设定

实现步骤

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 + "来过");
                }
            }
        }
    }

由上面的操作就完成基础显示

86da2586efb1a03b9737123d0587abae.webp

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.0f1.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();
    }
8f4edd93da87726363482ffcfd872b35.webp

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.0f0.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. 三阶赛贝尔曲线的计算

下面以左边的为例

这里我也没有更好的办法去计算 是通过不断预估尝试出来的 如果有大佬在这里有很好的计算方法 请务必告知下

6d6c6d0738336de1515fc1f85ad53f10.webp
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, 00);
        return path;
    }

最后连贯起来看看效

f4adc6dda9bb9a096d582f4609f4651b.webp

6. 最后使用 RxJava 的 timer()操作符 发延迟消息来让整个动画循环执行起来

这里也可以用 handler 来发消息处理

public void startAnimation(int innerDelay) {
        subscribe = Observable.timer(innerDelay, TimeUnit.MILLISECONDS)
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer() {
                    @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();
        }
    }
bbc74012b71e8873ce4fcbc350b77e70.webp

到这里整个动画流程到这里就结束了,当然在内存的管理上还没有做到极致,大家可以去自由发挥,希望这篇水文能帮助到那些有类似需求的同学,我们应该把时间拿去做一些更有用的事情,不过截止到目前,马蜂窝最新版 已经没有该头像的泡泡动画,想必他们也改了吧!



- End -



猜你喜欢Android 通讯录索引效果三个值得学习的自定义 View 开源项目使用 AirPods Pro 遇到的问题

浏览 39
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报