Android 屬性動畫實戰

  • 2019 年 10 月 3 日
  • 筆記

什麼是屬性動畫?

屬性動畫可以通過直接更改 View 的屬性來實現 View 動畫。例如:

  1. 通過不斷的更改 View 的坐標來實現讓 View 移動的效果;
  2. 通過不斷的更改 View 的背景來實現讓 View 的背景漸變的效果;
  3. 通過不斷的更改 View 的寬高來實現讓 View 變形的效果;

由此可見,利用屬性動畫幾乎可以處理任何的涉及到 View 的動畫效果。

實戰

具體的細節就不多說了,網上相應的教程也不少。這篇部落格主要是來實現類似於美團外賣購物車的效果。

分析

首先分析購物車動畫具體的細節:在滑動過程中,“購物車”向右移動,直至一半隱藏到右側;在手指停留在螢幕中時,“購物車”還隱藏在右側;當手指離開螢幕,“購物車”在一定時間後重新移動回來

以上的動畫細節可以分析出和 RecycleView 的滾動事件息息相關,因此動畫就應該在 RecycleView  的滾動事件中實現。

實現基本布局

上圖的藍色圖片既是我們要處理的 View 。

給 RecycleView 加上滾動事件

接下來給 RecycleView 加上滾動事件,滑動或者飛翔時圖片消失,當停止滑動時圖片顯示。

 1 // 給rv加上滾動事件   2 rv.addOnScrollListener(new RecyclerView.OnScrollListener() {   3     @Override   4     public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {   5         switch (newState) {   6             case RecyclerView.SCROLL_STATE_DRAGGING:// 滾動中   7             case RecyclerView.SCROLL_STATE_SETTLING:// 飛翔中   8                 iv.setVisibility(View.GONE);   9                 break;  10             case RecyclerView.SCROLL_STATE_IDLE:// 停止滾動  11                 iv.setVisibility(View.VISIBLE);  12                 break;  13         }  14     }  15 });

效果圖:

實現消失的動畫

根據上面的圖可以發現觸發時機基本是沒問題了,接下來要做的是讓消失不突兀,加上消失的動畫。

消失的實質是 View 的 x 坐標從當前位置一直往右直到變為隱藏了一半,下面讓我們來實現這個效果:

 1 // 消失動畫的基本屬性(從iv當前的x坐標一直到出了螢幕右側一半)   2 disappearAnimator = ValueAnimator.ofFloat(iv.getX(), (float) (Utils.getScreenWidth(this) - iv.getWidth() / 2.0));   3 disappearAnimator.setDuration(400);// 動畫持續時間   4 disappearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {   5     @Override   6     public void onAnimationUpdate(ValueAnimator animation) {// Value更新事件   7         float curValue = (float) animation.getAnimatedValue();   8         iv.setX(curValue);   9     }  10 });  11  12 // 給rv加上滾動事件  13 rv.addOnScrollListener(new RecyclerView.OnScrollListener() {  14     @Override  15     public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {  16         switch (newState) {  17             case RecyclerView.SCROLL_STATE_DRAGGING:// 滾動中  18             case RecyclerView.SCROLL_STATE_SETTLING:// 飛翔中  19                 disappearAnimator.start();  20                 break;  21             case RecyclerView.SCROLL_STATE_IDLE:// 停止滾動  22                 iv.setVisibility(View.VISIBLE);  23                 break;  24         }  25     }  26 });

然而發現實際上動畫是這樣的:

發現他是從最左邊一直移動到了最右邊,與我們的需求不符。

經調試發現,在 onCreate 的時候 iv 尚未初始化完畢,因此寬高以及坐標都還不能獲取到。所以獲取到的坐標以及寬度都是0。

所以可以在滾動事件中獲取 iv 的坐標以及寬高,更改後的程式碼如下:

 1 // 給rv加上滾動事件   2 rv.addOnScrollListener(new RecyclerView.OnScrollListener() {   3     @Override   4     public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {   5         // 獲取iv的坐標以及寬高   6         if (0 == originX) {   7             originX = iv.getX();   8             ivWidth = iv.getWidth();   9         }  10  11         switch (newState) {  12             case RecyclerView.SCROLL_STATE_DRAGGING:// 滾動中  13             case RecyclerView.SCROLL_STATE_SETTLING:// 飛翔中  14                 // 消失動畫的基本屬性(從iv當前的x坐標一直到出了螢幕右側一半)  15                 if (disappearAnimator == null) {  16                     disappearAnimator = ValueAnimator.ofFloat(originX, (float) (screenWidth - ivWidth / 2.0));  17                     disappearAnimator.setDuration(400);// 動畫持續時間  18                     disappearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  19                         @Override  20                         public void onAnimationUpdate(ValueAnimator animation) {// Value更新事件  21                             float curValue = (float) animation.getAnimatedValue();  22                             iv.setX(curValue);  23                         }  24                     });  25                 }  26  27                 disappearAnimator.start();  28                 break;  29             case RecyclerView.SCROLL_STATE_IDLE:// 停止滾動  30                 iv.setVisibility(View.VISIBLE);  31                 break;  32         }  33     }  34 });

