天天看點

ItemTouchHelper 實作互動動畫

目錄介紹

  • 01.拖拽需要實作功能
  • 02.幾個重要的方法說明
  • 03.簡單實作思路
  • 04.拖拽效果上優化
  • 05.完整代碼展示

好消息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護并且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計N篇[近100萬字,陸續搬到網上],轉載請注明出處,謝謝!
  • 連結位址: https://github.com/yangchong211/YCBlogs
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起于忽微,量變引起質變!

  • 需要實作拖拽的功能如下所示
    • 長按item後拖動,與其他item交換位置
    • 按住item右面的圖示後拖動,與其他item交換位置
    • 左滑item變透明并縮小,超出螢幕後,其他item補上
    • 右滑item變透明并縮小,超出螢幕後,其他item補上

  • 幾個重要的方法說明
    • 需要自定義類實作ItemTouchHelper.Callback類,并重寫其中幾個方法
    isLongPressDragEnabled                  是否可以長按拖拽排序
    isItemViewSwipeEnabled                  Item是否可以被滑動
    getMovementFlags                        當使用者拖拽或者滑動Item的時候需要我們告訴系統滑動或者拖拽的方向
    onMove                                  當Item被拖拽的時候被回調
    onSwiped                                當View被滑動删除的時候
    onSelectedChanged                       當item被拖拽或側滑時觸發           

  • 幾個方法中代碼思路
    • 要想達到上面功能需求,在getMovementFlags方法中,當使用者拖拽或者滑動Item的時候需要我們告訴系統滑動或者拖拽的方向,那我們知道支援拖拽和滑動删除的無非就是LinearLayoutManager和GridLayoutManager了,是以可以根據布局管理器的不同做了響應的區分。
    • 在onMove方法中處理拖拽的回調邏輯,那麼什麼時候被調用?當Item被拖拽排序移動到另一個Item的位置的時候被調用。在onSwiped方法[當Item被滑動删除到不見]中處理被删除後的邏輯。為了降低代碼耦合度,可以通過接口listener回調的方式交給外部處理。
  • 上下拖動時與其他item進行位置交換
    • ItemTouchHelper.Callback本身不具備将兩個item互換位置的功能,但RecyclerView可以,我們可以在item拖動的時候把目前item與另一個item的資料位置交換,再調用RecyclerView的notifyItemMoved()方法重新整理布局,同時,因為RecyclerView自帶item動畫,就可以完成上面的互動效果。
  • 左右滑出螢幕時其他item補上
    • 隻要在item滑出螢幕時,将對應的資料删掉,再調用RecyclerView的notifyItemRemoved()方法重新整理布局即可。

  • 拖拽效果優化
    • 在item被拖拽或側滑時修改背景色,當動作結束後将背景色恢複回來,而ItemTouchHelper.Callback中正好有對應這兩個狀态的方法,分别是:onSelectedChanged()、clearView()。那麼優化處理其實可以放到這兩個方法中處理。
    • 左右滑動使item透明度變淺且縮小該如何實作呢?讓item執行了兩種屬性動畫而已,在ItemTouchHelper.Callback中有一個方法可以拿到item被拖拽或滑動時的位移變化,那就是onChildDraw()方法,在該方法中設定item漸變和縮放屬性動畫。
    • 出現問題,按照上面做法會出現删除後有空白item留出來,那麼為什麼會出現這種情況呢?并不是多出了兩條空白資料,它們是正常的資料,隻是看不到了,這是因為RecyclerView條目(itemView)覆用導緻的,前面在onChildDraw()方法中對itemView設定了透明和縮小,而一個清單中固定隻有幾個itemView而已,當那兩個透明縮小的itemView被再次使用時,之前設定的透明度和高度比例已經是0,是以就出現了這種情況,解決方法也很簡單,隻要在item被移除後,将itemView的透明度和高度比例設定回來即可

  • 代碼的GitHub位址: https://github.com/yangchong211/YCRefreshView
  • 完整代碼如下所示
    /**
     * <pre>
     *     @author 楊充
     *     blog  : https://github.com/yangchong211
     *     time  : 2017/5/2
     *     desc  : 自定義ItemTouchHelper
     *     revise: 參考嚴正傑大神部落格:https://blog.csdn.net/yanzhenjie1003/article/details/51935982
     * </pre>
     */
    public class ItemTouchHelpCallback extends ItemTouchHelper.Callback {
    
        /**
         * Item操作的回調,去更新UI和資料源
         */
        private OnItemTouchCallbackListener onItemTouchCallbackListener;
        /**
         * 是否可以拖拽
         */
        private boolean isCanDrag = false;
        /**
         * 是否可以被滑動
         */
        private boolean isCanSwipe = false;
        /**
         * 按住拖動item的顔色
         */
        private int color = 0;
    
        public ItemTouchHelpCallback(OnItemTouchCallbackListener onItemTouchCallbackListener) {
            this.onItemTouchCallbackListener = onItemTouchCallbackListener;
        }
    
        /**
         * 設定是否可以被拖拽
         *
         * @param canDrag 是true,否false
         */
        public void setDragEnable(boolean canDrag) {
            isCanDrag = canDrag;
        }
    
        /**
         * 設定是否可以被滑動
         *
         * @param canSwipe 是true,否false
         */
        public void setSwipeEnable(boolean canSwipe) {
            isCanSwipe = canSwipe;
        }
    
        /**
         * 設定按住拖動item的顔色
         * @param color     顔色
         */
        public void setColor(@ColorInt int color){
            this.color = color;
        }
    
        /**
         * 當Item被長按的時候是否可以被拖拽
         *
         * @return                      true
         */
        @Override
        public boolean isLongPressDragEnabled() {
            return isCanDrag;
        }
    
        /**
         * Item是否可以被滑動(H:左右滑動,V:上下滑動)
         * isItemViewSwipeEnabled()傳回值是否可以拖拽排序,true可以,false不可以
         * @return                      true
         */
        @Override
        public boolean isItemViewSwipeEnabled() {
            return isCanSwipe;
        }
    
        /**
         * 當使用者拖拽或者滑動Item的時候需要我們告訴系統滑動或者拖拽的方向
         * 動作辨別分:dragFlags和swipeFlags
         * dragFlags:清單滾動方向的動作辨別(如豎直清單就是上和下,水準清單就是左和右)
         * wipeFlags:與清單滾動方向垂直的動作辨別(如豎直清單就是左和右,水準清單就是上和下)
         *
         * 思路:如果你不想上下拖動,可以将 dragFlags = 0
         *      如果你不想左右滑動,可以将 swipeFlags = 0
         *      最終的動作辨別(flags)必須要用makeMovementFlags()方法生成
         */
        @Override
        public int getMovementFlags(@NonNull RecyclerView recyclerView,
                                    @NonNull RecyclerView.ViewHolder viewHolder) {
            RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
            if (layoutManager instanceof GridLayoutManager) {
                // flag如果值是0,相當于這個功能被關閉
                int dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT
                        | ItemTouchHelper.UP | ItemTouchHelper.DOWN;
                int swipeFlag = 0;
                // create make
                return makeMovementFlags(dragFlag, swipeFlag);
            } else if (layoutManager instanceof LinearLayoutManager) {
                LinearLayoutManager linearLayoutManager = (LinearLayoutManager) layoutManager;
                int orientation = linearLayoutManager.getOrientation();
    
                int dragFlag = 0;
                int swipeFlag = 0;
    
                // 為了友善了解,相當于分為橫着的ListView和豎着的ListView
                // 如果是橫向的布局
                if (orientation == LinearLayoutManager.HORIZONTAL) {
                    swipeFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
                    dragFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
                } else if (orientation == LinearLayoutManager.VERTICAL) {
                    // 如果是豎向的布局,相當于ListView
                    dragFlag = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
                    swipeFlag = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
                }
                //第一個參數是拖拽flag,第二個是滑動的flag
                return makeMovementFlags(dragFlag, swipeFlag);
            }
            return 0;
        }
    
    
        /**
         * 當Item被拖拽的時候被回調
         *
         * @param recyclerView          recyclerView
         * @param srcViewHolder         目前被拖拽的item的viewHolder
         * @param targetViewHolder      目前被拖拽的item下方的另一個item的viewHolder
         * @return                      是否被移動
         */
        @Override
        public boolean onMove(@NonNull RecyclerView recyclerView,
                              @NonNull RecyclerView.ViewHolder srcViewHolder,
                              @NonNull RecyclerView.ViewHolder targetViewHolder) {
            if (onItemTouchCallbackListener != null) {
                int srcPosition = srcViewHolder.getAdapterPosition();
                int targetPosition = targetViewHolder.getAdapterPosition();
                return onItemTouchCallbackListener.onMove(srcPosition, targetPosition);
            }
            return false;
        }
    
    
        /**
         * 當item側滑出去時觸發(豎直清單是側滑,水準清單是豎滑)
         *
         * @param viewHolder            viewHolder
         * @param direction             滑動的方向
         */
        @Override
        public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
            if (onItemTouchCallbackListener != null) {
                onItemTouchCallbackListener.onSwiped(viewHolder.getAdapterPosition());
            }
        }
    
        /**
         * 當item被拖拽或側滑時觸發
         *
         * @param viewHolder            viewHolder
         * @param actionState           目前item的狀态
         */
        @Override
        public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
            super.onSelectedChanged(viewHolder, actionState);
            //不管是拖拽或是側滑,背景色都要變化
            if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
                if (color==0){
                    viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext()
                            .getResources().getColor(android.R.color.darker_gray));
                }else {
                    viewHolder.itemView.setBackgroundColor(color);
                }
            }
        }
    
    
        /**
         * 當item的互動動畫結束時觸發
         *
         * @param recyclerView          recyclerView
         * @param viewHolder            viewHolder
         */
        @Override
        public void clearView(@NonNull RecyclerView recyclerView,
                              @NonNull RecyclerView.ViewHolder viewHolder) {
            super.clearView(recyclerView, viewHolder);
            viewHolder.itemView.setBackgroundColor(viewHolder.itemView.getContext().getResources()
                    .getColor(android.R.color.white));
            viewHolder.itemView.setAlpha(1);
            viewHolder.itemView.setScaleY(1);
        }
    
    
        @Override
        public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
                                @NonNull RecyclerView.ViewHolder viewHolder,
                                float dX, float dY, int actionState, boolean isCurrentlyActive) {
            super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
            if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
                float value = 1 - Math.abs(dX) / viewHolder.itemView.getWidth();
                viewHolder.itemView.setAlpha(value);
                viewHolder.itemView.setScaleY(value);
            }
        }
    
    
        public interface OnItemTouchCallbackListener {
            /**
             * 當某個Item被滑動删除的時候
             *
             * @param adapterPosition   item的position
             */
            void onSwiped(int adapterPosition);
    
            /**
             * 當兩個Item位置互換的時候被回調
             *
             * @param srcPosition       拖拽的item的position
             * @param targetPosition    目的地的Item的position
             * @return                  開發者處理了操作應該傳回true,開發者沒有處理就傳回false
             */
            boolean onMove(int srcPosition, int targetPosition);
        }
    }           
  • 如何使用,代碼如下所示
    ItemTouchHelpCallback callback = new ItemTouchHelpCallback((srcPosition, targetPosition) -> {
            if (imageBeans != null) {
                try {
                    // 更換資料源中的資料Item的位置。更改list中開始和結尾position的位置
                    Collections.swap(imageBeans, srcPosition, targetPosition);
                    // 更新UI中的Item的位置,主要是給使用者看到互動效果
                    mAdapter.notifyItemMoved(srcPosition, targetPosition);
                } catch (Exception e){
                    e.printStackTrace();
                }
                return true;
            }
            return true;
        });
    callback.setDragEnable(true);
    callback.setSwipeEnable(true);
    callback.setColor(this.getResources().getColor(R.color.base_background_block));
    //建立helper對象,callback監聽recyclerView item 的各種狀态
    ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);
    try{
        //關聯recyclerView,一個helper對象隻能對應一個recyclerView
        itemTouchHelper.attachToRecyclerView(recyclerView);
    }catch (Exception e){
        e.printStackTrace();
    }           

