ItemTouchHelper 简介
ItemTouchHelper 是 RecyclerView 的一个辅助类,一般用于 RecyclerView 中的 item 的滑动删除和拖拽操作。
从 ItemTouchHelper 的源码可以看出来,ItemTouchHelper 继承自 ItemDecoration,本质上就是一个 ItemDecoration。关于 ItemDecoration 的分析,有兴趣的可以看这里:RecyclerView------ItemDecoration。
java
public class ItemTouchHelper extends RecyclerView.ItemDecoration
implements RecyclerView.OnChildAttachStateChangeListener {
它只有一个构造方法:
java
public ItemTouchHelper(@NonNull Callback callback) {
mCallback = callback;
}
构造方法需要传入一个 Callback,该 Callback 是 ItemTouchHelper 中的抽象静态内部类:
java
public abstract static class Callback {
public abstract int getMovementFlags(@NonNull RecyclerView recyclerView,
@NonNull ViewHolder viewHolder);
public abstract boolean onMove(@NonNull RecyclerView recyclerView,
@NonNull ViewHolder viewHolder, @NonNull ViewHolder target);
public abstract void onSwiped(@NonNull ViewHolder viewHolder, int direction);
}
里面的抽象方法有 3 个:
- getMovementFlags:需要在该方法中构建两个 flag,一个是 dragFlags,表示可以拖动的方向,另一个是 swipeFlags,表示可以滑动的方向。
- onMove:当 ItemTouchHelper 把 item 从一个 position 移动到另一个 position 的时候会调用该方法。如果该方法返回 true,ItemTouchHelper 会假定 viewHolder 已经移动到了 target ViewHolder 的 adapter Position(
ViewHolder#getAdapterPositionInRecyclerView()
)。如果你没有配置拖动的 flag,该方法不会调用。 - onSwiped:当用户侧滑的时候会调用该方法。如果你没有配置滑动的 flag,该方法不会调用。
简单使用
MyItemTouchCallback 代码如下:
kotlin
class MyItemTouchCallback: ItemTouchHelper.Callback(){
var itemMoveListener: ((startPos: Int, endPos: Int) -> Boolean)? = null
var itemSwipeListener: ((position: Int) -> Unit)? = null
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
val dragFlags = ItemTouchHelper.UP or ItemTouchHelper.DOWN // 上下拖动的 flag
val swipeFlags = ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT // 左右滑动的 flag
return makeMovementFlags(dragFlags, swipeFlags)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
val startPos = viewHolder.bindingAdapterPosition
val endPos = target.bindingAdapterPosition
return itemMoveListener?.invoke(startPos, endPos) ?: false // 拖动后交换位置
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.bindingAdapterPosition
itemSwipeListener?.invoke(position) // 滑动后触发
}
}
RecyclerViewActivity 代码如下:
kotlin
class RecyclerViewActivity : AppCompatActivity(){
val TAG = this::class.java.simpleName
private lateinit var binding: ActivityRecyclerViewBinding
private var list = mutableListOf<Int>()
private lateinit var myAdapter: MyAdapter
private val itemTouchHelper by lazy {
val myItemTouchCallback = MyItemTouchCallback()
myItemTouchCallback.itemMoveListener = { startPos, endPos ->
Log.d(TAG, "Item moved from $startPos to $endPos")
Collections.swap(list, startPos, endPos)
myAdapter.notifyItemMoved(startPos, endPos)
true
}
myItemTouchCallback.itemSwipeListener = { position ->
Log.d(TAG, "Swiped item at $position")
list.removeAt(position)
myAdapter.notifyItemRemoved(position)
}
ItemTouchHelper(myItemTouchCallback)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityRecyclerViewBinding.inflate(layoutInflater)
setContentView(binding.root)
init()
}
private fun init(){
for(i in 0 .. 100){
list.add(i)
}
myAdapter = MyAdapter(list)
binding.rv.adapter = myAdapter
itemTouchHelper.attachToRecyclerView(binding.rv)
}
}
我们在上下拖动 item 的时候在数据源中交换相应的数据,在左右滑动 item 的时候从数据源中移除相应的数据,ItemTouchHelper 对 RecyclerView 生效则是通过其 attachToRecyclerView() 方法,这样就达到了侧滑删除和拖动排序的效果。
源码解析
ItemTouchHelper 用起来非常简单,但是里面究竟是怎么实现的呢?下面我们来一探究竟,本文源码基于 androidx.recyclerview:recyclerview:1.2.1。
事件处理
ItemTouchHelper 的入口方法是 attachToRecyclerView() 方法,其代码如下:
java
public void attachToRecyclerView(@Nullable RecyclerView recyclerView) {
if (mRecyclerView == recyclerView) { // 如果当前已经有了,并且与传入的相等
return; // nothing to do
}
if (mRecyclerView != null) { // 如果当前已经有了,并且与传入的不相等,移除当前
destroyCallbacks();
}
mRecyclerView = recyclerView; // 如果当前没有,赋值给成员变量 mRecyclerView
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(); // 调用 setupCallbacks()
}
}
private void setupCallbacks() {
ViewConfiguration vc = ViewConfiguration.get(mRecyclerView.getContext());
mSlop = vc.getScaledTouchSlop();
mRecyclerView.addItemDecoration(this);
mRecyclerView.addOnItemTouchListener(mOnItemTouchListener); // 添加 item touch 监听
mRecyclerView.addOnChildAttachStateChangeListener(this);
startGestureDetection();
}
private void startGestureDetection() {
// 手势相关的接口的初始化
mItemTouchHelperGestureListener = new ItemTouchHelperGestureListener();
mGestureDetector = new GestureDetectorCompat(mRecyclerView.getContext(),
mItemTouchHelperGestureListener);
}
可以看到 RecyclerView 的 touch 事件是通过 mOnItemTouchListener 进行处理的,而 mOnItemTouchListener 里面又会调用 mGestureDetector.onTouchEvent() :
java
GestureDetectorCompat mGestureDetector;
private final OnItemTouchListener mOnItemTouchListener = new OnItemTouchListener() {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView,
@NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event); // 交给 ItemTouchHelperGestureListener 进行处理
...
final int action = event.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
...
} else if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
mActivePointerId = ACTIVE_POINTER_ID_NONE;
select(null, ACTION_STATE_IDLE);
} else if (mActivePointerId != ACTIVE_POINTER_ID_NONE) {
final int index = event.findPointerIndex(mActivePointerId);
...
if (index >= 0) {
checkSelectForSwipe(action, event, index);
}
}
...
return mSelected != null;
}
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent event) {
mGestureDetector.onTouchEvent(event); //交给 ItemTouchHelperGestureListener 进行处理
...
final int action = event.getActionMasked();
final int activePointerIndex = event.findPointerIndex(mActivePointerId);
if (activePointerIndex >= 0) {
checkSelectForSwipe(action, event, activePointerIndex);
}
ViewHolder viewHolder = mSelected;
if (viewHolder == null) {
return;
}
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;
}
...
case MotionEvent.ACTION_UP:
select(null, ACTION_STATE_IDLE);
mActivePointerId = ACTIVE_POINTER_ID_NONE;
break;
...
}
}
};
拖拽手势处理
mGestureDetector 会把手势处理交给 ItemTouchHelperGestureListener ,其代码如下:
java
private class ItemTouchHelperGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public void onLongPress(MotionEvent e) {
...
View child = findChildView(e); // 通过 event 找到对应的 View
if (child != null) {
ViewHolder vh = mRecyclerView.getChildViewHolder(child); // 通过 View 找到对应的 ViewHolder
if (vh != null) {
if (!mCallback.hasDragFlag(mRecyclerView, vh)) { // 判断是否有设置 dragFlag
return;
}
...
if (mCallback.isLongPressDragEnabled()) { // 如果 isLongPressDragEnabled() 为 true
select(vh, ACTION_STATE_DRAG);
}
}
}
}
}
在这里对长按手势进行了处理,如果条件满足,最后会调用 select() 方法,并传入了 ACTION_STATE_DRAG。
ACTION_STATE_DRAG 又是什么呢?在 ItemTouchHelper 中定义了 3 种状态,ACTION_STATE_DRAG 是其中一种状态。
java
// ItemTouchHelper 处于空闲状态,这种状态下,要么没有相应的用户动作,
// 要么最新的动作还没有触发侧滑或拖动
public static final int ACTION_STATE_IDLE = 0;
// View 被侧滑的状态
public static final int ACTION_STATE_SWIPE = 1;
// View 被拖动的状态
public static final int ACTION_STATE_DRAG = 2;
ACTION_STATE_DRAG 表示 View 被侧滑的状态,ACTION_STATE_DRAG 表示 View 被拖动的状态, ACTION_STATE_IDLE 表示空闲状态。
在 OnItemTouchListener 中可以发现,MotionEvent 为 ACTION_UP 的时候也会调用 select() 方法,并给 selected 传入 null 。参数 selected 表示当前操作的 ViewHolder,selected 如果不为 null,则表示当前处于拖动或侧滑开始的时机;selected 如果为 null,则表示当前处于拖动或侧滑释放的时机。select() 方法代码如下:
java
// 当前选中的 view holder
ViewHolder mSelected = null;
void select(@Nullable ViewHolder selected, int actionState) {
...
// 如果是拖动
if (actionState == ACTION_STATE_DRAG) {
...
mOverdrawChild = selected.itemView;
// 这里主要是为了让拖动的 View 最后绘制
addChildDrawingOrderCallback();
}
...
// 手势释放
if (mSelected != null) {
...
mSelected = null; // 把 mSelected 赋值为 null
}
// 手势开始
if (selected != null) {
mSelectedFlags =
(mCallback.getAbsoluteMovementFlags(mRecyclerView, selected) & actionStateMask)
>> (mActionState * DIRECTION_FLAG_COUNT);
mSelectedStartX = selected.itemView.getLeft();
mSelectedStartY = selected.itemView.getTop();
mSelected = selected; // 把 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); // 回调 onSelectedChanged()
mRecyclerView.invalidate(); // 刷新
}
mSelected 表示当前选中的 view holder,通过分析源码可以发现,这里只有 select() 方法会改变 mSelected 的值。从这里可以获取到以下信息:
- 如果是拖动,会调用 addChildDrawingOrderCallback() 方法让拖动的 View 最后绘制,这里我们留到最后再来分析。
- 如果处于手势开始阶段,即 selected 不为 null 时,会通过 Callback 的 getAbsoluteMovementFlags() 方法来获取我们配置的 flag ,从而确定侧滑或拖动的方向,同时还会记录下对应的 ItemView 的位置,并把 select 赋值给 mSelected 。
- 如果处于手势释放阶段,此时传入的 selected 参数为 null,但同时 mSelected 不为 null(手势开始阶段给 mSelected 赋了值),那么此时需要做的事情就稍微有一点复杂了。手势释放之后,如果滑动的条件不满足,会执行一个动画(RecoverAnimation)返回原来的位置,还会把 mSelected 重置为 null。
怎么判断滑动的条件是否满足呢,如果是水平方向的滑动,会通过判断滑动距离是否大于 mRecyclerView.getWidth() * mCallback.getSwipeThreshold(viewHolder),也就是 RecyclerView 的宽度乘以滑动阈值,滑动阈值默认是 0.5f;
侧滑手势处理
调用 select() 方法并传入 ACTION_STATE_SWIPE 的地方有两处,一处是在 checkSelectForSwipe() 方法中,另一次是在 startSwipe() 方法中,checkSelectForSwipe() 方法在 ItemTouchHelperGestureListener 的事件分发和处理中调用,用于检查是否要对对应的 ViewHolder 做滑动处理,其代码如下:
java
void checkSelectForSwipe(int action, MotionEvent motionEvent, int pointerIndex) {
// 如果 mSelected 不为 null 表示已经有 ViewHolder 被选中,nothing to do
// 如果 action != MotionEvent.ACTION_MOVE,nothing to do
// 如果 Callback.isItemViewSwipeEnabled() 为 false,nothing to do
if (mSelected != null || action != MotionEvent.ACTION_MOVE
|| mActionState == ACTION_STATE_DRAG || !mCallback.isItemViewSwipeEnabled()) {
return;
}
if (mRecyclerView.getScrollState() == RecyclerView.SCROLL_STATE_DRAGGING) {
return;
}
final ViewHolder vh = findSwipedView(motionEvent);
if (vh == null) {
return;
}
final int movementFlags = mCallback.getAbsoluteMovementFlags(mRecyclerView, vh);
final int swipeFlags = (movementFlags & ACTION_MODE_SWIPE_MASK)
>> (DIRECTION_FLAG_COUNT * ACTION_STATE_SWIPE);
// 如果 swipeFlags == 0, noting to do
if (swipeFlags == 0) {
return;
}
// 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);
// 计算滑动距离
final float dx = x - mInitialTouchX;
final float dy = y - mInitialTouchY;
final float absDx = Math.abs(dx);
final float absDy = Math.abs(dy);
// 判断是否达到了滑动的阈值
if (absDx < mSlop && absDy < mSlop) {
return;
}
// 如果水平方向的滑动距离大于垂直方向的滑动距离
if (absDx > absDy) {
// 左滑但没有配置左滑的 flag
if (dx < 0 && (swipeFlags & LEFT) == 0) {
return;
}
if (dx > 0 && (swipeFlags & RIGHT) == 0) {
return;
}
} else {
if (dy < 0 && (swipeFlags & UP) == 0) {
return;
}
if (dy > 0 && (swipeFlags & DOWN) == 0) {
return;
}
}
mDx = mDy = 0f;
mActivePointerId = motionEvent.getPointerId(0);
// 条件满足后调用 select() 方法
select(vh, ACTION_STATE_SWIPE);
}
可以看到无非就是判断滑动条件是否满足,满足的话就会调用 select() 方法,并传入当前侧滑的 ViewHolder 和 ACTION_STATE_SWIPE。
为什么在事件分发和处理阶段都要调用 checkSelectForSwipe() 呢? 在事件分发阶段是为了尽早判断用户的手势是否是一个有效的滑动手势,从而自己来处理该手势,防止其他视图或手势处理器(比如 RecyclerView 的滚动)处理该事件。在滑动阶段是因为手势可能会发生变化比如从左滑变成右滑,此时需要重新计算滑动的方向,确保滑动的准确性。
itemView 随着手势移动
前面我们知道了 ItemTouchHelper 怎么通过侧滑和拖动手势来选中一个 itemView,选中之后的操作就是 itemView 随着手指移动,这段代码在 mOnItemTouchListener 的 onTouchEvent() 方法中:
java
@Override
public void onTouchEvent(@NonNull RecyclerView recyclerView, @NonNull 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 步:
- 通过 updateDxDy() 方法更新 mDx 和 mDy 的值,mDx 和 mDy 分别表示手指在 x 轴和 y 轴方向上移动的距离。
- 通过 moveIfNecessary() 方法检查是否需要与另一个 ViewHolder 交换位置。
- 如果用户把 itemView 拖到了 RecyclerView 的边界,这时候需要滚动 RecyclerView。比如在一个纵向的 RecyclerView 中,如果数据很多,一屏显示不下,当你把 itemView 拖动到底部的时候,RecyclerView 需要自动往上滚动。
- 通过 mRecyclerView.invalidate() 刷新视图。
我们来看看第 2 步是怎么与另一个 ViewHolder 交换位置的:
java
void moveIfNecessary(ViewHolder viewHolder) {
...
// threshold = 0.5f
final float threshold = mCallback.getMoveThreshold(viewHolder);
final int x = (int) (mSelectedStartX + mDx); // mSelectedStartX 的值来自前面的 select() 方法
final int y = (int) (mSelectedStartY + mDy);
// 如果手指移动的距离没有超过 itemView 宽和高的一半,return
if (Math.abs(y - viewHolder.itemView.getTop()) < viewHolder.itemView.getHeight() * threshold
&& Math.abs(x - viewHolder.itemView.getLeft())
< viewHolder.itemView.getWidth() * threshold) {
return;
}
// 查找可能交换位置的 ViewHolder
List<ViewHolder> swapTargets = findSwapTargets(viewHolder);
if (swapTargets.size() == 0) {
return;
}
// 根据 x 和 y 找到要交换位置的目标 ViewHolder
// may swap.
ViewHolder target = mCallback.chooseDropTarget(viewHolder, swapTargets, x, y);
if (target == null) {
mSwapTargets.clear();
mDistances.clear();
return;
}
final int toPosition = target.getAbsoluteAdapterPosition();
final int fromPosition = viewHolder.getAbsoluteAdapterPosition();
// 回调 Callback 里面的 onMove() 方法,这个方法需要我们手动实现
if (mCallback.onMove(mRecyclerView, viewHolder, target)) {
// onMove() 返回 true 后调用,当在 RecyclerView 的边缘交换 itemView 的
// 时候通过滚动 RecyclerView 让目标处在可见范围内
mCallback.onMoved(mRecyclerView, viewHolder, fromPosition,
target, toPosition, x, y);
}
}
这里分成 3 步:
- 通过 findSwapTarget() 方法查找可能会跟选中的 ViewHolder 交换位置的 ViewHolder。这里判断的条件是只要选中的 ViewHolder 跟某一个 ViewHolder 有重叠的部分,那么这个 ViewHolder 可能会跟选中的 ViewHolder 交换位置。
- 调用 Callback 的 chooseDropTarget() 方法来找到符合交换条件的 ViewHolder。这里符合的条件是指选中的 ItemView 的 bottom 大于目标 ItemView 的 bottom 或者它的 top 大于目标 ItemView 的 top 。我们可以通过重写 chooseDropTarget() 方法,来定义什么条件下就交换位置。
- 回调 Callback 的 onMove() 方法,这个方法需要我们自己实现。这里需要注意的是,如果onMove() 方法返回 true 会调用 Callback 的 onMoved() 方法来滚动 RecyclerView 让 target 处在可见范围内。为什么必须保证 target 可见呢?官方文档的意思是,如果 ViewHolder 不可见,LayoutManager 可能会移除 ViewHolder ,从而导致拖动过早结束。
当调用 mRecyclerView.invalidate() 刷新视图的时候,会调用对应的 RecyclerView 的 onDraw() 方法:
java
@Override
public void onDraw(Canvas c) {
super.onDraw(c);
final int count = mItemDecorations.size();
for (int i = 0; i < count; i++) {
mItemDecorations.get(i).onDraw(c, this, mState);
}
}
可以看到这里会遍历 mItemDecorations 并调用其 onDraw() 方法,前面 attachToRecyclerView() 的时候会把 ItemTouchHelper 添加到 mItemDecorations 里面,这样就会调用 ItemTouchHelper 的 onDraw() 方法,代码如下:
java
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// we don't know if RV changed something so we should invalidate this index.
mOverdrawChildPosition = -1;
float dx = 0, dy = 0;
if (mSelected != null) {
getSelectedDxDy(mTmpPosition);
dx = mTmpPosition[0];
dy = mTmpPosition[1];
}
mCallback.onDraw(c, parent, mSelected,
mRecoverAnimations, mActionState, dx, dy);
}
这里会调用 Callback 的 onDraw() 方法,代码如下:
java
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() 方法:
java
public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
@NonNull ViewHolder viewHolder,
float dX, float dY, int actionState, boolean isCurrentlyActive) {
ItemTouchUIUtilImpl.INSTANCE.onDraw(c, recyclerView, viewHolder.itemView, dX, dY,
actionState, isCurrentlyActive);
}
在 onChildDraw() 方法中,调用了 ItemTouchUIUtilImpl 的单例 INSTANCE 的 onDraw() 方法:
java
@Override
public void onDraw(Canvas c, RecyclerView recyclerView, View view, float dX, float dY,
int actionState, boolean isCurrentlyActive) {
if (Build.VERSION.SDK_INT >= 21) {
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);
}
}
}
view.setTranslationX(dX); // 改变 ItemView 的 translationX
view.setTranslationY(dY);
}
在这里改变了每个 ItemView 的 translationX 和 translationY,从而实现了 ItemView 随着手指移动的效果。
为什么拖动的 ItemView 始终显示在其他 ItemView 的上面?
在拖动的时候,很容易发现一个问题,就是拖动的 ItemView 始终在其他 ItemView 的上面。我们都知道,在 ViewGroup 里面,所有的 child 都有绘制顺序。通常先添加的 child 先绘制,后添加的 child 后绘制,在RecyclerView 中也不例外,上面的 ItemView 先绘制,下面的 ItemView 后绘制。而在这个拖动效果中,为什么拖动的 itemView 没有被下面的 itemView 挡住呢?来看看 ItemTouchHelper 是怎么实现的:
java
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);
}
从代码中可以看到,在 API 小于 21 时候,给 RecyclerView 配置了一个 ChildDrawingOrderCallback 接口,里面有一个 onGetChildDrawingOrder() 方法来改变 child 的绘制顺序。不过使用 ChildDrawingOrderCallback 接口时需要注意:要想接口是有效的,必须保证所有 child 的 elevation 是一样的,elevation 优先级更高。
如果 API 大于或等于 21,在前面的 ItemTouchUIUtilImpl 的 onDraw() 方法中可以找到,就是通过改变 ItemView 的 elevation 的值来改变 child 的绘制顺序的。