Android RecyclerView ItemTouchHelper отменить прокрутку и восстановить держатель представления
Есть ли способ отменить действие смахивания и восстановить держатель вида в исходное положение после завершения смахивания и onSwiped
называется на ItemTouchHelper.Callback
пример? Я получил RecyclerView
, ItemTouchHelper
а также ItemTouchHelper.Callback
чтобы экземпляры работали идеально, мне просто нужно отменить действие смахивания и не удалять удаленный элемент в некоторых случаях.
13 ответов
Google-х ItemTouchHelper
В реализации предполагается, что каждый сотретый элемент в конечном итоге будет удален из представления переработчика, в то время как в некоторых приложениях это может быть не так.
RecoverAnimation
является вложенным классом в ItemTouchHelper
он управляет сенсорной анимацией перетаскиваемых / перетаскиваемых объектов. Хотя название подразумевает, что оно восстанавливает только положение элементов, на самом деле это единственный класс, который используется для восстановления (отмены пролистывания / перетаскивания) и замены (перемещения при пролистывании или замены при перетаскивании) элементов. Странные названия.
Есть логическое свойство с именем mIsPendingCleanup
в RecoverAnimation
, который ItemTouchHelper
используется для определения того, ожидает ли элемент удаления. Так ItemTouchHelper
после прикрепления RecoverAnimation
для этого элемента, устанавливает это свойство после удачного пролистывания, и анимация не удаляется из списка анимаций восстановления, пока это свойство установлено. Проблема в том, что, mIsPendingCleanup
всегда будет установлен для отмахивающегося элемента, вызывая RecoverAnimation
чтобы элемент никогда не удалялся из списка анимаций. Таким образом, даже если вы восстановите позицию элемента после удачного пролистывания, он будет возвращен обратно в положение смахивания, как только вы к нему прикоснетесь - потому что RecoverAnimation вызовет запуск анимации с последней позиции смахивания.
Решение этой проблемы, к сожалению, чтобы скопировать ItemTouchHelper
исходный код класса в тот же пакет, что и в библиотеке поддержки, и удалите mIsPendingCleanup
собственность от RecoverAnimation
учебный класс. Я не уверен, является ли это приемлемым для Google, и я еще не опубликовал обновление в Play Store, чтобы узнать, приведет ли оно к отклонению, но вы можете найти исходный код класса из библиотеки поддержки v22.2.1 с указанным выше упомянутое исправление на https://gist.github.com/kukabi/f46e1c0503d2806acbe2.
После некоторого случайного тычка я нашел решение. Вызов notifyItemChanged
на вашем адаптере. Это позволит ожившему представлению вернуться в исходное положение.
Вы должны переопределить onSwiped
метод в ItemTouchHelper.Callback
и обновить этот конкретный элемент.
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder,
int direction) {
adapter.notifyItemChanged(viewHolder.getAdapterPosition());
}
грязный обходной путь Решением этой проблемы является повторное присоединение ItemTouchHelper путем вызова ItemTouchHelper::attachToRecyclerView(RecyclerView)
дважды, который затем вызывает закрытый метод ItemTouchHelper::destroyCallbacks()
, destroyCallbacks()
удаляет украшение элемента и всех слушателей, а также очищает все RecoverAnimations.
Обратите внимание, что нам нужно позвонить 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 уже подключен к RecyclerView, он сначала отсоединится от предыдущего. Вы можете вызвать этот метод с нулем, чтобы отделить его от текущего RecyclerView.
и в документации параметров:
Экземпляр RecyclerView, к которому вы хотите добавить этот помощник или ноль, если вы хотите удалить ItemTouchHelper из текущего RecyclerView.
Так что, по крайней мере, это частично документировано.
В случае использования LiveData
предоставить список ListAdapter
звонит notifyItemChanged
не работает. Тем не менее, я нашел ошибочный обходной путь, который включает в себя ItemTouchHelper
на переработчик вид в onSwiped
обратный вызов как таковой
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)
С последними пакетами anndroidX у меня все еще есть эта проблема, поэтому мне нужно было немного настроить решение @jimmy0251, чтобы правильно сбросить элемент (его решение будет работать только для первого смахивания).
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
clipAdapter.notifyItemChanged(viewHolder.adapterPosition)
itemTouchHelper.startSwipe(viewHolder)
}
Обратите внимание, что startSwipe()
правильно сбрасывает анимацию восстановления предмета.
On Swiped никогда не звонил, всегда возвращался
override fun getSwipeThreshold(viewHolder: RecyclerView.ViewHolder): Float {
return 1f
}
override fun getSwipeEscapeVelocity(defaultValue: Float): Float {
return Float.MAX_VALUE
}
Решение основано на ответе Яна Поллаке. Проблема в том, что уведомление об изменении элемента не работает с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()
}
}
Вызовите notifyDataSetChanged на вашем адаптере, чтобы заставить работать назад
Поскольку большая часть
ItemTouchHelper
члены имеют модификатор доступа к частному пакету, и мы не хотим копировать класс строки 2000 только для изменения одной строки, давайте укажем наш пакет как
androidx.recyclerview.widget
.
Когда происходит свайп (), мы можем восстановить исходное состояние пролистнутого представления.
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 на адаптере.
См. /questions/7039173/android-recyclerview-itemtouchhelper-otmenit-prokrutku-i-vosstanovit-derzhatel-predstavleniya/7039189#7039189 для получения дополнительной информации.
Если вы хотите посмотреть хороший учебник, посвященный анимации RecyclerView Swipe & Drap, вы можете посмотреть это видео, и оно показалось мне весьма полезным. https://www.youtube.com/watch?v=grRAvv-uk1c&t=1228s
Решение @Павел Карпычев на самом деле почти правильное
public class SimpleSwipeCallback extends ItemTouchHelper.SimpleCallback {
boolean swipeOutEnabled = true;
int swipeDir = 0;
public SimpleSwipeCallback() {
super(0, ItemTouchHelper.RIGHT | ItemTouchHelper.LEFT);
}
public SimpleSwipeCallback() {
this(0);
}
@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) {
//normal threshold
boolean swipeThreshold = dx/recyclerView.getWidth() > getSwipeThreshold(viewHolder);
//check if it should swipe out
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
swipeThreshold = displacementPercentage == 1;
// Move slower when getting near the middle
dx = sign * maxMovement * (float) Math.sin((Math.PI / 2) * displacementPercentage);
} else {
swipeOutEnabled = true;
}
if (isCurrentlyActive && swipeThreshold) {
swipeDir = dx > 0 ? ItemTouchHelper.RIGHT : ItemTouchHelper.LEFT;
}
//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