使用ItemTouchHelper高效地實(shí)現(xiàn) 今日頭條 、網(wǎng)易新聞 的頻道排序、移動(dòng)

主要效果圖

使用RecyclerView配合ItemTouchHelper實(shí)現(xiàn),性能更好、更流暢!
支持大數(shù)量item的情況(即 RecyclerView內(nèi)容較多,可滑動(dòng)的情況)
下載Demo

僅供參考,實(shí)際使用建議使用類(lèi)似BRVAH的庫(kù)再封裝下

部分效果演示.gif

主要功能

在普通模式下,長(zhǎng)按“我的頻道”的item,可以拖拽排序并進(jìn)入編輯模式

在編輯模式下,觸摸“我的頻道”的item,可以直接拖拽排序

在任意模式下,點(diǎn)擊“其他頻道”的item,移動(dòng)到“我的頻道”,并伴隨移動(dòng)動(dòng)畫(huà)

在編輯模式下,點(diǎn)擊“我的頻道”的item,移動(dòng)到“其他頻道”,并伴隨移動(dòng)動(dòng)畫(huà)

實(shí)現(xiàn)思路

一、實(shí)現(xiàn)拖拽排序
3種方式

1、WindowManager
我在之前的項(xiàng)目中使用的方式,大致思路是:獲取需要拖拽的View,生成鏡像View,添加到WindowManager,移動(dòng)時(shí),通過(guò)Touch事件的X、Y坐標(biāo),利用windowManager的updateViewLayout方法更新位置。需要自己維護(hù)onInterceptTouchEvent、onTouchEvent,并且在拖拽的item移動(dòng)的高度超過(guò)一屏?xí)r,需要手動(dòng)控制RecyclerView(ListView/GridView)的滾動(dòng),較為繁瑣。

2、View的startDrag方法配合setOnDragListener
這種方式不需要處理RecyclerView(ListView/GridView)的onInterceptTouchEvent和onTouchEvent,實(shí)現(xiàn)起來(lái)更方便一些,詳情可以參考官方教程(Drag & Drop)。

3、使用RecyclerView包的ItemTouchHelper
Demo使用的方式。只能用RecyclerView實(shí)現(xiàn),但是性能、功能都很強(qiáng)大,實(shí)現(xiàn)也非常簡(jiǎn)單,ItemTouchHelper處理好了關(guān)于在RecyclerView上添加拖動(dòng)排序與滑動(dòng)刪除的所有事情。
通過(guò)ItemTouchHelper.Callback的onMove回調(diào)方法,對(duì)數(shù)組集合進(jìn)行交換位置,并通過(guò)notifyItemMove方法刷新界面,RecyclerView默認(rèn)的item動(dòng)畫(huà)為DefaultItemAnimator,它的notifyItemMove方法使范圍內(nèi)item有一個(gè)很自然的位移動(dòng)畫(huà)。

二、實(shí)現(xiàn)不同Grid的item之間移動(dòng)(伴隨位移動(dòng)畫(huà))
不同Grid的item之間移動(dòng).png

這部分是Demo中邏輯最復(fù)雜的部分,移動(dòng)的同時(shí)還要排序,并且Demo考慮了內(nèi)容特別多(RecyclerView可滑動(dòng))的情況下的移動(dòng)。

Demo實(shí)現(xiàn)的思路是依靠notifyItemMove方法實(shí)現(xiàn) 需要移動(dòng)的item 的后面各個(gè)item的移動(dòng)效果,例如在上圖中,即item4、item5向左方向的移動(dòng)動(dòng)畫(huà),但是并不會(huì)有下方item3向上方item3移動(dòng)的位移動(dòng)畫(huà),所以這里還需要使用位移動(dòng)畫(huà)實(shí)現(xiàn)該效果。

三、狀態(tài)-普通模式、編輯模式

