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

作者: 像程序一样思考 | 来源:发表于2020-06-08 16:13 被阅读0次

    / 前言 /

    一名优秀的Android开发,需要一份完备的 知识体系,在这里,让我们一起成长为自己所想的那样~。

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

    效果分析:

    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 + "来过");
                    }
                }
            }
        }
    
    
    由上面的操作就完成基础显示 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();
            }
        } 
    

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

    相关文章

      网友评论

        本文标题:高仿马蜂窝旅游头像泡泡动画

        本文链接:https://www.haomeiwen.com/subject/taurtktx.html