Android 回收视图 ItemTouchHelper 还原滑动和还原视图持有者

有没有一种方法可以恢复滑动动作,并恢复视图持有人的初始位置 之后滑动完成,并在 ItemTouchHelper.Callback实例上调用 onSwiped?我得到的 RecyclerViewItemTouchHelperItemTouchHelper.Callback实例一起工作完美,我只需要恢复滑动行动和 没有删除在某些情况下滑动项目。

36197 次浏览

Google 的 ItemTouchHelper实现假设 每个滑出的项目最终将从回收者视图中移除,而在某些应用程序中可能并非如此。

RecoverAnimationItemTouchHelper中的一个嵌套类,它管理滑动/拖动项的触摸动画。虽然名称暗示它只能恢复 recovers项的位置,但实际上它是唯一用于恢复(取消滑动/拖动) 还有替换(滑动时移出或拖动时替换)项的类。奇怪的名字。

RecoverAnimation中有一个名为 mIsPendingCleanup的布尔属性,ItemTouchHelper使用该属性来判断该项是否等待删除。因此,ItemTouchHelper在将一个 RecoverAnimation附加到该项之后,在成功地滑出之后设置此属性,并且只要设置了此属性,动画就不会从恢复动画列表中移除。问题是,mIsPendingCleanup总是被设置为一个滑出的项目,导致该项目的 RecoverAnimation永远不会从动画列表中删除。因此,即使你在成功滑动之后恢复了该物品的位置,它也会在你触摸它之后立即被发送回滑出位置——因为恢复动画会导致动画从最新的滑出位置开始。

遗憾的是,解决这个问题的办法是将 ItemTouchHelper类的源代码复制到支持库中的同一个包中,并从 RecoverAnimation类中删除 mIsPendingCleanup属性。我不确定这是否被 Google 接受,我还没有把更新发布到 Play Store,看看它是否会导致拒绝,但是你可以找到来自支持库 v22.2.1的类源代码和上面提到的在 https://gist.github.com/kukabi/f46e1c0503d2806acbe2的修复。

随便戳了几下之后,我找到了一个解决办法。在适配器上调用 notifyItemChanged。这将使滑出视图动画回到它的原始位置。

针对此问题的 肮脏的手段解决方案是通过调用 ItemTouchHelper::attachToRecyclerView(RecyclerView)两次重新附加 ItemTouchHelper,然后 ItemTouchHelper::attachToRecyclerView(RecyclerView)调用私有方法 ItemTouchHelper::destroyCallbacks()destroyCallbacks()删除项目装饰和所有侦听器,但也清除所有恢复动画。

请注意,我们需要首先调用 itemTouchHelper.attachToRecyclerView(null)来欺骗 ItemTouchHelper,使其认为对 itemTouchHelper.attachToRecyclerView(recyclerView)的第二次调用是一个新的回收者视图。

要了解更多细节,请查看 ItemTouchHelper 给你的源代码。

变通方法的例子:

RecyclerView recyclerView = findViewById(R.id.recycler_view);
ItemTouchHelper itemTouchHelper = new ItemTouchHelper(callback);


...
// Workaround to reset swiped out views
itemTouchHelper.attachToRecyclerView(null);
itemTouchHelper.attachToRecyclerView(recyclerView);

考虑它作为一个肮脏的解决方案,因为这个方法使用内部的,未记录的 ItemTouchHelper实现细节。

更新 :

来自 ItemTouchHelper::attachToRecyclerView(RecyclerView)文件:

如果 TouchHelper 已经连接到一个侯选视图,它将首先与前一个侯选视图分离。可以使用 null 调用此方法,以将其与当前回收视图分离。

以及参数文件:

如果要从当前回收视图中移除 ItemTouchHelper,则需要将此帮助器添加到其中的回收视图实例或 null。

所以至少有一部分记录在案。

在适配器上调用 notifyDataSetChanged,以使回扫工作一致

您应该重写 ItemTouchHelper.Callback中的 onSwiped方法并刷新该特定项。

 @Override
public void onSwiped(RecyclerView.ViewHolder viewHolder,
int direction) {
adapter.notifyItemChanged(viewHolder.getAdapterPosition());
}

在使用 LiveDataListAdapter提供列表的情况下,调用 notifyItemChanged不起作用。然而,我发现了一个难看的解决方案,它涉及到在 onSwiped回调中将 ItemTouchHelper重新连接到回收者视图

val recyclerView = someRecyclerViewInYourCode


var itemTouchHelper: ItemTouchHelper? = null


val itemTouchCallback = object : ItemTouchHelper.Callback {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction:Int) {
itemTouchHelper?.attachToRecyclerView(null)
itemTouchHelper?.attachToRecyclerView(recyclerView)
}
}


itemTouchHelper = ItemTouchHelper(itemTouchCallback)


itemTouchHelper.attachToRecyclerView(recyclerView)


对于最新的 anddroidX 软件包,我仍然有这个问题,所以我需要稍微调整@jimmy0251解决方案,以正确地重置项目(他的解决方案只在第一次滑动时有效)。

 override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
clipAdapter.notifyItemChanged(viewHolder.adapterPosition)
itemTouchHelper.startSwipe(viewHolder)
}

请注意,startSwipe()可以正确地重置项目的恢复动画。

从不调用,总是恢复

override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
return 1f
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return Float.MAX_VALUE
}

@ 解答实际上几乎是正确的

notifyItemChanged的问题在于,它可以做额外的动画,并且可能与 onDraw的装饰重叠,所以只要做一个干净的幻灯片,这就是你可以做的:

public class SimpleSwipeCallback extends ItemTouchHelper.SimpleCallback {


boolean swipeOutEnabled = true;
int swipeDir = 0;


public SimpleSwipeCallback() {
super(0, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT);
}


@Override
public boolean onMove(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder, @NonNull RecyclerView.ViewHolder target) {
return false;
}


@Override
public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int swipeDir) {
//Do action
}


@Override
public void onChildDraw(Canvas c, RecyclerView recyclerView,
RecyclerView.ViewHolder viewHolder,
float dx, float dy, int actionState, boolean isCurrentlyActive) {


//check if it should swipe out
boolean shouldSwipeOut = //TODO;
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE && (!shouldSwipeOut) {
swipeOutEnabled = false;


//Limit swipe
int maxMovement = recyclerView.getWidth() / 3;


//swipe right : left
float sign = dx > 0 ? 1 : -1;


float limitMovement = Math.min(maxMovement, sign * dx); // Only move to maxMovement


float displacementPercentage = limitMovement / maxMovement;


//limited threshold
boolean swipeThreshold = displacementPercentage == 1;


// Move slower when getting near the middle
dx = sign * maxMovement * (float) Math.sin((Math.PI / 2) * displacementPercentage);


if (isCurrentlyActive) {
int dir = dx > 0 ? ItemTouchHelper.RIGHT : ItemTouchHelper.LEFT;
swipeDir = swipeThreshold ? dir : 0;
}
} else {
swipeOutEnabled = true;
}


//do decoration


super.onChildDraw(c, recyclerView, viewHolder, dx, dy, actionState, isCurrentlyActive);
}


@Override
public float getSwipeEscapeVelocity(float defaultValue) {
return swipeOutEnabled ? defaultValue : Float.MAX_VALUE;
}


@Override
public float getSwipeVelocityThreshold(float defaultValue) {
return swipeOutEnabled ? defaultValue : 0;
}


@Override
public float getSwipeThreshold(RecyclerView.ViewHolder viewHolder) {
return swipeOutEnabled ? 0.6f : 1.0f;
}


@Override
public void clearView(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
super.clearView(recyclerView, viewHolder);


if (swipeDir != 0) {
onSwiped(viewHolder, swipeDir);
swipeDir = 0;
}
}
}

请注意,这可以启用一个正常的滑动(“ swipeOut”)或一个有限的滑动,这取决于 shouldSwipeOut

由于大多数 ItemTouchHelper成员都有一个私有包访问修饰符,我们不希望仅仅为了更改一行而复制一个2000行的类,因此让我们将包指向 androidx.recyclerview.widget

当滑动发生时(mCallback.onSwiped) ,我们可以恢复滑动视图的初始状态。 mCallback.onSwiped只能从 postDispatchSwipe方法中调用,因此之后我们将注入视图恢复(recoverOnSwiped) ,它将清除任何滑动视图中的滑动效果和动画。

@file:Suppress("PackageDirectoryMismatch")


package androidx.recyclerview.widget


import android.annotation.SuppressLint


/**
* [ItemTouchHelper] with recover viewHolder's itemView from clean up
*/
class RecoveredItemTouchHelper(callback: Callback, private val withRecover: Boolean = true) : ItemTouchHelper(callback) {


private fun recoverOnSwiped(viewHolder: RecyclerView.ViewHolder) {
// clear any swipe effects from [viewHolder]
endRecoverAnimation(viewHolder, false)
if (mPendingCleanup.remove(viewHolder.itemView)) {
mCallback.clearView(mRecyclerView, viewHolder)
}
if (mOverdrawChild == viewHolder.itemView) {
mOverdrawChild = null
mOverdrawChildPosition = -1
}
viewHolder.itemView.requestLayout()
}


@Suppress("DEPRECATED_IDENTITY_EQUALS")
@SuppressLint("VisibleForTests")
internal override fun postDispatchSwipe(anim: RecoverAnimation, swipeDir: Int) {
// wait until animations are complete.
mRecyclerView.post(object : Runnable {
override fun run() {
if (mRecyclerView != null && mRecyclerView.isAttachedToWindow
&& !anim.mOverridden
&& (anim.mViewHolder.absoluteAdapterPosition !== RecyclerView.NO_POSITION)
) {
val animator = mRecyclerView.itemAnimator
// if animator is running or we have other active recover animations, we try
// not to call onSwiped because DefaultItemAnimator is not good at merging
// animations. Instead, we wait and batch.
if ((animator == null || !animator.isRunning(null))
&& !hasRunningRecoverAnim()
) {
mCallback.onSwiped(anim.mViewHolder, swipeDir)
if (withRecover) {
// recover swiped
recoverOnSwiped(anim.mViewHolder)
}
} else {
mRecyclerView.post(this)
}
}
}
})
}
}

在适配器上调用 notifyItemChanged 对我来说是可行的。

有关更多信息,请参见 https://stackoverflow.com/a/32159154/8820118

解决方案是基于詹波拉克的回答。问题是,通知项目更改对于 ListAdapter或手动使用 DiffUtil都不起作用。重置 ItemTouchHelper看起来很糟糕,因为它没有动画。

所以这是我的最终解决方案,它将解决所有情况下的问题(有或没有 diff util 使用) ,并给你一个漂亮的 反向动画,如果你想允许取消/撤消删除内的 onSwiped事件。

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val allowDelete = false // or show a dialog and ask for confirmation or whatever logic you need


if (allowDelete) {
adapter.remove(viewHolder.bindingAdapterPosition)
} else {
// start the inverse animation and reset the internal swipe state AFTERWARDS
viewHolder.itemView
.animate()
.translationX(0f)
.withEndAction {
itemTouchHelper.attachToRecyclerView(null)
itemTouchHelper.attachToRecyclerView(recyclerView)
}
.start()
}
}