普通模式下,邏輯很簡(jiǎn)單,長(zhǎng)按“我的頻道”的item,可以拖拽排序并進(jìn)入編輯模式;
編輯模式下,可以直接拖拽“我的頻道”的item,同時(shí)保證點(diǎn)擊事件可用以及不能影響RecyclerView的滑動(dòng),Demo的解決方式是,對(duì)item設(shè)置setTouchListener事件,當(dāng)MOVE事件與DOWN事件的觸發(fā)的間隔時(shí)間大于100ms時(shí),則認(rèn)為是拖拽starDrag,小于100ms不做任何處理,return false。

核心代碼

拖拽排序:

使用 ItemTouchHelper 和 ItemTouchHelper.Callback


/**
 * ItemDragHelperCallback
 * Created by YoKeyword on 15/12/29.
 */
public class ItemDragHelperCallback extends ItemTouchHelper.Callback {

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags;
        RecyclerView.LayoutManager manager = recyclerView.getLayoutManager();
        if (manager instanceof GridLayoutManager || manager instanceof StaggeredGridLayoutManager) {
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN | ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
        } else {
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        }
        // 如果想支持滑動(dòng)(刪除)操作, swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END
        int swipeFlags = 0;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
        // 不同Type之間不可移動(dòng)
        if (viewHolder.getItemViewType() != target.getItemViewType()) {
            return false;
        }

        if (recyclerView.getAdapter() instanceof OnItemMoveListener) {
            OnItemMoveListener listener = ((OnItemMoveListener) recyclerView.getAdapter());
            listener.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
        }
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {}

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        // 不在閑置狀態(tài)
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            if (viewHolder instanceof OnDragVHListener) {
                OnDragVHListener itemViewHolder = (OnDragVHListener) viewHolder;
                itemViewHolder.onItemSelected();
            }
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        if (viewHolder instanceof OnDragVHListener) {
            OnDragVHListener itemViewHolder = (OnDragVHListener) viewHolder;
            itemViewHolder.onItemFinish();
        }
        super.clearView(recyclerView, viewHolder);
    }

    @Override
    public boolean isLongPressDragEnabled() {
        // 不需要長(zhǎng)按拖拽功能  我們手動(dòng)控制
        return false;
    }

    @Override
    public boolean isItemViewSwipeEnabled() {
        // 不需要滑動(dòng)功能
        return false;
    }
}

getMovementFlags()可以指定需要拖拽的方向;

isLongPressDragEnabled()如果返回true,則支持長(zhǎng)按拖拽,該Demo中,“其他頻道”等不需要拖拽,所以返回false,手動(dòng)調(diào)用ItemTouchHelper的startDrag方法啟動(dòng)拖拽。

onMove()是在拖動(dòng)到新位置時(shí)候的回調(diào)方法,我們?cè)谶@里做數(shù)組集合的交換操作,在這里我們把它暴漏出去,交給Adapter自己處理;
一般來(lái)說(shuō),實(shí)現(xiàn)拖拽排序的寫(xiě)法為:

@Override
    public void onItemMove(int fromPosition, int toPosition) {
        String item = mItems.get(fromPosition);
        mItems.remove(fromPosition);
        mItems.add(toPosition , item);
        notifyItemMoved(fromPosition, toPosition);
    }

onSelectedChanged()方法和clearView()方法,分別在item被選中以及取消選中的時(shí)候調(diào)用,這里同樣將它們以接口暴漏出去,在Adapter的ViewHolder里實(shí)現(xiàn)接口,讓item在選中時(shí)高亮;

    //我的頻道
    class MyViewHolder extends RecyclerView.ViewHolder implements OnDragVHListener {
        private TextView textView;
        private ImageView imgEdit;

        public MyViewHolder(View itemView) {
            super(itemView);
            textView = (TextView) itemView.findViewById(R.id.tv);
            imgEdit = (ImageView) itemView.findViewById(R.id.img_edit);
        }

        // item 被選中時(shí)
        @Override
        public void onItemSelected() {
            textView.setBackgroundResource(R.drawable.bg_channel_p);
        }

        // item 取消選中時(shí)
        @Override
        public void onItemFinish() {
            textView.setBackgroundResource(R.drawable.bg_channel);
        }
    }

最終在Activity中,調(diào)用:

