高仿新聞類APP頻道管理功能,ItemTouchHelper的實踐

轉載請標明出處:
http://blog.csdn.net/iamzgx/article/details/52843653
本文出自:【iGoach的博客】
在上篇博客 簡單仿TabLayout實現個性化Tab,讓Tab展現多樣化,通過HorizontalScrollView實現了類似TabLayout的功能,並且進行了紅點提醒,數字提醒的拓展功能。這種功能在新聞類APP是很常見的,還有一種很常見的功能在上一篇博客結尾也提到過,也就是頻道管理的功能。以常用的今日頭條爲例,頻道管理功能效果圖如下
這裏寫圖片描述

仔細玩下這裏的功能,這裏最難的點應該是它怎麼實現Item移動的。以前實現這種功能,網上有用GridView實現了這些功能,但是很複雜,而且實現的功能沒有這個這麼好看。那麼RecyclerView能不能更簡單的實現這項功能呢?ItemTouchHelper就是一個很好的item移動幫助類。這樣就能很好的去實現這項功能。下面就先來了解下它怎麼用。

ItemTouchHelper類

這裏我們需要使用的是ItemTouchHelper.Callback這個抽象類,它需要用到下面幾個方法:

boolean isLongPressDragEnabled()
boolean isItemViewSwipeEnabled()
int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target)
void onSwiped(RecyclerView.ViewHolder viewHolder, int i)

以上5個方法都是必須要重寫的,而下面2個方法是可選重寫的:

void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState)
void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder)
  • isLongPressDragEnabled返回的是一個boolean值,當boolean值爲true時,下面的makeMovementFlags方法的dragFlags值纔會起效,它具有上下拖動作用,返回false時則沒有任何效果。
  • isItemViewSwipeEnabled返回的也是一個boolean值,它和isLongPressDragEnabled類似。不同的是它控制的是左右滑動效果。
  • getMovementFlags方法返回的是一個int值,這個int值主要是makeMovementFlags(int
    dragFlags, int swipeFlags)方法返回的int值,其中makeMovementFlags需要傳遞兩個參數dragFlags和swipeFlags。dragFlags和swipeFlags是通過下面幾種方式結合
ItemTouchHelper.UP | ItemTouchHelper.DOWN
ItemTouchHelper.START | ItemTouchHelper.END
ItemTouchHelper.UP | ItemTouchHelper.DOWN|ItemTouchHelper.START | ItemTouchHelper.END

當然,如果我們不需要其中一個方向的效果,那麼參數直接傳0值就行了。

  • onMove方法,主要是拖動的時候,可以在這裏監聽進行數據更新的操作
  • onSwiped方法,主要是相鄰的item進行數據交換的數據更新。
  • onSelectedChanged和clearView主要是長按操作對象可以進行一些操作,比如放大縮小操作。

實現ItemTouchHelper.Callback

瞭解了ItemTouchHelper後,下面我們來實現下頻道管理移動效果需要用到的類ItemDragHelperCallback

先設計兩個接口來對ItemTouchHelper.CallBack的事件監聽

移動交換的數據更新監聽

public interface ItemDragListener {
    void onItemMove(int fromPosition, int toPosition);
    void onItemSwiped(int position);
}

開始移動和結束移動的事件監聽

public interface ItemDragVHListener {
    void onItemSelected();
    void onItemFinished();
}

再結合上面的介紹來一步一步實現ItemTouchHelper.CallBack的方法

public class ItemDragHelperCallback extends ItemTouchHelper.Callback {
    private ItemDragListener mDragMoveListener;

    public ItemDragHelperCallback(ItemDragListener listener) {
        mDragMoveListener = listener;
    }

    @Override
    public boolean isLongPressDragEnabled() {
        return false;
    }

    @Override
    public boolean isItemViewSwipeEnabled() {
        return false;
    }

    @Override
    public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        int dragFlags;
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if(layoutManager instanceof GridLayoutManager || layoutManager instanceof StaggeredGridLayoutManager){
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN|ItemTouchHelper.START | ItemTouchHelper.END;
        }else{
            dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
        }
        int swipeFlags = 0;
        return makeMovementFlags(dragFlags, swipeFlags);
    }

    @Override
    public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder source, RecyclerView.ViewHolder target) {
        if(source.getItemViewType()!=target.getItemViewType())
            return false;
        mDragMoveListener.onItemMove(source.getAdapterPosition(), target.getAdapterPosition());
        return true;
    }

    @Override
    public void onSwiped(RecyclerView.ViewHolder viewHolder, int i) {
        mDragMoveListener.onItemSwiped(viewHolder.getAdapterPosition());
    }

    @Override
    public void onSelectedChanged(RecyclerView.ViewHolder viewHolder, int actionState) {
        if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) {
            ItemDragVHListener itemViewHolder = (ItemDragVHListener) viewHolder;
            itemViewHolder.onItemSelected();
        }
        super.onSelectedChanged(viewHolder, actionState);
    }

    @Override
    public void clearView(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
        super.clearView(recyclerView, viewHolder);
        ItemDragVHListener itemViewHolder = (ItemDragVHListener) viewHolder;
        itemViewHolder.onItemFinished();
    }
}

這裏通過下面幾點解釋下上面的代碼

1,首先把監聽事件的ItemDragListener 傳遞進來,
2,把isLongPressDragEnabled的isItemViewSwipeEnabled返回改爲false,因爲在我的分類頻道里面可能前兩個Tab不能進行操作,如果返回true,那麼我的分類裏面的所有Tab就都可以移動了。就無法實現這種效果了。返回false的話,我們後續可以通過調用ItemTouchHelper的startDrag方法進行拖動操作,
3、在getMovementFlags方法裏面通過控制dragFlags的賦值來決定可移動的方向,如果RecyclerView的LayoutManager是GridLayoutManager或者StaggeredGridLayoutManager的話我們就可以上下左右進行移動,如果是LinearLayoutManager的話,就只能上下移動了。至於swipeFlags,暫時沒用到,所以這裏直接賦值爲0,最後調用makeMovementFlags(dragFlags, swipeFlags)方法即可,
4,onMove裏面有RecyclerView.ViewHolder source, RecyclerView.ViewHolder target這兩個參數,一個是操作對象的ViewHolder,一個是操作對象移動到最終位置對於的item對應的ViewHolder。這裏如果是兩個不同的ViewHolder,我們直接返回false,不對它進行更新數據操作,相同的ViewHolder就可以調用DragMoveListener接口的onItemMove後續進行更新數據,
5,onSelectedChanged方法裏面,我們首先通過actionState參數判斷RecyclerView是否在拖動,當不在拖動的情況下,通過viewHolder參數獲取ItemDragVHListener接口對象,然後調用ItemDragVHListener接口的onItemSelected方法來監聽Tab選中狀態,
6,clearView方法裏面,通過viewHolder參數獲取ItemDragVHListener接口對象,然後調用ItemDragVHListener接口的onItemFinished方法來監聽Tab取消選中狀態。

實現了ItemDragHelperCallback 之後,再通過下面幾個步驟

1,創建Callback對象

ItemDragHelperCallback itemDragHelperCallback = new ItemDragHelperCallback();

2,創建ItemTouchHelper對象

 ItemTouchHelper touchHelper = new ItemTouchHelper(itemDragHelperCallback);

3,touchHelper綁定對應的RecyclerView

touchHelper.attachToRecyclerView(mRecyclerView);

getItemViewType實戰

實現完了上面的ItemDragHelperCallback 對象之後,接下來我們就應該實現一下這個UI的基本佈局,
首先它整體是一個RecyclerView,它可以規劃爲4個Type:我的分類頭部,我的分類,推薦分類頭部,推薦分類。分析到這裏,我們就可以定義一個接口,把不同Type模塊代碼分離出來實現不同的佈局

public interface IChannelType {
    //我的頻道頭部部分
    int TYPE_MY_CHANNEL_HEADER = 0;
    //我的頻道部分
    int TYPE_MY_CHANNEL = 1;
    //推薦頭部部分
    int TYPE_REC_CHANNEL_HEADER = 2;
    //推薦部分
    int  TYPE_REC_CHANNEL = 3 ;

    ChannelAdapter.ChannelViewHolder createViewHolder(LayoutInflater mInflater, ViewGroup parent);
    void bindViewHolder(ChannelAdapter.ChannelViewHolder holder, int position, ChannelBean data);
}

接下來就是4個模塊都實現這個接口,然後進行佈局和數據的綁定,主要部分如下:

我的分類頭部模塊

public class MyChannelHeaderWidget implements IChannelType {
    private RecyclerView mRecyclerView;
    private EditModeHandler editModeHandler;
    public MyChannelHeaderWidget(EditModeHandler handler){
        this.editModeHandler = handler;
    }
    @Override
    public ChannelAdapter.ChannelViewHolder createViewHolder(LayoutInflater mInflater, ViewGroup parent) {
        mRecyclerView = (RecyclerView) parent;
        return new MyChannelHeaderViewHolder(mInflater.inflate(R.layout.activity_channel_my_header,parent,false));
    }

    @Override
    public void bindViewHolder(final ChannelAdapter.ChannelViewHolder holder, int position, ChannelBean data) {
       final MyChannelHeaderViewHolder viewHolder = (MyChannelHeaderViewHolder) holder;
        viewHolder.mEditModeTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(!viewHolder.mEditModeTv.isSelected()){
                    if(editModeHandler!=null)
                        editModeHandler.startEditMode(mRecyclerView);
                    viewHolder.mEditModeTv.setText("完成");
                }else{
                    if(editModeHandler!=null)
                        editModeHandler.cancelEditMode(mRecyclerView);
                    viewHolder.mEditModeTv.setText("編輯");
                }
                viewHolder.mEditModeTv.setSelected(!viewHolder.mEditModeTv.isSelected());
            }
        });
    }
    public class MyChannelHeaderViewHolder extends ChannelAdapter.ChannelViewHolder{
        private TextView mEditModeTv;
        public MyChannelHeaderViewHolder(View itemView) {
            super(itemView);
            mEditModeTv = (TextView) itemView.findViewById(R.id.id_edit_mode);
        }
    }
}

我的分類模塊

public class MyChannelWidget implements IChannelType {
    private RecyclerView mRecyclerView;
    private EditModeHandler editModeHandler;
    public MyChannelWidget(EditModeHandler editModeHandler){
        this.editModeHandler = editModeHandler;
    }
    @Override
    public ChannelAdapter.ChannelViewHolder createViewHolder(LayoutInflater mInflater, ViewGroup parent) {
        mRecyclerView = (RecyclerView) parent;
        return new MyChannelHeaderViewHolder(mInflater.inflate(R.layout.activity_channel_my,parent,false));
    }

    @Override
    public void bindViewHolder(final ChannelAdapter.ChannelViewHolder holder,final int position,final ChannelBean data) {
       final MyChannelHeaderViewHolder myHolder = (MyChannelHeaderViewHolder) holder;
        myHolder.mChannelTitleTv.setText(data.getTabName());
        int textSize = data.getTabName().length()>=4?14:16;
        myHolder.mChannelTitleTv.setTextSize(TypedValue.COMPLEX_UNIT_SP,textSize);
        myHolder.mChannelTitleTv.setBackgroundResource(data.getTabType()==0||data.getTabType()==1?
                R.drawable.channel_fixed_bg_shape:R.drawable.channel_my_bg_shape);
        myHolder.mChannelTitleTv.setTextColor(data.getTabType()==0?Color.RED:
                data.getTabType()==1?Color.parseColor("#666666"):Color.parseColor("#333333"));
        myHolder.mDeleteIv.setVisibility(data.getEditStatus()==1?View.VISIBLE:View.INVISIBLE);
        myHolder.mChannelTitleTv.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if(editModeHandler!=null&&data.getTabType()==2){
                    editModeHandler.clickMyChannel(mRecyclerView,holder);
                }
            }
        });
        myHolder.mChannelTitleTv.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View view, MotionEvent motionEvent) {
                if(editModeHandler!=null&&data.getTabType()==2){
                    editModeHandler.touchMyChannel(motionEvent,holder);
                }
                return false;
            }
        });
        myHolder.mChannelTitleTv.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                if(editModeHandler!=null&&data.getTabType()==2){
                    editModeHandler.clickLongMyChannel(mRecyclerView,holder);
                }
                return true;
            }
        });
    }
    public class MyChannelHeaderViewHolder extends ChannelAdapter.ChannelViewHolder{
        private TextView mChannelTitleTv;
        private ImageView mDeleteIv;
        private MyChannelHeaderViewHolder(View itemView) {
            super(itemView);
            mChannelTitleTv = (TextView) itemView.findViewById(R.id.id_channel_title);
            mDeleteIv = (ImageView) itemView.findViewById(R.id.id_delete_icon);
        }
    }
}

推薦分類頭部模塊和推薦分類代碼和上面的類似,這裏就不一一貼出來了,以防代碼過多。

通過查看代碼我們會發現,我們傳遞了一個EditModeHandler抽象類出來,這個抽象類主要是抽象了各個模塊的點擊事件,然後在RecyclerView.Adapter裏面統一處理,主要的點擊事件有如下:

public abstract class EditModeHandler {
    //開始編輯處理的事件
    public void startEditMode(RecyclerView mRecyclerView){}
    //取消編輯完成狀態的事件
    public void cancelEditMode(RecyclerView mRecyclerView){}
    //點擊我的分類裏面item事件
    public void clickMyChannel(RecyclerView mRecyclerView,ChannelAdapter.ChannelViewHolder holder){}
    //長按我的分類裏面item事件
    public void clickLongMyChannel(RecyclerView mRecyclerView,ChannelAdapter.ChannelViewHolder holder){}
    //手機觸摸我的分類裏面item事件
    public void touchMyChannel(MotionEvent motionEvent, ChannelAdapter.ChannelViewHolder holder){}
    //點擊推薦分類裏面的item事件
    public void clickRecChannel(RecyclerView mRecyclerView,ChannelAdapter.ChannelViewHolder holder){}
}

實現了各個模塊佈局和數據綁定之後,接下來我們要在RecyclerView.Adapter裏面把這些模塊通過getItemViewType進行綁定。
首先定義一個SparseArray,存儲各個模塊,

private SparseArray<IChannelType> mTypeMap = new SparseArray();
mTypeMap.put(IChannelType.TYPE_MY_CHANNEL_HEADER,new MyChannelHeaderWidget(new EditHandler()));
mTypeMap.put(IChannelType.TYPE_MY_CHANNEL,new MyChannelWidget(new EditHandler()));
mTypeMap.put(IChannelType.TYPE_REC_CHANNEL_HEADER,new RecChannelHeaderWidget());
mTypeMap.put(IChannelType.TYPE_REC_CHANNEL,new RecChannelWidget(new EditHandler()));

然後getItemViewType返回不同的類型

 @Override
public int getItemViewType(int position) {
    if(position<mMyHeaderCount)
        return IChannelType.TYPE_MY_CHANNEL_HEADER;
    if(position>=mMyHeaderCount&&position<mMyChannelItems.size()+mMyHeaderCount)
        return IChannelType.TYPE_MY_CHANNEL;
    if(position>=mMyChannelItems.size()+mMyHeaderCount&&position<mMyChannelItems.size()+mMyHeaderCount+mRecHeaderCount)
        return IChannelType.TYPE_REC_CHANNEL_HEADER;
    return IChannelType.TYPE_REC_CHANNEL;
}

其中mMyHeaderCount我的分類頭部總量,這裏爲1,mMyChannelItems爲我的分類裏面的Tab數據,mRecHeaderCount爲推薦分類頭部總量,這裏也爲1,
最後我們再調用對應的實現IChannelType接口模塊的createViewHolder方法和bindViewHolder方法。

   @Override
    public ChannelViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return mTypeMap.get(viewType).createViewHolder(mInflater,parent);
    }

    @Override
    public void onBindViewHolder(ChannelViewHolder holder, int position) {
        if(getItemViewType(position)==IChannelType.TYPE_MY_CHANNEL){
            int myPosition = position-mMyHeaderCount;
            myPosition = myPosition<0||myPosition>=mMyChannelItems.size()?0:myPosition;
            mTypeMap.get(getItemViewType(position)).bindViewHolder(holder,position,mMyChannelItems.get(myPosition));
            return;
        }
        if(getItemViewType(position)==IChannelType.TYPE_REC_CHANNEL){
            int otherPosition = position-mMyChannelItems.size()-mMyHeaderCount-mRecHeaderCount;
            otherPosition = otherPosition<0||otherPosition>=mOtherChannelItems.size()?0:otherPosition;
            mTypeMap.get(getItemViewType(position)).bindViewHolder(holder,position,mOtherChannelItems.get(otherPosition));
            return;
        }
        mTypeMap.get(getItemViewType(position)).bindViewHolder(holder,position,null);
    }

到這裏,基本的佈局就完成了。

實現頻道管理效果

首先,我們需要實現點擊我的分類頭部的“完成/編輯”按鈕,然後切換不同的編輯狀態,這裏的變化主要是可編輯狀態時,我的分類頭部提示文案修改,以及我的分類Tab增加刪除Icon,對應的抽象點擊事件爲startEditMode和cancelEditMode,所以定義一個繼承EditModeHandler的類EditHandler,重寫這兩個事件。代碼如下

 private class EditHandler extends EditModeHandler{
        @Override
        public void startEditMode(RecyclerView mRecyclerView) {
            doStartEditMode(mRecyclerView);
        }
        @Override
        public void cancelEditMode(RecyclerView mRecyclerView) {
            doCancelEditMode(mRecyclerView);
        }
  }
    private void doStartEditMode(RecyclerView parent) {
        isEditMode = true;
        int visibleChildCount = parent.getChildCount();
        for (int i = 0; i < visibleChildCount; i++) {
            View view = parent.getChildAt(i);
            ImageView imgEdit = (ImageView) view.findViewById(R.id.id_delete_icon);
            if (imgEdit != null) {
                ChannelBean item = mMyChannelItems.get(i - mMyHeaderCount);
                if(item.getTabType() == 2 ){
                    imgEdit.setVisibility(View.VISIBLE);
                }else{
                    imgEdit.setVisibility(View.INVISIBLE);
                }
            }
        }
    }
    private void doCancelEditMode(RecyclerView parent) {
        isEditMode = false;
        int visibleChildCount = parent.getChildCount();
        for (int i = 0; i < visibleChildCount; i++) {
            View view = parent.getChildAt(i);
            ImageView imgEdit = (ImageView) view.findViewById(R.id.id_delete_icon);
            if (imgEdit != null) {
                imgEdit.setVisibility(View.INVISIBLE);
            }
        }
    }

主要是通過RecyclerView獲取RecyclerView的所有子View,然後通過子View查找佈局裏面id爲id_delete_icon的View,如果查找到了,可編輯狀態且Tab類型爲2(通過定義Tab類型控制我的分類前兩個Tab永遠不可編輯)的情況下VISIBLE,不可編輯狀態則INVISIBLE

實現了狀態切換之後,我們繼續實現移除分類的功能,這項功能對應的抽象點擊事件爲clickMyChannel。所以繼續在EditModeHandler類裏面從寫這個方法

  @Override
        public void clickMyChannel(RecyclerView mRecyclerView,ChannelAdapter.ChannelViewHolder holder) {
            RecyclerView.LayoutManager layoutManager = mRecyclerView.getLayoutManager();
            int position = holder.getAdapterPosition();
            if(isEditMode){
                View targetView = layoutManager.findViewByPosition(mMyChannelItems.size()
                        +mMyHeaderCount+mRecHeaderCount);
                View currentView = mRecyclerView.getLayoutManager().findViewByPosition(position);
                int targetX ;
                int targetY;
                if(mRecyclerView.indexOfChild(targetView)>=0){
                    int spanCount = ((GridLayoutManager)layoutManager).getSpanCount();
                    targetX = targetView.getLeft();
                    targetY = targetView.getTop();
                    if ((mMyChannelItems.size()) % spanCount == 1) {
                        View preTargetView = layoutManager.findViewByPosition(mMyChannelItems.size()
                                + mMyHeaderCount+mRecHeaderCount - 1);
                        targetX = preTargetView.getLeft();
                        targetY = preTargetView.getTop();
                    }
                }else{
                    View preTargetView = layoutManager.findViewByPosition(mMyChannelItems.size()
                            + mMyHeaderCount+ mRecHeaderCount- 1);
                    targetX = preTargetView.getLeft();
                    targetY = preTargetView.getTop()+preTargetView.getHeight()+APPConst.ITEM_SPACE;
                }
                moveMyToOther(position);
                startAnimation(mRecyclerView, currentView, targetX, targetY);
            }else{
                if(channelItemClickListener!=null){
                    channelItemClickListener.onChannelItemClick(mMyChannelItems,position-mMyHeaderCount);
                }
            }
        }

這個方法裏面通過isEditMode獲取當前編輯狀態,如果爲不可編輯狀態,那麼點擊我的分類Tab,我們直接結束當前的DialogFragment,然後切換到首頁相應的Tab對應的頁面就行了。如果爲可編輯狀態,點擊的話,那就是移除當前點擊的Tab,同時把移除的Tab添加到推薦分類的第一位。直接操作mMyChannelItems和mOtherChannelItems進行數據源更新然後通過RecyclerView的notifyItemMoved(int fromPosition, int toPosition)是沒有從fromPosition到toPosition移動的動畫,所以這裏再給它添加一個移動的動畫,這樣我們就要進行動畫初始位置和結束位置的計算。計算過程爲
首先獲取當前操作的View和移動到最終位置也就是推薦分類第一個Tab的View

View  currentView = mRecyclerView.getLayoutManager().findViewByPosition(position);
View targetView = layoutManager.findViewByPosition(mMyChannelItems.size()
                        +mMyHeaderCount+mRecHeaderCount);

這樣當前位置通過currentView.getLeft()和currentView.getTop()就獲取到了,而最終位置如果移除後有換行或者targetView不存在的話位置是可變的,所以這裏要判斷下targetView是否存在,如果不存在,則要通過targetView的前一個item來計算最終的位置,計算代碼如上。
如果targetView存在的話,那麼就要判斷移除後是否會換行,如果不換行直接取targetView .getLeft()和targetView .getTop(),如果換行就要取targetView的前一個item位置。
最後我們需要更新數據源

private void moveMyToOther(int position) {
        int myPosition = position - mMyHeaderCount;
        ChannelBean item = mMyChannelItems.get(myPosition);
        mMyChannelItems.remove(myPosition);
        mOtherChannelItems.add(0, item);
        notifyItemMoved(position, mMyChannelItems.size() + mMyHeaderCount+mRecHeaderCount);
    }

在實現移動的動畫之前,還需要對當前操作的currentView生成鏡像

/**
 * 我們要獲取cache首先要通過setDrawingCacheEnable方法開啓cache,然後再調用getDrawingCache方法就可以獲得view的cache圖片了。
 buildDrawingCache方法可以不用調用,因爲調用getDrawingCache方法時,若果cache沒有建立,系統會自動調用buildDrawingCache方法生成cache。
 若想更新cache, 必須要調用destoryDrawingCache方法把舊的cache銷燬,才能建立新的。
 當調用setDrawingCacheEnabled方法設置爲false, 系統也會自動把原來的cache銷燬。
 */
private ImageView addMirrorView(ViewGroup parent, RecyclerView recyclerView, View view) {
    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];
    parent.getLocationOnScreen(parenLocations);
    RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(bitmap.getWidth(), bitmap.getHeight());
    params.setMargins(locations[0], locations[1] - parenLocations[1], 0, 0);
    parent.addView(mirrorView, params);
    return mirrorView;
}

接下來就是實現動畫

    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) {
            }
        });
    }
    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);
        // RecyclerView默認移動動畫250ms 這裏設置360ms 是爲了防止在位移動畫結束後 remove(view)過早 導致閃爍
        translateAnimation.setDuration(360);
        translateAnimation.setFillAfter(true);
        return translateAnimation;
    }

實現了移除頻道之後,繼續實現增加分類的功能,這項功能對應的抽象點擊事件爲clickRecChannel。所以繼續在EditModeHandler類裏面從寫這個方法

      @Override
        public void clickRecChannel(RecyclerView mRecyclerView, ChannelViewHolder holder) {
            GridLayoutManager  layoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
            int position = holder.getAdapterPosition();
            View targetView = layoutManager.findViewByPosition(mMyChannelItems.size() +mMyHeaderCount-1);
            View currentView = mRecyclerView.getLayoutManager().findViewByPosition(position);
            if(mRecyclerView.indexOfChild(targetView)>=0){
                int targetX = targetView.getLeft();
                int targetY = targetView.getTop();
                int spanCount = layoutManager.getSpanCount();
                View nextTargetView = layoutManager.findViewByPosition(mMyChannelItems.size() +mMyHeaderCount);
                if (mMyChannelItems.size() % spanCount == 0) {
                    targetX = nextTargetView.getLeft();
                    targetY = nextTargetView.getTop();
                }else{
                    targetX += targetView.getWidth() + 2* APPConst.ITEM_SPACE;
                }
                moveOtherToMy(position);
                startAnimation(mRecyclerView, currentView, targetX, targetY);
            }else{
                moveOtherToMy(position);
            }
        }
    private void moveMyToOther(int position) {
        int myPosition = position - mMyHeaderCount;
        ChannelBean item = mMyChannelItems.get(myPosition);
        mMyChannelItems.remove(myPosition);
        mOtherChannelItems.add(0, item);
        notifyItemMoved(position, mMyChannelItems.size() + mMyHeaderCount+mRecHeaderCount);
    }
    private void moveOtherToMy(int position) {
        int recPosition = processItemRemoveAdd(position);
        if (recPosition == -1) {
            return;
        }
        notifyItemMoved(position, mMyChannelItems.size() + mMyHeaderCount-1);
    }
    private int processItemRemoveAdd(int position) {
        int startPosition = position - mMyChannelItems.size() - mRecHeaderCount-mMyHeaderCount;
        if (startPosition > mOtherChannelItems.size() - 1) {
            return -1;
        }
        ChannelBean item = mOtherChannelItems.get(startPosition);
        item.setEditStatus(isEditMode?1:0);
        mOtherChannelItems.remove(startPosition);
        mMyChannelItems.add(item);
        return position;
    }

這個方法和上面移除頻道類似,主要不同的就是更新數據源不同以及計算動畫起始位置和終點位置計算不同,更新數據源不同的是,當我的分類爲可編輯狀態時,我們要改變添加Item的編輯狀態,不可編輯則不用,這裏通過改變數據源裏面的EditStatus來改變編輯狀態。主要也是終點位置,也就是我的分類最後一個item的位置的計算,如果不換行的話,那就是當前targetView的getLeft加上targetView的寬度加上Item之間的間距,就可以計算出來了,如果換行的話,那就計算下一個item的位置

實現了增加頻道之後,繼續實現改變我的分類Item的順序的功能,這項功能對應的抽象點擊事件不可編輯狀態的clickLongMyChannel和可編輯狀態時的touchMyChannel。所以繼續在EditModeHandler類裏面從寫這兩個個方法
首先是clickLongMyChannel

  @Override
  public void clickLongMyChannel(RecyclerView mRecyclerView, ChannelViewHolder holder) {
      if(!isEditMode){
          doStartEditMode(mRecyclerView);
          View view = mRecyclerView.getChildAt(0);
          if(view == mRecyclerView.getLayoutManager().findViewByPosition(0)){
              TextView dragTip = (TextView) view.findViewById(R.id.id_my_header_tip_tv);
              dragTip.setText("拖拽可以排序");

              TextView tvBtnEdit = (TextView) view.findViewById(R.id.id_edit_mode);
              tvBtnEdit.setText("完成");
              tvBtnEdit.setSelected(true);
          }
          mItemTouchHelper.startDrag(holder);
      }
  }

這裏方法首先要改變的是我的分類改爲可編輯狀態,以及修改我的分類頭部提示文案,然後就是調用mItemTouchHelper.startDrag來進行拖動。

然後就是touchMyChannel

@Override
 public void touchMyChannel(MotionEvent motionEvent, ChannelViewHolder holder) {
     if (!isEditMode) {
         return;
     }
     switch (MotionEventCompat.getActionMasked(motionEvent)) {
         case MotionEvent.ACTION_DOWN:
             startTime = System.currentTimeMillis();
             break;
         case MotionEvent.ACTION_MOVE:
             if (System.currentTimeMillis() - startTime > SPACE_TIME) {
                 mItemTouchHelper.startDrag(holder);
             }
             break;
         case MotionEvent.ACTION_CANCEL:
         case MotionEvent.ACTION_UP:
             startTime = 0;
             break;
     }
 }

這個方法主要是當手指按下拖拽時間達到100ms,就調用mItemTouchHelper.startDrag(holder)進行拖拽item。

以上兩個方法的前提是,需要再RecyclerView.Adapter裏面初始化mItemTouchHelper以及實現ItemDragListener接口。
初始化代碼主要是

this.mItemTouchHelper = new ItemTouchHelper(new ItemDragHelperCallback(this));
mItemTouchHelper.attachToRecyclerView(recyclerView);

上文也有提到過,接下來就是在ItemDragListener實現的兩個接口方法裏面進行頻道順序數據的更新,代碼如下

  @Override
    public void onItemMove(int fromPosition, int toPosition) {
        if(toPosition > 2){
            ChannelBean item = mMyChannelItems.get(fromPosition - mMyHeaderCount);
            mMyChannelItems.remove(fromPosition - mMyHeaderCount);
            mMyChannelItems.add(toPosition - mMyHeaderCount, item);
            notifyItemMoved(fromPosition, toPosition);
        }
    }
    @Override
    public void onItemSwiped(int position) {
    }

當它調用onItemMove方法的時候,我的分類後面2個item的都進行更新。onItemSwiped暫時沒用到。

接下來爲了item選中狀態更明顯,當選中的時候進行放大效果,如果取消選中之後則還原,這個就要在RecyclerView.ViewHolder實現ItemDragVHListener,在ItemDragVHListener的兩個方法裏面實現

 @Override
        public void onItemSelected() {
            scaleItem(1.0f , 1.2f , 0.5f);
        }

        @Override
        public void onItemFinished() {
            scaleItem(1.2f , 1.0f , 1.0f);
        }

scaleItem的動畫爲

     public void scaleItem(float start , float end , float alpha) {
            ObjectAnimator anim1 = ObjectAnimator.ofFloat(itemView, "scaleX",
                    start, end);
            ObjectAnimator anim2 = ObjectAnimator.ofFloat(itemView, "scaleY",
                    start, end);
            ObjectAnimator anim3 = ObjectAnimator.ofFloat(itemView, "alpha",
                    alpha);

            AnimatorSet animSet = new AnimatorSet();
            animSet.setDuration(200);
            animSet.setInterpolator(new LinearInterpolator());
            animSet.playTogether(anim1, anim2 ,anim3);
            animSet.start();
        }

這樣我們就實現了所有的效果,最後看下效果

這裏寫圖片描述

gif有點不太流暢,感興趣的可以下載源碼查看效果,源碼基於上篇博客代碼

源碼下載

發佈了56 篇原創文章 · 獲贊 50 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章