今天咱们来学习一下RecyclerView
另外一个不为人知的辅助类--ItemTouchHelper
。咱们在作列表视图,就好比说,ListView
或者RecyclerView
,一般会有两种需求:1. 侧滑删除;2. 拖动交换位置。对于第一种需求使用传统的版本实现还比较简单,咱们能够自定义ItemView
来实现;而第二种的话,可能就稍微有一点复杂,可能须要重写LayoutManager
。git
这些办法也不否定是有效的解决方案,可是是不是简单和低耦合性的办法呢?固然不是,踩过坑的同窗应该都知道,不论是自定义View
仍是自定义LayoutManager
都不是一件简单的事情,其次,自定义ItemView
致使Adapter
的通用性下降。这些实现方式都是比较麻烦的。github
而谷歌爸爸真是贴心,知道咱们都有这种需求,就小手一抖,随便帮咱们实现了一个Helper类,来减轻咱们的工做量。这就是ItemTouchHelper
的做用。bash
本文打算从两个方面来教你们认识ItemTouchHelper
类:app
ItemTouchHelper
的基本使用ItemTouchHelper
的源码分析
本文参考资料:ide
在正式介绍ItemTouchHelper
以前,咱们先来了解ItemTouchHelper
是什么东西。源码分析
从ItemTouchHelper
的源码中,咱们能够看出来,ItemTouchHelper
继承了ItemDecoration
,根本上就是一个ItemDecoration
。关于ItemDecoration
的分析,有兴趣的同窗能够参考个人文章:RecyclerView 扩展(一) - 手把手教你认识ItemDecoration。post
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {
}
复制代码
至于为何ItemTouchHelper
会继承ItemDecoration
,后面会详细的解释,这里就先卖一下关子。学习
而后,咱们先来看看ItemTouchHelper
实现的效果,让你们有一个直观的体验。fetch
先是侧滑删除的效果: 动画
ItemTouchHelper
的使用。
既然是手把手教你们认识ItemTouchHelper
,因此天然须要介绍它的的基本使用,如今让咱们来看看究竟怎么使用ItemTouchHelper
。
在正式介绍ItemTouchHelper
的基本使用以前,咱们还必须了解一个类--ItemTouchHelper.Callback
。ItemTouchHelper
就是依靠这个类来实现侧滑删除和拖动位置两种效果的,我来看看它。
咱们在使用ItemTouchHelper
时,必须自定义一个ItemTouchHelper.Callback
,咱们来了解一下其中比较重要的几个方法。
方法名 | 做用 |
---|---|
getMovementFlags | 在此方法里面咱们须要构建两个flag,一个是dragFlags,表示拖动效果支持的方向,另外一个是swipeFlags,表示侧滑效果支持的方向。在咱们的Demo中,拖动执行上下两个方向,侧滑执行左右两个方向,这些操做咱们均可以在此方法里面定义。 |
onMove | 当拖动效果已经产生了,会回调此方法。在此方法里面,咱们一般会更新数据源,就好比说,一个ItemView 从0拖到了1位置,那么对应的数据源也须要更改位置。 |
onSwiped | 当侧滑效果以上产生了,会回调此方法。在此方法里面,咱们也会更新数据源。与onMove 方法不一样到的是,咱们在这个方法里面从数据源里面移除相应的数据,而后调用notifyXXX 方法就好了。 |
对于ItemTouchHelper
的基本使用来讲,咱们只须要了解这三个方法就已经OK了。接下来,我将正式介绍ItemTouchHelper
的基本使用。
首先,咱们须要自定义一个ItemTouchHelper.Callback
,以下:
public class CustomItemTouchCallback extends ItemTouchHelper.Callback {
private final ItemTouchStatus mItemTouchStatus;
public CustomItemTouchCallback(ItemTouchStatus itemTouchStatus) {
mItemTouchStatus = itemTouchStatus;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
// 上下拖动
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
// 向左滑动
int swipeFlags = ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
// 交换在数据源中相应数据源的位置
return mItemTouchStatus.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
// 从数据源中移除相应的数据
mItemTouchStatus.onItemRemove(viewHolder.getAdapterPosition());
}
}
复制代码
而后,咱们在使用RecyclerView
时,添加这两行代码就好了:
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(new CustomItemTouchCallback(mAdapter));
itemTouchHelper.attachToRecyclerView(mRecyclerView);
复制代码
最终的效果就是上面的动图展现的,是否是以为很是的简单呢?接下来,我将正式的分析ItemTouchHelper
的源码。
为了方便你们理解,我将个人代码上传到github,有兴趣的同窗能够看看:ItemTouchHelperDemo。
咱们从基本使用中了解到,ItemTouchHelper
的使用是很是简单的,因此你们心里有没有一种好奇呢?那就是ItemTouchHelper
到底是怎么实现,为何两个相对比较复杂的效果,经过几行代码就能实现呢?接下来的内容就能找到答案。
咱们都知道,ItemTouchHelper
的入口方法就是attachToRecyclerView
方法,接下来,咱们先来看看这个方法为咱们作了哪些事情。
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) {
return; // nothing to do
}
if (mRecyclerView != null) {
destroyCallbacks();
}
mRecyclerView = recyclerView;
if (recyclerView != null) {
final Resources resources = recyclerView.getResources();
mSwipeEscapeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_velocity);
mMaxSwipeVelocity = resources
.getDimension(R.dimen.item_touch_helper_swipe_escape_max_velocity);
setupCallbacks();
}
}
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener);
mRecyclerView.addOnChildAttachStateChangeListener(this);
startGestureDetection();
}
private void startGestureDetection() {
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}
复制代码
相对来讲,attachToRecyclerView
方法是比较简单的。这其中,咱们发现ItemTouchHelper
是经过ItemTouchListener
接口来为每一个ItemView
处理事件,同时,从这里咱们能够看出来,在ItemTouchHelper
内部还使用了GestureDetector
,而这里GestureDetector
的做用主要是来判断ItemView
是否进行了长按行为。
ItemTouchHelper
的分析重点应该是事件处理,可是在这以前,咱们先来看一个方法,这个方法很是的重要的。
当咱们的操做触发了长按或者侧滑的行为,都会回调此方法,同时当咱们手势释放,也会回调此方法。
因此从大的时机来看,当手势开始或者释放都会回调select
方法;而每一个大时机又分为两个小时机,分别是长按和侧滑,分别表示拖动交换位置和侧滑删除操做。
在正式分析select
方法的代码以前,咱们须要了解两个东西:
selected
表示被选中的ViewHolder
。其中,selected
若是为null,则表示当前处于手势(包括长按和侧滑)释放时机;反之,selected
不为null,则表示当前处于手势开始的时机。actionState
表示当前的状态,一共有三个值可选,分别是:1.ACTION_STATE_IDLE
表示没有任何手势,此时selected
对应的应当是null;2.ACTION_STATE_SWIPE
表示当前ItemView
处于侧滑状态;3.ACTION_STATE_DRAG
表示当前ItemView
处于拖动状态。在ItemTouchHelper
内部,就是经过这三个状态来判断ItemView
处于什么状态。
接下来咱们来看看select
方法的代码:
void select(ViewHolder selected, int actionState) {
if (selected == mSelected && actionState == mActionState) {
return;
}
mDragScrollStartTimeInMs = Long.MIN_VALUE;
final int prevActionState = mActionState;
endRecoverAnimation(selected, true);
mActionState = actionState;
// 若是当前是拖动行为,给RecyclerView设置一个ChildDrawingOrderCallback接口
// 主要是为了调整ItemView绘制的顺序
if (actionState == ACTION_STATE_DRAG) {
mOverdrawChild = selected.itemView;
addChildDrawingOrderCallback();
}
int actionStateMask = (1 << (DIRECTION_FLAG_COUNT + DIRECTION_FLAG_COUNT * actionState))
- 1;
boolean preventLayout = false;
// 1.手势释放
if (mSelected != null) {
// ······
}
// 2. 手势开始
// selected不为null表示手势开始,反之selected为null表示手势释放
if (selected != null) {
mSelectedFlags =
(mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
>> (mActionState * DIRECTION_FLAG_COUNT);
mSelectedStartX = selected.itemView.getLeft();
mSelectedStartY = selected.itemView.getTop();
mSelected = selected;
if (actionState == ACTION_STATE_DRAG) {
mSelected.itemView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
}
}
final ViewParent rvParent = mRecyclerView.getParent();
if (rvParent != null) {
rvParent.requestDisallowInterceptTouchEvent(mSelected != null);
}
if (!preventLayout) {
mRecyclerView.getLayoutManager().requestSimpleAnimationsInNextLayout();
}
mCallback.onSelectedChanged(mSelected, mActionState);
mRecyclerView.invalidate();
}
复制代码
从上面的代码中,咱们能够总结出来几个结论:
- 若是处于手势开始阶段,即
selected
不为null,那么会经过getAbsoluteMovementFlags
方法来获取执行咱们设置的flag,从而就知道执行哪些行为(侧滑或者拖动)和方向(上、下、左和右)。同时还会记录下被选中ItemView
的位置。简而言之,就是一些变量的初始化。- 若是处于手势释放阶段,即
selected
为null,同时mSelected
不为null,那么此时须要作的事情就稍微有一点复杂。手势释放以后,须要作的事情无非有两件:1. 相关的ItemView
到正确的位置,就好比说,若是滑动条件不知足,那么就返回原来的位置,这个就是一个动画;2. 清理操做,好比说将mSelected
重置为null之类的
ItemView
是否被选中 咱们知道,一旦调用selected
就意味着一个ItemView
被选中,接下来的就会随着手势出现侧滑或者拖动的效果了。可是怎么来判断一个ItemView
是否被选中,咱们从代码来看看,咱们分两步来理解:1.侧滑的选中;2. 拖动的选中。
判断侧滑行为是否选中主要在checkSelectForSwipe
方法,咱们来看看checkSelectForSwipe
放大的代码:
boolean checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
// 若是mSelected不为null表示已经有ItemView被选中
// 同时从这里能够看出来Callback的isItemViewSwipeEnabled方法的做用
if (mSelected != null || action != MotionEvent.ACTION_MOVE
|| mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
return false;
}
if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
return false;
}
final ViewHolder vh = findSwipedView(motionEvent);
if (vh == null) {
return false;
}
final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
>> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
// 若是flag没有支持侧滑的方向值,那么返回为false
if (swipeFlags == 0) {
return false;
}
// mDx and mDy are only set in allowed directions. We use custom x/y here instead of
// updateDxDy to avoid swiping if user moves more in the other direction
final float x = motionEvent.getX(pointerIndex);
final float y = motionEvent.getY(pointerIndex);
// Calculate the distance moved
final float dx = x - mInitialTouchX;
final float dy = y - mInitialTouchY;
// swipe target is chose w/o applying flags so it does not really check if swiping in that
// direction is allowed. This why here, we use mDx mDy to check slope value again.
final float absDx = Math.abs(dx);
final float absDy = Math.abs(dy);
if (absDx < mSlop && absDy < mSlop) {
return false;
}
// 这里主要是判断一个滑动是否符合侧滑的条件
if (absDx > absDy) {
if (dx < 0 && (swipeFlags & LEFT) == 0) {
return false;
}
if (dx > 0 && (swipeFlags & RIGHT) == 0) {
return false;
}
} else {
if (dy < 0 && (swipeFlags & UP) == 0) {
return false;
}
if (dy > 0 && (swipeFlags & DOWN) == 0) {
return false;
}
}
mDx = mDy = 0f;
mActivePointerId = motionEvent.getPointerId(0);
// 表示当前ItemView被侧滑行为选中
select(vh, ACTION_STATE_SWIPE);
return true;
}
复制代码
checkSelectForSwipe
方法的代码相对来讲比较长,可是无非就是判断当前ItemView
是否符合侧滑行为,若是到最后符合的话,那么就会调用select
方法来初始化一些值。 同时,咱们看一下checkSelectForSwipe
方法的调用时机只有两个地方:
onTouchEvent
方法onInterceptTouchEvent
方法
调用的时机也是比较正确的,至于为何须要两个地方来调用这个方法,我也不太清楚,估计作什么保险操做吧。
拖动选中的时机比较简单,由于拖动触发的前提是长按ItemView
,因此咱们直接在ItemTouchHelperGestureListener
的onLongPress
方法找到相关代码:
@Override
public void onLongPress(MotionEvent e) {
if (!mShouldReactToLongPress) {
return;
}
View child = findChildView(e);
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child);
if (vh != null) {
if (!mCallback.hasDragFlag(mRecyclerView, vh)) {
return;
}
int pointerId = e.getPointerId(0);
// Long press is deferred.
// Check w/ active pointer id to avoid selecting after motion
// event is canceled.
if (pointerId == mActivePointerId) {
final int index = e.findPointerIndex(mActivePointerId);
final float x = e.getX(index);
final float y = e.getY(index);
mInitialTouchX = x;
mInitialTouchY = y;
mDx = mDy = 0f;
if (DEBUG) {
Log.d(TAG,
"onlong press: x:" + mInitialTouchX + ",y:" + mInitialTouchY);
}
if (mCallback.isLongPressDragEnabled()) {
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
复制代码
这段代码表达的意思很是简单,这里我就很少余的解释了。从这里能够看出来,最终仍是调用了select
方法表示选中一个ItemView
。
咱们知道了ItemTouchHelper
怎么进行手势判断来选中一个ItemView
,选中以后的操做就是ItemView
随着手指滑动,咱们来看看ItemView
是怎么实现的。
咱们知道,随着手指的滑动,onTouchEvent
方法会被调用,咱们来看看相关的代码:
public void onTouchEvent(RecyclerView recyclerView, MotionEvent event) {
// ······
switch (action) {
case MotionEvent.ACTION_MOVE: {
// Find the index of the active pointer and fetch its position
if (activePointerIndex >= 0) {
updateDxDy(event, mSelectedFlags, activePointerIndex);
moveIfNecessary(viewHolder);
mRecyclerView.removeCallbacks(mScrollRunnable);
mScrollRunnable.run();
mRecyclerView.invalidate();
}
break;
}
// ······
}
}
复制代码
上面的代码我将它分为4步:
- 更新
mDx
和mDy
的值。mDx
和mDy
表示手指在x轴和y轴上分别滑动的距离。- 若是须要,移动其余
ItemView
的位置。这个主要针对拖动行为。- 若是须要,滑动
RecyclerView
。这个主要针对拖动行为,而这里滑动RecyclerView
的条件就是,RecyclerView
自己有大量的数据,一屏显示不完,此时若是拖动一个ItemView
达到RecyclerView
的底部或者顶部,会滑动RecyclerView
。- 更新被选中的
ItemView
的位置。代码体如今mRecyclerView.invalidate()
。
其中,更新mDx
和mDy
的值是经过updateDxDy
方法来实现的,而updateDxDy
方法方法比较简单,这里就不展开了。
咱们再来看看第二步,移动其余ItemView
的位置主要是经过moveIfNecessary
方法实现的。咱们来看看具体的代码:
void moveIfNecessary(ViewHolder viewHolder) {
// ······
// 以上都是不符合move的条件
// 1.寻找可能会交换位置的ItemView
List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
if (swapTargets.size() == 0) {
return;
}
// 2.找到符合条件交换的ItemView
// may swap.
ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
if (target == null) {
mSwapTargets.clear();
mDistances.clear();
return;
}
final int toPosition = target.getAdapterPosition();
final int fromPosition = viewHolder.getAdapterPosition();
// 3.回调Callback里面的onMove方法,这个方法须要咱们手动实现
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// 保证target的可见
// keep target visible
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}
复制代码
如上就是moveIfNecessary
方法的代码,这里讲它分为3步:
- 调用
findSwapTarget
方法,寻找可能会跟选中的ItemView
交换位置的ItemView
。这里判断的条件是只要选中的ItemView
跟某一个ItemView
重叠,那么这个ItemView
可能会跟选中的ItemView
交换位置。- 调用Callback的
chooseDropTarget
方法来找到符合交换条件的ItemView
。这里符合的条件是指,选中的ItemView
的bottom
大于目标ItemView
的bottom
或者ItemView
的top
大于目标ItemView
的top
。一般来讲,咱们能够重写chooseDropTarget
方法,来定义什么条件下就交换位置。- 回调
Callback
的onMove
方法,这个方法须要咱们本身实现。这里须要注意的是,若是onMove
方法返回为true的话,会调用Callback
另外一个onMove
方法来保证target可见。为何必须保证target可见呢?从官方文档上来看的话,若是target不可见,在某些滑动的情形下,target会被remove掉(回收掉),从而致使drag过早的中止。
关于ItemTouchHelper
是怎么来选择交换位置的ItemView
,重点就在findSwapTarget
方法和chooseDropTarget
方法。其中findSwapTarget
方法是找到可能会交换位置的ItemView
,chooseDropTarget
方法是找到会交换位置的ItemView
,这是两个方法的不一样点。同时,若是此时在拖动,可是拖动的ItemView
还未达到交换条件,也就是跟另外一个ItemView
只是重叠了一小部分,这种状况下,findSwapTargets
方法返回的集合不为空,可是chooseDropTarget
方法寻找的ItemView
为空。
而后就是第三步,第三步的做用是当ItemView
拖动到边缘,若是此时RecyclerView
能够滑动,那么RecyclerView
会滚动。具体的实现是在mScrollRunnable
的run
方法调用:
final Runnable mScrollRunnable = new Runnable() {
@Override
public void run() {
if (mSelected != null && scrollIfNecessary()) {
if (mSelected != null) { //it might be lost during scrolling
moveIfNecessary(mSelected);
}
mRecyclerView.removeCallbacks(mScrollRunnable);
ViewCompat.postOnAnimation(mRecyclerView, this);
}
}
};
复制代码
在run
方法里面经过scrollIfNecessary
方法来判断RecyclerView
是否滚动,若是须要滚动,scrollIfNecessary
方法会自动完成滚动操做。
最后一步就是ItemView
位置的更新,也就是mRecyclerView.invalidate()
的执行。这里须要理解的是,为何经过invalidate
方法就能更新ItemView
的位置呢?由于ItemView
在随着手指移动时,变化的是translationX
和translationY
两个属性,因此只须要调用invalidate
方法就行。调用invalidate
方法以后,至关于RecyclerView
会从新绘制一次,那么全部ItemDecoration
的onDraw
和onDrawOver
方法都会被调用,而刚好的是,ItemTouchHelper
就是一个ItemDecoration
。咱们想要知道ItemView
是怎么随着手指移动的,答案就在onDraw
方法里面:
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// ······
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy);
}
复制代码
在onDraw
方法里面,调用了Callback
的onDraw
方法。咱们来看看Callback
的onDraw
方法:
void onDraw(Canvas c, RecyclerView parent, ViewHolder selected,
List<ItemTouchHelper.RecoverAnimation> recoverAnimationList,
int actionState, float dX, float dY) {
final int recoverAnimSize = recoverAnimationList.size();
for (int i = 0; i < recoverAnimSize; i++) {
final ItemTouchHelper.RecoverAnimation anim = recoverAnimationList.get(i);
anim.update();
final int count = c.save();
onChildDraw(c, parent, anim.mViewHolder, anim.mX, anim.mY, anim.mActionState,
false);
c.restoreToCount(count);
}
if (selected != null) {
final int count = c.save();
onChildDraw(c, parent, selected, dX, dY, actionState, true);
c.restoreToCount(count);
}
}
复制代码
代码仍是比较长,可是表示的意思是很是简单的。就是调用onChildDraw
方法,将全部正在交换位置的ItemView
和被选中的ItemView
做为参数传递过去。
而在onChildDraw
方法里面,调用了ItemTouchUIUtil
的onDraw
方法。咱们从ItemTouchUiUtil
的实现类BaseImpl
找到答案:
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
view.setTranslationX(dX);
view.setTranslationY(dY);
}
复制代码
在这里改变了每一个ItemView
的translationX
和translationY
,从而实现了ItemView
随着手指移动的效果。
从这里,咱们能够看出来,一旦调用RecyclerView
的invalidate
方法,ItemTouchHelper
的onDraw
方法和onDrawOver
方法都会被执行。这个可能就是ItemTouchHelper
继承ItemDecoration
的缘由吧。
当咱们在上下拖动的时候,咱们发现一个问题,就是拖动的ItemView
始终在其余ItemView
的上面。这里,咱们不由疑惑,咱们都知道,在ViewGroup
里面,全部的child
都有绘制顺序。一般来讲,先添加的child
先绘制,后添加的child
后绘制,在RecyclerView
中也是不例外,上面的ItemView
先绘制,而下面的ItemView
后绘制。而在这个拖动效果中,为何不符合这个规则呢?咱们来看看ItemTouchHelper
是怎么帮忙实现的。
答案得分为两个种状况,一种是Api小于21,一种是Api大于等于21。
咱们先来看看Api小于21的状况。这个得从addChildDrawingOrderCallback
方法里面去寻找答案:
private void addChildDrawingOrderCallback() {
if (Build.VERSION.SDK_INT >= 21) {
return; // we use elevation on Lollipop
}
if (mChildDrawingOrderCallback == null) {
mChildDrawingOrderCallback = new RecyclerView.ChildDrawingOrderCallback() {
@Override
public int onGetChildDrawingOrder(int childCount, int i) {
if (mOverdrawChild == null) {
return i;
}
int childPosition = mOverdrawChildPosition;
if (childPosition == -1) {
childPosition = mRecyclerView.indexOfChild(mOverdrawChild);
mOverdrawChildPosition = childPosition;
}
if (i == childCount - 1) {
return childPosition;
}
return i < childPosition ? i : i + 1;
}
};
}
mRecyclerView.setChildDrawingOrderCallback(mChildDrawingOrderCallback);
}
复制代码
实现的原理就是给RecyclerView
设置了一个ChildDrawingOrderCallback
接口来改变child
的绘制顺序,这样能保证被选中的ItemView
后于重叠的ItemView
绘制,这样就实现了被选中的ItemView
始终在上面。
不过使用ChildDrawingOrderCallback
接口时,咱们须要注意的是:要想是接口有效,必须保证全部child
的elevation
是同样的,若是不同,那么elevation
优先级更高。
从上面的注意点,咱们应该都知道Api大于等于21时,使用的是什么方式来实现的吧。没错就是经过改变 ItemView
的elevation
值实现的。咱们来看看具体实现,在Api21Impl
的onDraw
方法里面:
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
if (isCurrentlyActive) {
Object originalElevation = view.getTag(R.id.item_touch_helper_previous_elevation);
if (originalElevation == null) {
originalElevation = ViewCompat.getElevation(view);
float newElevation = 1f + findMaxElevation(recyclerView, view);
ViewCompat.setElevation(view, newElevation);
view.setTag(R.id.item_touch_helper_previous_elevation, originalElevation);
}
}
super.onDraw(c, recyclerView, view, dX, dY, actionState, isCurrentlyActive);
}
复制代码
由于这里使用的是ViewCompcat
,因此当Api小于21时,调用setElevation
是无效的。如上就是Api大于等于21时实现被选中的ItemView
在全部ItemView上面的代码。
不论是拖动仍是侧滑,当咱们手势释放以后,作的操做无非两种:1. 回到原位;2.移动到正确的位置。那这部分的具体实如今哪里呢?没错,就在咱们以前分析过的select
方法里面,此时看select
方法代码时,咱们需得注意两个点:
- 此时,参数
selected
为null。- 此时,变量
mSelected
不为null。
而后,咱们在来看看相关代码:
void select(ViewHolder selected, int actionState) {
// ······
if (mSelected != null) {
final ViewHolder prevSelected = mSelected;
if (prevSelected.itemView.getParent() != null) {
// 1. 计算须要移动的距离
final int swipeDir = prevActionState == ACTION_STATE_DRAG ? 0
: swipeIfNecessary(prevSelected);
releaseVelocityTracker();
// find where we should animate to
final float targetTranslateX, targetTranslateY;
int animationType;
switch (swipeDir) {
case LEFT:
case RIGHT:
case START:
case END:
targetTranslateY = 0;
targetTranslateX = Math.signum(mDx) * mRecyclerView.getWidth();
break;
case UP:
case DOWN:
targetTranslateX = 0;
targetTranslateY = Math.signum(mDy) * mRecyclerView.getHeight();
break;
default:
targetTranslateX = 0;
targetTranslateY = 0;
}
if (prevActionState == ACTION_STATE_DRAG) {
animationType = ANIMATION_TYPE_DRAG;
} else if (swipeDir > 0) {
animationType = ANIMATION_TYPE_SWIPE_SUCCESS;
} else {
animationType = ANIMATION_TYPE_SWIPE_CANCEL;
}
getSelectedDxDy(mTmpPosition);
final float currentTranslateX = mTmpPosition[0];
final float currentTranslateY = mTmpPosition[1];
// 2.建立动画
final RecoverAnimation rv = new RecoverAnimation(prevSelected, animationType,
prevActionState, currentTranslateX, currentTranslateY,
targetTranslateX, targetTranslateY) {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
if (this.mOverridden) {
return;
}
if (swipeDir <= 0) {
// this is a drag or failed swipe. recover immediately
mCallback.clearView(mRecyclerView, prevSelected);
// full cleanup will happen on onDrawOver
} else {
// wait until remove animation is complete.
mPendingCleanup.add(prevSelected.itemView);
mIsPendingCleanup = true;
if (swipeDir > 0) {
// Animation might be ended by other animators during a layout.
// We defer callback to avoid editing adapter during a layout.
postDispatchSwipe(this, swipeDir);
}
}
// removed from the list after it is drawn for the last time
if (mOverdrawChild == prevSelected.itemView) {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
}
}
};
final long duration = mCallback.getAnimationDuration(mRecyclerView, animationType,
targetTranslateX - currentTranslateX, targetTranslateY - currentTranslateY);
rv.setDuration(duration);
mRecoverAnimations.add(rv);
// 3.执行动画
rv.start();
preventLayout = true;
} else {
removeChildDrawingOrderCallbackIfNecessary(prevSelected.itemView);
mCallback.clearView(mRecyclerView, prevSelected);
}
mSelected = null;
}
// ······
}
复制代码
上面的代码仍是比较长,我简单的将它分为3步,分别是:
- 计算
ItemView
此时须要移动的距离。- 根据计算出来的距离,建立动画。
- 执行动画,让
ItemView
回到正确的位置。
而这三步的具体实现都是比较简单的,在这里就不过多的解释了。
到此为止,ItemTouchHelper
就差很少了,在这里我对ItemTouchHelper
作一个简单的总结。
- 咱们使用
ItemTouchHelper
时,须要实现一个ItemTouchHelper.Callback
类。在这个实现类里面,咱们须要实现 三个方法,分别是:1.getMovementFlags
,主要是设置ItemTouchHelper
执行那些行为和方向;2.onMove
方法,表示当前有两个ItemView
发生了交换,此时须要咱们更新数据源;3.onSwiped
方法,表示当前有ItemView
被侧滑删除,也须要咱们更新数据源。ItemTouochHelper
是经过ItemTouchListener
来获取每一个ItemView
的事件,经过GestureDetector
来判断长按行为。ItemTouchHelper
是经过改变ItemView
的translationX
和translationY
属性值,进而改变每一个ItemView
的位置。ItemTouchHelper
是经过ChildDrawingOrderCallback
接口和Elevation
来改变ItemView
的绘制顺序的。