高仿馬蜂窩旅游頭像泡泡動(dòng)畫

當(dāng)pm制定完下一版本需求打開馬蜂窩旅游app準(zhǔn)備出去嗨一圈的時(shí)候 看到了馬蜂窩旅游app的一個(gè)用戶頭像動(dòng)畫后。。。(=@__@=) 先看看效果圖

demo_01

效果分析:

demo_03

  1. 涉及到有多個(gè)view在做動(dòng)畫操作 這里需要繼承FrameLayout來左父布局 供圖片做動(dòng)畫操作
  2. 每個(gè)子view的動(dòng)畫路徑類似于S型 我這里采用的是三階貝塞爾曲線和PathMeasure來完成動(dòng)畫運(yùn)動(dòng)路徑的封裝
  3. 每個(gè)子view動(dòng)畫執(zhí)行完后 是移除添加新的view進(jìn)來 還是回收重新利用 本案例是直接移除再添加新的(回收重新利用還沒來得及去考慮該怎么寫)
  4. 動(dòng)畫是循環(huán)不停的播放 我采用的是RxJava timer()操作符 不斷的發(fā)送隨機(jī)延遲消息去通知?jiǎng)赢嫷膱?zhí)行
  5. 最后就剩下一些停止動(dòng)畫操作的開關(guān)設(shè)定

實(shí)現(xiàn)步驟

1.一些基本的初始化工作

public class HeadBubbleView extends FrameLayout {
    //這個(gè)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);
        //三階貝塞爾曲線控制點(diǎn)一
        controlPointOne = new Point();
        //三階貝塞爾曲線控制點(diǎn)二
        controlPointTwo = new Point();
        //每個(gè)子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.初始化的時(shí)候數(shù)據(jù)的加載狀態(tài)

private void initView() {
        //這個(gè)ImageView將不執(zhí)行動(dòng)畫 用于底部不斷切換圖片展示
        tempImageView = getImageView();
        textView = getTextView();
        initData(tempImageView);
    } 
//創(chuàng)建執(zhí)行動(dòng)畫的具體角色    
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;
    }
//創(chuàng)建用于顯示坐標(biāo)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;
    }
//第一次加載數(shù)據(jù)
private void initData(ImageView roundedImageView) {
        if (null != browseEntities && browseEntities.size() > 0) {
            //第一次去第0個(gè)數(shù)據(jù)
            BrowseEntity browseEntity = browseEntities.get(position);
            if (null != browseEntity) {
                roundedImageView.setBackgroundResource(browseEntity.drawableId);
                String username = browseEntity.name;
                if (!TextUtils.isEmpty(username)) {
                    textView.setText(username + "來過");
                }
            }
        }
    }

由上面的操作就完成基礎(chǔ)顯示


demo_02

3.接下來完成第一階段動(dòng)畫 由最小縮放到最大

private boolean createAnimView() {
        if (!isStop) {
            return true;
        }
        ImageView imageView = getImageView();
        //創(chuàng)建好后 設(shè)置縮放到最小
        imageView.setScaleX(0);
        imageView.setScaleY(0);
        initData(imageView);
        startScaleAnim(imageView);
        return false;
    }
//執(zhí)行縮放動(dòng)畫    
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);
        //動(dòng)畫執(zhí)行完后要立馬取出下一個(gè)圖片 把底部的圖片顯示更新
        tempImageView.setBackgroundResource(browseEntity.drawableId);
        //動(dòng)畫執(zhí)行完執(zhí)行平移動(dòng)畫       
        startTranslationAnimator(imageView);
            }
        });
        valueAnimator.start();
    }
demo_04

4.第二階段的曲線運(yùn)動(dòng)縮小動(dòng)畫

private void startTranslationAnimator(final ImageView imageView) {
        Path path;
        int seed = (int) (Math.random() * 100);
        //根據(jù)隨機(jī)數(shù)來確定是走左邊曲線還是右邊曲線
        if (seed % 2 == 0) {
            //曲線路徑的封裝
            path = createRightPath();
        } else {
            //曲線路徑的封裝
            path = createLeftPath();
        }
        //通過PathMeasure 和ValueAnimator結(jié)合 在不同的階段取出運(yùn)動(dòng)路徑的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);
               //在不同的階段取出運(yùn)動(dòng)路徑的x,y值
                pathMeasure.getPosTan(length, pos, tan);
                imageView.setTranslationX(pos[0]);
                imageView.setTranslationY(pos[1]);
                //同時(shí)做透明度動(dòng)畫
                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) {
                //動(dòng)畫執(zhí)行完就移除View
                removeView(imageView);
            }
        });
        valueAnimator.start();
    }

5.三階賽貝爾曲線的計(jì)算 下面以左邊的為例

這里我也沒有更好的辦法去計(jì)算 是通過不斷預(yù)估嘗試出來的 如果有大佬在這里有很好的計(jì)算方法 請(qǐng)務(wù)必告知下

dome_08
private Path createLeftPath() {
        Path path = new Path();
        float nextFloat = new Random().nextFloat();
        path.moveTo(nextFloat, -height * 1.0f / 1.8f);
        //曲線控制點(diǎn)一
        controlPointOne.x = -(viewWidth);
        controlPointOne.y = -height / 5;
        //曲線控制點(diǎn)二
        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;
    }

最后連貫起來看看效果


demo_07

6.最后使用RxJava 的timer()操作符 發(fā)延遲消息來讓整個(gè)動(dòng)畫循環(huán)執(zhí)行起來

這里也可以用handler 來發(fā)消息處理

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;
                        }
                        //循環(huán)調(diào)用
                        startAnimation(500 + duration);
                    }
                });
    }
    
//動(dòng)畫執(zhí)行的一些開關(guān)操作  
public void stopAnimator() {
        isStop = false;
        if (null != subscribe) {
            subscribe.dispose();
        }
    }   
demo_08

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,685評(píng)論 1 32
  • 1 CALayer IOS SDK詳解之CALayer(一) http://doc.okbase.net/Hell...
    Kevin_Junbaozi閱讀 5,346評(píng)論 3 23
  • 在iOS中隨處都可以看到絢麗的動(dòng)畫效果,實(shí)現(xiàn)這些動(dòng)畫的過程并不復(fù)雜,今天將帶大家一窺iOS動(dòng)畫全貌。在這里你可以看...
    F麥子閱讀 5,273評(píng)論 5 13
  • 如果想讓事情變得順利,只有靠自己--夏爾·紀(jì)堯姆 上一章介紹了隱式動(dòng)畫的概念。隱式動(dòng)畫是在iOS平臺(tái)創(chuàng)建動(dòng)態(tài)用戶界...
    夜空下最亮的亮點(diǎn)閱讀 2,106評(píng)論 0 1
  • 【Android 動(dòng)畫】 動(dòng)畫分類補(bǔ)間動(dòng)畫(Tween動(dòng)畫)幀動(dòng)畫(Frame 動(dòng)畫)屬性動(dòng)畫(Property ...
    Rtia閱讀 6,437評(píng)論 1 38

友情鏈接更多精彩內(nèi)容