ItemTouchHelper的实践

频道管理的功能,在新闻类APP是很常见的.频道管理功能效果图如下


以前实现这种功能,网上有用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.DOWNItemTouchHelper.START |  
ItemTouchHelper.ENDItemTouchHelper.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();

}

这样我们就实现了所有的效果,最后看下效果


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容