高仿馬蜂窩旅遊頭像泡泡動畫

當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 + "來過");
                }
            }
        }
    }

由上面的操作就完成基礎顯示



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();
    }

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();
        }
    }   

到這裏整個動畫流程到這裏就結束了,當然在內存的管理上還沒有做到極致 大家可以去自由發揮, 希望這篇水文能幫助到那些有類似需求的同學,我們應該把時間拿去做一些更有用的事情,不過截止到目前 馬蜂窩最新版 已經沒有該頭像的泡泡動畫,想必他們也改了吧!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章