開源庫位址[融合大多數recyclerView使用案例,可以直接下載下傳demo]:

  • 00.RecyclerView複雜封裝庫
    • 幾乎融合了該系列部落格中絕大部分的知識點,歡迎一遍看部落格一遍實踐,一步步從簡單實作功能強大的庫
  • 01.RecyclerView
    • RecycleView的結構,RecyclerView簡單用法介紹
  • 02.Adapter
    • RecyclerView.Adapter扮演的角色,一般常用的重寫方法說明,資料變更通知之觀察者模式,檢視.notifyChanged();源碼
  • 03.ViewHolder
    • ViewHolder的作用,如何了解對于ViewHolder對象的數量“夠用”之後就停止調用onCreateViewHolder方法,ViewHolder簡單封裝
  • 04.LayoutManager
    • LayoutManager作用是什麼?setLayoutManager源碼分析
  • 05.SnapHelper
    • SnapHelper作用,什麼是Fling操作 ,SnapHelper類重要的方法,
  • 06.ItemTouchHelper
  • 07.SpanSizeLookup
    • SpanSizeLookup如何使用,同時包含清單,2列的網格,3列的網格如何優雅實作?
  • 08.ItemDecoration
    • ItemDecoration的用途,addItemDecoration()源碼分析
  • 09.RecycledViewPool
    • RecyclerViewPool用于多個RecyclerView之間共享View。
  • 11.RecyclerView上拉加載
    • 添加recyclerView的滑動事件,上拉加載分頁資料,設定上拉加載的底部footer布局,顯示和隐藏footer布局
  • 12.RecyclerView緩存原理
    • RecyclerView做性能優化要說複雜也複雜,比如說布局優化,緩存,預加載,複用池,重新整理資料等等
  • 13.SnapHelper源碼分析
    • SnapHelper旨在支援RecyclerView的對齊方式,也就是通過計算對齊RecyclerView中TargetView 的指定點或者容器中的任何像素點。
  • 16.自定義SnapHelper
    • 自定義SnapHelper
  • 19.自定義ItemDecoration分割線
    • 需要自定義類實作RecyclerView.ItemDecoration類,并選擇重寫合适方法
  • 22.RecyclerView問題彙總
    • getLayoutPosition()和getAdapterPosition()的差別
  • 23.RecyclerView滑動沖突
    • 01.如何判斷RecyclerView控件滑動到頂部和底部
    • 02.RecyclerView嵌套RecyclerView 條目自動上滾的Bug
    • 03.ScrollView嵌套RecyclerView滑動沖突
    • 04.ViewPager嵌套水準RecyclerView橫向滑動到底後不滑動ViewPager
    • 05.RecyclerView嵌套RecyclerView的滑動沖突問題
    • 06.RecyclerView使用Glide加載圖檔導緻圖檔錯亂問題解決
  • 24.ScrollView嵌套RecyclerView問題
    • 要實作在NestedScrollView中嵌入一個或多個RecyclerView,會出現滑動沖突,焦點搶占,顯示不全等。如何處理?

其他介紹

01.關于部落格彙總連結

02.關于我的部落格

代碼案例: