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

Другие вопросы по тегам