手把手教你打造RecyclerView滾動特效

GloSpowers 7年前發布 | 19K 次閱讀 Android開發 移動開發 RecyclerView

前情提要

效果圖

最近開發中遇到這樣的需求,recyclerview的item隨滾動改變大小和透明度。這個效果看起來挺有動感的,似乎實現起來有點復雜,其實不然,接下來將帶領大家手把手實現這個效果。

Item動畫分析

我們化整為零,將這個效果分解到一個item上來看其實是這樣的:

item動畫

  • 實現思路
    看到這個動畫效果時,我首先想到的是,這個動畫是可控的,不是通過設置anim.setDuration來實現的,所以要放棄Animation的念頭,轉而用傳入process(動畫執行的進度)的思路。
  • 分解動畫
    繼續化整為零,可以將這個動畫效果分解為:蒙版透明度(alpha)、寬度(width)、圖片縮放(scale)
  • 狀態轉換
    先不考慮動畫變化的具體細節,先分清楚狀態機。動畫的變化狀態為:
    蒙版:暗->亮->暗
    寬度:小->大->小
    圖片:縮->放->縮
  • 考慮細節
    蒙版(黑色蒙版):
    1%->50%: 1.0->0.0;
    51%->100%: 0.0->1.0;
    寬度(通過設置橫向外邊距):
    1%->25%: 16dp->0dp;
    26%->75%: 0dp;
    76%->100%: 0dp->16dp
    圖片縮放:

圖片縮放

1%->25%: 1.0->(b/a);

26%->50%: (b/a)->(c/a);

51%->75%: (c/a)->(b/a);

76%->100%: (b/a)->1.0;

Item動畫代碼實現

新建一個CustomAnimation類,定義相應動畫控件的id,并初始化:

// 無控件
private static final int NO_VIEW = -999;
// 透明度變化視圖
private int mAlphaViewId = NO_VIEW;
// 圖片變化視圖
private int mImageViewId = NO_VIEW;
// 邊距變化視圖
private int mMarginViewId = NO_VIEW;

/**

  • 設置透明度變化控件的ID
  • @param resId */ public void setAlphaViewId(int resId) { Log.i("animm", "setAlphaViewId"); mAlphaViewId = resId; }

/**

  • 設置圖片變化控件的ID
  • @param resId */ public void setImageViewId(int resId) { Log.i("animm", "setImageViewId"); mImageViewId = resId; }

/**

  • 設置外邊距變化控件的ID
  • @param resId */ public void setMarginViewId(int resId) { Log.i("animm", "setMarginViewId"); mMarginViewId = resId; }</code></pre>

    定義變量process,并通過傳入process的值進行效果實現:

    // 動畫進度
    private int mProcess = 0;

/**

  • 通過進度值控制動畫的進度
  • @param viewGroup 父容器
  • @param process 動畫變化進度 */ public void setAnimByProcess(ViewGroup viewGroup, int process) { if (viewGroup == null) {

     return;
    

    } mProcess = process; /**

    • 蒙版透明度設置 */ if (enableAlpha && mAlphaViewId != NO_VIEW) { View view = viewGroup.findViewById(mAlphaViewId); if (process > 0 && process <= 25) {
       float alpha = (25 - process) / 25.0f;
       view.setAlpha(alpha);
      
      } else if (process > 75 && process <= 100) {
       float alpha = (process - 75) / 25.0f;
       view.setAlpha(alpha);
      
      } }

    /*

    • 設置圖片大小 */ if (enableImage && mImageViewId != NO_VIEW) { ImageView imageView = (ImageView) viewGroup.findViewById(mImageViewId); float curWidth = 0; if (process <= 25) {
       float percent = process / 25.0f;
       float marginHorizontal = mMarginHorizontal * percent;
       curWidth = mImgOrgWidth + 2 * marginHorizontal;
      
      } else if (process > 25 && process <= 50) {
       float percent = (process - 25) / 25.0f;
       float marginHorizontal = mMarginHorizontal * percent;
       curWidth = mScreenWidth + 2 * marginHorizontal;
      
      } else if (process > 50 && process <= 75) {
       float percent = (75 - process) / 25.0f;
       float marginHorizontal = mMarginHorizontal * percent;
       curWidth =  mScreenWidth + 2 * marginHorizontal;
      
      } else {
       float percent = (100 - process) / 25.0f;
       float marginHorizontal = mMarginHorizontal * percent;
       curWidth = mImgOrgWidth + 2 * marginHorizontal;
      
      } float scale = curWidth / mImgOrgWidth ; scale *= 1.1f; imageView.setScaleX(scale); imageView.setScaleY(scale); } /**
    • 設置外邊距(橫向) */ if (enableMargin && mMarginViewId != NO_VIEW) { View view = viewGroup.findViewById(mMarginViewId); RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) view.getLayoutParams(); if (process > 0 && process <= 25) {
       float percent = (25 - process) / 25.0f;
       float marginHorizontal = mMarginHorizontal * percent;
       lp.setMargins((int)marginHorizontal, (int)mMarginTop, (int)marginHorizontal, (int)mMarginBottom);
       view.setLayoutParams(lp);
      
      } else if (process > 75 && process <= 100) {
       float percent = (process - 75) / 25.0f;
       float marginHorizontal = mMarginHorizontal * percent;
       lp.setMargins((int)marginHorizontal, (int)mMarginTop, (int)marginHorizontal, (int)mMarginBottom);
       view.setLayoutParams(lp);
      
      } } }</code></pre>

      結合RecyclerView思考

      基于上述代碼,我們基本實現動畫的細節,接下來我們需要思考的是,如何將RecyclerView與process結合?思考這個問題前,我們來看一下這個效果:

      列表滑動效果

      這是我用簡書的 Markdown 代碼塊語法實現的 仿RecyclerView列表的效果 ,基于這個效果我想到將側邊欄的滑塊和RecyclerView的Item結合起來,與動畫的process變量相關聯:

      0%

      50%

      100%

      通過右側小滑塊底部與Item頂部之間的距離占兩個Item高度的百分比作為process的值:

      手機屏幕坐標示意圖

      process = (turningLine – itemTop) / (2 * itemHeight);

      如此,我們將此關系放入新建的類TurnProcess中:

      public class TurnProcess {
      /**
    • 返回動畫完成的進度
    • @param itemTop
    • @param turningLine
    • @param itemHeight
    • @return */ public static int getProcess(float itemTop, float turningLine, float itemHeight) { if (turningLine < itemTop || turningLine > (itemHeight + itemTop)) {
       return 0;
      
      } else {
       float percent = (turningLine - itemTop) / itemHeight;
       return (int) (percent * 100);
      
      } } }</code></pre>

      計算滑動塊底部的位置

      得到了上一步滑動與process的關系,接下來我們來計算一下滑塊底部到RecyclerView可見范圍頂部的距離。

      RecyclerView初始情況

      我們可以將RecyclerView初始情況設想如上圖,此時turningLine的值為0。當RecyclerView滑動時:

      RecyclerView滾動高度與turningLine的關系

      由上圖,我們可得到turniingLine與RecyclerView滑動距離的關系,從而得到turningLine的值:

      scrollY / totalScroll = turningLine / totalHeight;

      turningLine = scrollY * totalHeight / totalScroll;

      totalScroll的值可以通過RecyclerView總高度(包含不可見部分)與RecyclerView可見部分的高度相差得到;而scrollY則隨著RecyclerView的滾動變化,因此需要對RecyclerView進行滾動事件的監聽:

      recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
      @Override
      public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
       float scrollY = getScrollDistance(recyclerView);
      }
      }

/**

  • 獲取滾動的距離 / private int getScrollDistance(RecyclerView recyclerView) { LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager(); View firstVisibleItem = recyclerView.getChildAt(0); int firstItemPosition = layoutManager.findFirstVisibleItemPosition(); int itemHeight = firstVisibleItem.getHeight(); int firstItemBottom = layoutManager.getDecoratedBottom(firstVisibleItem); return (firstItemPosition + 1) itemHeight - firstItemBottom;}</code></pre>

    如此,不斷變化的turningLine與RecyclerView的滾動建立了關系;至此,動畫與RecyclerView的邏輯關系梳理完畢。按照實現RecyclerView的套路一步步實現最基本的列表效果,然后將動畫與滾動監聽的關系放入Adapter中。需要強調的是:每一個Item都是隨著RecyclerView的滾動進行變化的,所以每一個Item的ViewHolder中都注冊RecyclerView的監聽事件來監聽RecyclerView的滑動。

    不足及期望

    這樣的動畫效果固然有趣,但是其仍存在很多不足,就自己發現的問題,列不足如下:

    • 每一個Item都監聽RecyclerView的滑動事件非常耗時,在低端機上可能存在滑動不流暢的現象,尚未測試,但在紅米 Not 3聯發科版系統(不得不說這個系統真的很渣,親測體驗)上運行未出現異常。
    • 當RecyclerView滑動太快時,單位滾動距離內,滾動監聽事件的觸發頻率較低,導致有些Item的動畫進度未達到100%便從屏幕中消失,從而存在重新滾動到那個Item時,Item的動畫停留在1%~99%之間的某一幀,影響RecyclerView的展示效果。
    • 因ImageView設置的ScaleType為CenterCrop,所以圖片右側變化在放大過程中會有類似于金屬拉絲的效果,因此圖片縮放的scale最好在原來的基礎上乘以1.1,在單個Item的動畫中此問題已解決,但在RecyclerView中,此問題仍然存在。

    在此,期望有耐心將本文看完的小伙伴們在文章下方的評論里留下寶貴意見,一起來完善這個效果。另,若有小伙伴在Github上看到有這樣效果的穩定的第三方庫,希望可以在文章下方評論中留下鏈接。

     

     

    來自:http://www.androidchina.net/6535.html

     

 本文由用戶 GloSpowers 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!