ItemDragHelperCallback callback = new ItemDragHelperCallback();
ItemTouchHelper touchHelper = new ItemTouchHelper(callback);
touchHelper.attachToRecyclerView(recyclerView);

以上部分,可參照Demo1

不同Grid的item的移動(dòng)(item的刪除和添加)

Demo中僅僅使用一個(gè)RecyclerView實(shí)現(xiàn),ViewType如下圖所示:

RecyclerView的ViewType.png

首先是需要移動(dòng)的item的位移動(dòng)畫(huà)(即"不同Grid的item之間移動(dòng).png"圖中的item3),因?yàn)閕tem3向上方移動(dòng)的動(dòng)畫(huà)以及item4、item5向左移動(dòng)的動(dòng)畫(huà)是同時(shí)的,并且我們使用的notifyItemMove自帶的動(dòng)畫(huà),所以我們要在調(diào)用notifyItemMove()的同時(shí),啟動(dòng)item3的位移動(dòng)畫(huà)。
所以我們需要制造一個(gè)item3的鏡像ImageView,添加到recyclerView的父控件中,直接控制item3進(jìn)行位移不會(huì)起作用,因?yàn)閚otifyItemMove的時(shí)候,RecyclerView處于動(dòng)畫(huà)和充繪界面中,item3并不受控制,并且因?yàn)镽ecyclerView的子控件的層級(jí)問(wèn)題,當(dāng)上方item向下方移動(dòng)時(shí),會(huì)被遮擋。

生成鏡像ImageView代碼如下:

/**
     * 添加需要移動(dòng)的 鏡像View
     */
    private ImageView addMirrorView(ViewGroup parent, RecyclerView recyclerView, View view) {
        /**
         * 我們要獲取cache首先要通過(guò)setDrawingCacheEnable方法開(kāi)啟cache,然后再調(diào)用getDrawingCache方法就可以獲得view的cache圖片了。
         buildDrawingCache方法可以不用調(diào)用,因?yàn)檎{(diào)用getDrawingCache方法時(shí),若果cache沒(méi)有建立,系統(tǒng)會(huì)自動(dòng)調(diào)用buildDrawingCache方法生成cache。
         若想更新cache, 必須要調(diào)用destoryDrawingCache方法把舊的cache銷(xiāo)毀,才能建立新的。
         當(dāng)調(diào)用setDrawingCacheEnabled方法設(shè)置為false, 系統(tǒng)也會(huì)自動(dòng)把原來(lái)的cache銷(xiāo)毀。
         */
        view.destroyDrawingCache();
        view.setDrawingCacheEnabled(true);

        final ImageView mirrorView = new ImageView(recyclerView.getContext());
        Bitmap bitmap = Bitmap.createBitmap(view.getDrawingCache());
        mirrorView.setImageBitmap(bitmap);
        view.setDrawingCacheEnabled(false);

        int[] locations = new int[2];
        view.getLocationOnScreen(locations);
        int[] parenLocations = new int[2];
        recyclerView.getLocationOnScreen(parenLocations);
        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight());
        params.setMargins(locations[0], locations[1] - parenLocations[1], 0, 0);

        parent.addView(mirrorView, params);

        return mirrorView;
    }

最終,鏡像ImageView啟動(dòng)位移動(dòng)畫(huà)的同時(shí),調(diào)用notifyItemMove:

private void startAnimation(RecyclerView recyclerView, final View currentView, float targetX, float targetY) {
        final ViewGroup viewGroup = (ViewGroup) recyclerView.getParent();
        final ImageView mirrorView = addMirrorView(viewGroup, recyclerView, currentView);

        Animation animation = getTranslateAnimator(
                targetX - currentView.getLeft(), targetY - currentView.getTop());
        currentView.setVisibility(View.INVISIBLE);
        mirrorView.startAnimation(animation);

        animation.setAnimationListener(new Animation.AnimationListener() {
            @Override
            public void onAnimationStart(Animation animation) {
            }

            @Override
            public void onAnimationEnd(Animation animation) {
                viewGroup.removeView(mirrorView);
                if (currentView.getVisibility() == View.INVISIBLE) {
                    currentView.setVisibility(View.VISIBLE);
                }
            }

            @Override
            public void onAnimationRepeat(Animation animation) {

            }
        });
    }

    /**
     * 獲取位移動(dòng)畫(huà)
     */
    private TranslateAnimation getTranslateAnimator(float targetX, float targetY) {
        TranslateAnimation translateAnimation = new TranslateAnimation(
                Animation.RELATIVE_TO_SELF, 0f,
                Animation.ABSOLUTE, targetX,
                Animation.RELATIVE_TO_SELF, 0f,
                Animation.ABSOLUTE, targetY);
        translateAnimation.setDuration(ANIM_TIME);
        translateAnimation.setFillAfter(true);
        return translateAnimation;
    }

