高仿新闻类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万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章