效果如下圖:

實現出現的動畫

既然已經實現了消失的動畫,那出現的動畫也就不難了。出現的實質是 View 的 x 坐標從右側一半一直運動到原始位置。

 1 case RecyclerView.SCROLL_STATE_IDLE:// 停止滾動   2     // 出現動畫的基本屬性(從螢幕右側一半到原始位置)   3     if (appearAnimator == null) {   4         appearAnimator = ValueAnimator.ofFloat((float) (screenWidth - ivWidth / 2.0), originX);   5         appearAnimator.setDuration(400);// 動畫持續時間   6         appearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {   7             @Override   8             public void onAnimationUpdate(ValueAnimator animation) {// Value更新事件   9                 float curValue = (float) animation.getAnimatedValue();  10                 iv.setX(curValue);  11             }  12         });  13     }  14  15     appearAnimator.start();  16     break;

效果圖如下:

但是發現如果頻繁的滑動暫停的話動畫會衝突,因此需要做一些判定,如果動畫正在運行則不再重新開始動畫。改動後的程式碼如下:

 1 switch (newState) {   2     case RecyclerView.SCROLL_STATE_DRAGGING:// 滾動中   3     case RecyclerView.SCROLL_STATE_SETTLING:// 飛翔中   4         // 消失動畫的基本屬性(從iv當前的x坐標一直到出了螢幕右側一半)   5         if (disappearAnimator == null) {   6             disappearAnimator = ValueAnimator.ofFloat(originX, (float) (screenWidth - ivWidth / 2.0));   7             disappearAnimator.setDuration(400);// 動畫持續時間   8             disappearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {   9                 @Override  10                 public void onAnimationUpdate(ValueAnimator animation) {// Value更新事件  11                     float curValue = (float) animation.getAnimatedValue();  12                     iv.setX(curValue);  13                 }  14             });  15         }  16  17         // 如果消失動畫還未開始執行並且iv的位置在原始位置,則執行  18         if (!disappearAnimator.isStarted() && originX == iv.getX()) {  19             disappearAnimator.start();  20         }  21         break;  22     case RecyclerView.SCROLL_STATE_IDLE:// 停止滾動  23         // 出現動畫的基本屬性(從螢幕右側一半到原始位置)  24         if (appearAnimator == null) {  25             appearAnimator = ValueAnimator.ofFloat((float) (screenWidth - ivWidth / 2.0), originX);  26             appearAnimator.setDuration(400);// 動畫持續時間  27             appearAnimator.setStartDelay(700);// 延遲時間  28             appearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {  29                 @Override  30                 public void onAnimationUpdate(ValueAnimator animation) {// Value更新事件  31                     float curValue = (float) animation.getAnimatedValue();  32                     iv.setX(curValue);  33                 }  34             });  35         }  36  37         // 如果出現動畫還未開始執行,則執行  38         if (!appearAnimator.isStarted()) {  39             appearAnimator.start();  40         }  41         break;  42 }

但是發現還是會有衝突,經檢測,發現是出現動畫和消失動畫互相干擾的緣故。當滑動已暫停但出現動畫還未執行完畢,此時重新滑動會觸發消失動畫

因此需要給出現動畫加一個延遲,並且如果處於非暫停狀態需要取消出現動畫。(也許美團外賣暫停一段時間才開始出現的原因就是防止用戶不停的滑動暫停吧。)

修改後的程式碼如下:

case RecyclerView.SCROLL_STATE_DRAGGING:// 滾動中  case RecyclerView.SCROLL_STATE_SETTLING:// 飛翔中      // 消失動畫的基本屬性(從iv當前的x坐標一直到出了螢幕右側一半)      if (disappearAnimator == null) {          disappearAnimator = ValueAnimator.ofFloat(originX, (float) (screenWidth - ivWidth / 2.0));          disappearAnimator.setDuration(400);// 動畫持續時間          disappearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {              @Override              public void onAnimationUpdate(ValueAnimator animation) {// Value更新事件                  float curValue = (float) animation.getAnimatedValue();                  iv.setX(curValue);              }          });      }        // 如果此時出現動畫已開始,則取消      if (appearAnimator != null && appearAnimator.isStarted()) {          appearAnimator.cancel();      }        // 如果消失動畫還未開始執行並且iv的位置在原始位置,則執行      if (!disappearAnimator.isStarted() && originX == iv.getX()) {          disappearAnimator.start();      }      break;

最終效果如圖所示:

總結

  • 如果要實現其他的效果,例如淡入淡出等同理就可以實現;
  • 多個動畫對統一個 View 做變換時一定要注意動畫之間的衝突;
  • 屬性動畫+函數方程可以實現一些豐富多變的效果,待研究;
  • 本文的實現還是比較簡陋,最好的效果是動畫可以被打斷,由於比較麻煩,所以沒有實現。

Github地址:屬性動畫初戰

 

大家如果有什麼疑問或者建議可以通過評論或者郵件的方式聯繫我,歡迎大家的評論~