邏輯最復(fù)雜的部分來(lái)了:如何獲取移動(dòng)目標(biāo)的位置?
比如:“我的頻道”的item移動(dòng)到"其他頻道",這種情況比較簡(jiǎn)單,因?yàn)榭偸且苿?dòng)到“其他頻道”的第一個(gè)item
正常情況下,可以這樣獲?。?/p>

View targetView = recyclerView.getLayoutManager().findViewByPosition(mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER);

targetX = targetView.getLeft();
targetY = targetView.getTop();

但是當(dāng)item足夠多的時(shí)候,一屏幕不能容納的時(shí)候,會(huì)有下面的情況:


這時(shí),item x+4 向下移動(dòng)的同時(shí),RecyclerView同時(shí)會(huì)向下滾動(dòng),導(dǎo)致“其他頻道”的內(nèi)容向上移動(dòng),這時(shí)再使用上面的方式獲取目標(biāo)位置就不正確了,要這樣獲取:

// 移動(dòng)后 高度將變化 (我的頻道Grid 最后一個(gè)item在新的一行第一個(gè))
if ((mMyChannelItems.size() - COUNT_PRE_MY_HEADER) % spanCount == 0) {
    View preTargetView = recyclerView.getLayoutManager().findViewByPosition(mMyChannelItems.size() + COUNT_PRE_OTHER_HEADER - 1);
    targetX = preTargetView.getLeft();
    targetY = preTargetView.getTop();} 

同樣的道理,“其他頻道”移動(dòng)到“我的頻道”一樣的處理方式,不過(guò)細(xì)節(jié)更多、更復(fù)雜些,這里就不說(shuō)明了,感興趣的可以在文章最后查看源碼。

編輯模式下的Touch事件傳遞

當(dāng)MOVE事件與DOWN事件的觸發(fā)的間隔時(shí)間大于100ms時(shí),則認(rèn)為是拖拽starDrag,小于100ms不做任何處理,return false。這樣item的點(diǎn)擊事件、RecyclerView的滾動(dòng)事件都可以正常執(zhí)行。

 myHolder.textView.setOnTouchListener(new View.OnTouchListener() {
                    @Override
                    public boolean onTouch(View v, MotionEvent event) {
                        if (isEditMode) {
                            switch (MotionEventCompat.getActionMasked(event)) {
                                case MotionEvent.ACTION_DOWN:
                                    startTime = System.currentTimeMillis();
                                    break;
                                case MotionEvent.ACTION_MOVE:
                                    if (System.currentTimeMillis() - startTime > SPACE_TIME) {
                                        mItemTouchHelper.startDrag(myHolder);
                                    }
                                    break;
                                case MotionEvent.ACTION_CANCEL:
                                case MotionEvent.ACTION_UP:
                                    startTime = 0;
                                    break;
                            }

                        }
                        return false;
                    }
                });

總結(jié)

Demo里對(duì)于頻道的排序和移動(dòng),要考慮的細(xì)節(jié)還是挺多的,但是理清好思路,解決起來(lái)并不是很困難。
從最終Demo效果來(lái)看,RecyclerView配合ItemTouchHelper的實(shí)現(xiàn)方式,確實(shí)比今日頭條、網(wǎng)易新聞 性能更高效、動(dòng)畫(huà)更流暢。

完整源碼、Demo下載

Demo地址
完整源碼在GitHub

最后編輯于
?著作權(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),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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