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

本篇文章轉(zhuǎn)自 明朗__ 的博客,分享了一個(gè)炫酷的動(dòng)畫(huà)效果,希望對(duì)大家有所幫助!
原文地址:https://www.jianshu.com/p/dd29f1ae5239
前言
當(dāng) pm 制定完下一版本需求打開(kāi)馬蜂窩旅游 app 準(zhǔn)備出去嗨一圈的時(shí)候看到了馬蜂窩旅游 app 的一個(gè)用戶(hù)頭像動(dòng)畫(huà)后。。。(=@__@=) 先看看效果圖

效果分析:

- 涉及到有多個(gè) view 在做動(dòng)畫(huà)操作 這里需要繼承 FrameLayout 來(lái)左父布局 供圖片做動(dòng)畫(huà)操作
- 每個(gè)子 view 的動(dòng)畫(huà)路徑類(lèi)似于 S 型 我這里采用的是三階貝塞爾曲線和 PathMeasure 來(lái)完成動(dòng)畫(huà)運(yùn)動(dòng)路徑的封裝
- 每個(gè)子 view 動(dòng)畫(huà)執(zhí)行完后 是移除添加新的 view 進(jìn)來(lái) 還是回收重新利用 本案例是直接移除再添加新的(回收重新利用還沒(méi)來(lái)得及去考慮該怎么寫(xiě))
- 動(dòng)畫(huà)是循環(huán)不停的播放 我采用的是 RxJava timer()操作符 不斷的發(fā)送隨機(jī)延遲消息去通知?jiǎng)赢?huà)的執(zhí)行
- 最后就剩下一些停止動(dòng)畫(huà)操作的開(kāi)關(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)畫(huà)?用于底部不斷切換圖片展示
????????tempImageView?=?getImageView();
????????textView?=?getTextView();
????????initData(tempImageView);
????}
//創(chuàng)建執(zhí)行動(dòng)畫(huà)的具體角色
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來(lái)過(guò)的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?+?"來(lái)過(guò)");
????????????????}
????????????}
????????}
????}
由上面的操作就完成基礎(chǔ)顯示

3. 接下來(lái)完成第一階段動(dòng)畫(huà) 由最小縮放到最大
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)畫(huà)
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)畫(huà)執(zhí)行完后要立馬取出下一個(gè)圖片?把底部的圖片顯示更新
????????tempImageView.setBackgroundResource(browseEntity.drawableId);
????????//動(dòng)畫(huà)執(zhí)行完執(zhí)行平移動(dòng)畫(huà)
????????startTranslationAnimator(imageView);
????????????}
????????});
????????valueAnimator.start();
????}

4. 第二階段的曲線運(yùn)動(dòng)縮小動(dòng)畫(huà)
private?void?startTranslationAnimator(final?ImageView?imageView)?{
????????Path?path;
????????int?seed?=?(int)?(Math.random()?*?100);
????????//根據(jù)隨機(jī)數(shù)來(lái)確定是走左邊曲線還是右邊曲線
????????if?(seed?%?2?==?0)?{
????????????//曲線路徑的封裝
????????????path?=?createRightPath();
????????}?else?{
????????????//曲線路徑的封裝
????????????path?=?createLeftPath();
????????}
????????//通過(guò)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)畫(huà)
????????????????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)畫(huà)執(zhí)行完就移除View
????????????????removeView(imageView);
????????????}
????????});
????????valueAnimator.start();
????}
5. 三階賽貝爾曲線的計(jì)算
下面以左邊的為例
這里我也沒(méi)有更好的辦法去計(jì)算 是通過(guò)不斷預(yù)估嘗試出來(lái)的 如果有大佬在這里有很好的計(jì)算方法 請(qǐng)務(wù)必告知下

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;
????}
最后連貫起來(lái)看看效

6. 最后使用 RxJava 的 timer()操作符 發(fā)延遲消息來(lái)讓整個(gè)動(dòng)畫(huà)循環(huán)執(zhí)行起來(lái)
這里也可以用 handler 來(lái)發(fā)消息處理
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;
????????????????????????}
????????????????????????//循環(huán)調(diào)用
????????????????????????startAnimation(500?+?duration);
????????????????????}
????????????????});
????}
//動(dòng)畫(huà)執(zhí)行的一些開(kāi)關(guān)操作
public?void?stopAnimator()?{
????????isStop?=?false;
????????if?(null?!=?subscribe)?{
????????????subscribe.dispose();
????????}
????}

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