ItemTouchHelper с RecyclerView в NestedScrollView: прокрутка с перетаскиванием не работает

Я реализовал ItemTouchHelper, как описано в этой статье: https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf

Все работает нормально, если RecyclerView является дочерним элементом CoordinatorLayout.

Но если RecyclerView является дочерним элементом NestedScrollView в CoordinatorLayout, прокрутка с перетаскиванием больше не работает. Перетаскивая элемент и перемещая его в верхнюю или нижнюю часть экрана, RecyclerView не прокручивается, как если бы он не был дочерним элементом NestedScrollView.

Есть идеи?

4 ответа

Вы должны отключить nestedScrolling для recyclerView:

recyclerViewAdapter.setIsNestedScrollingEnabled(false);

Я столкнулся с этой же проблемой, и я потратил почти целый день, чтобы решить ее.

Условие:

Прежде всего, мой макет xml выглядит так:

      <CoordinatorLayout>
    <com.google.android.material.appbar.AppBarLayout
        ...
    </com.google.android.material.appbar.AppBarLayout>
    <NestedScrollView>
        <RecyclerView/>
    </NestedScrollView>
</CoordinatorLayout>

И чтобы сделать поведение прокрутки нормальным, я также разрешаю для инвалидов: RecyclerView.setIsNestedScrollingEnabled(false);

Причина:

Но с я все еще не могу сделать Recyclerviewавтоматическая прокрутка, как и ожидалось, когда я перетаскиваю в нее элемент. Причина, по которой ЭТО НЕ МОЖЕТ ПРОКРУТИТЬ , заключается в методе :

      boolean scrollIfNecessary() {
    RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
    if (mTmpRect == null) {
        mTmpRect = new Rect();
    }
    int scrollY = 0;
    lm.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect);
    if (lm.canScrollVertically()) {
        int curY = (int) (mSelectedStartY + mDy);
        final int topDiff = curY - mTmpRect.top - mRecyclerView.getPaddingTop();
        if (mDy < 0 && topDiff < 0) {
            scrollY = topDiff;
        } else if (mDy > 0) {
            final int bottomDiff = curY + mSelected.itemView.getHeight() + mTmpRect.bottom
                    - (mRecyclerView.getHeight() - mRecyclerView.getPaddingBottom());
            if (bottomDiff > 0) {
                scrollY = bottomDiff;
            }
        }
    }
    if (scrollY != 0) {
        scrollY = mCallback.interpolateOutOfBoundsScroll(mRecyclerView,
                mSelected.itemView.getHeight(), scrollY,
                mRecyclerView.getHeight(), scrollDuration);
    }
    if (scrollY != 0) {
        mRecyclerView.scrollBy(scrollX, scrollY);
        return true;
    }
    return false;
}
  • Причина 1: когда nestedScrollingпоскольку установлено значение false, на самом деле эффективным объектом прокрутки является , который является родителем . Так RecyclerView.scrollBy(x, y)тут вообще не работает!
  • Причина 2: mRecyclerView.getHeight()намного больше, чем NestedScrollView.getHeight(). Поэтому, когда я перетаскиваю элемент в RecyclerViewвниз, результат также ложен.
  • Причина 3: не похоже на ожидаемое значение в нашем случае. Потому что нам нужно вычислить в нашем случае.

Поэтому нам нужно переопределить этот метод, чтобы оправдать наши ожидания. Вот решение:

Решение:

Шаг 1:

Чтобы переопределить это (этот метод не public), вам нужно создать новый класс в пакете с таким же именем, как ItemTouchHelperс. Как это:

Шаг 2:

Помимо переопределения, нам также необходимо переопределить select()чтобы получить значение и scrollYпри начале перетаскивания.

      public override fun select(selected: RecyclerView.ViewHolder?, actionState: Int) {
    super.select(selected, actionState)
    if (selected != null) {
        mSelectedStartY = selected.itemView.top
        mSelectedStartScrollY = (mRecyclerView.parent as NestedScrollView).scrollY.toFloat()
    }
}

Уведомление: mSelectedStartYа также mSelectedStartScrollYоба очень важны для прокрутки NestedScrollViewвверх или вниз.

Шаг 3:

Теперь мы можем переопределить scrollIfNecessary(), и вам нужно обратить внимание на комментарии ниже:

      public override fun scrollIfNecessary(): Boolean {
    ...
    val lm = mRecyclerView.layoutManager
    if (mTmpRect == null) {
        mTmpRect = Rect()
    }
    var scrollY = 0
    val currentScrollY = (mRecyclerView.parent as NestedScrollView).scrollY
    
    // We need to use the height of NestedScrollView, not RecyclerView's!
    val actualShowingHeight = (mRecyclerView.parent as NestedScrollView).height

    lm!!.calculateItemDecorationsForChild(mSelected.itemView, mTmpRect!!)
    if (lm.canScrollVertically()) {
        // The true current Y of the item in NestedScrollView, not in RecyclerView!
        val curY = (mSelectedStartY + mDy - currentScrollY).toInt()

        // The true mDy should plus the initial scrollY and minus current scrollY of NestedScrollView
        val checkDy = (mDy + mSelectedStartScrollY - currentScrollY).toInt()
        
        val topDiff = curY - mTmpRect!!.top - mRecyclerView.paddingTop
        if (checkDy < 0 && topDiff < 0) {// User is draging the item out of the top edge.
            scrollY = topDiff
        } else if (checkDy > 0) { // User is draging the item out of the bottom edge.
            val bottomDiff = (curY + mSelected.itemView.height + mTmpRect!!.bottom
                    - (actualShowingHeight - mRecyclerView.paddingBottom))
            if (bottomDiff > 0) {
                scrollY = bottomDiff
            }
        }
    }
    if (scrollY != 0) {
        scrollY = mCallback.interpolateOutOfBoundsScroll(
            mRecyclerView,
            mSelected.itemView.height, scrollY, actualShowingHeight, scrollDuration
        )
    }
    if (scrollY != 0) {
        ...
        // The scrolling behavior should be assigned to NestedScrollView!
        (mRecyclerView.parent as NestedScrollView).scrollBy(0, scrollY)
        return true
    }
    ...
    return false
}

Результат:

Я могу просто показать вам свою работу через Gif ниже:

андроид:descendantFocusability="blocksDescendants"

добавить в NestedScrollView и добавить

андроид:focusableInTouchMode="истина"

в дочернем макете это выглядит ниже

   <androidx.core.widget.NestedScrollView 
        android:descendantFocusability="blocksDescendants"> 

    <androidx.constraintlayout.widget.ConstraintLayout
        android:focusableInTouchMode="true">
        </androidx.constraintlayout.widget.ConstraintLayout> 

</androidx.core.widget.NestedScrollView>

Это решение, которое работает для меня.

Создайте 2 пользовательских класса

1> LockableScrollView

открытый класс LockableScrollView расширяет NestedScrollView {

      // true if we can scroll (not locked)
// false if we cannot scroll (locked)
private boolean mScrollable = true;

public LockableScrollView(@NonNull Context context) {
    super(context);
}

public LockableScrollView(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

public LockableScrollView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}


public void setScrollingEnabled(boolean enabled) {
    mScrollable = enabled;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    // Don't do anything with intercepted touch events if
    // we are not scrollable
    if (ev.getAction() == MotionEvent.ACTION_MOVE) {// if we can scroll pass the event to the superclass
        return mScrollable && super.onInterceptTouchEvent(ev);
    }
    return super.onInterceptTouchEvent(ev);

}

}

2>LockableRecyclerView расширяет RecyclerView

      public class LockableRecyclerView extends RecyclerView {

private LockableScrollView scrollview;

public LockableRecyclerView(@NonNull Context context) {
    super(context);
}

public LockableRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

public LockableRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

public void setScrollview(LockableScrollView lockedscrollview) {
    this.scrollview = lockedscrollview;
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_MOVE) {
        scrollview.setScrollingEnabled(false);
        return super.onInterceptTouchEvent(ev);
    }
    scrollview.setScrollingEnabled(true);
    return super.onInterceptTouchEvent(ev);

}

@Override
public boolean onTouchEvent(MotionEvent e) {
    if (e.getAction() == MotionEvent.ACTION_MOVE) {
        scrollview.setScrollingEnabled(false);
        return super.onTouchEvent(e);
    }
    scrollview.setScrollingEnabled(true);
    return super.onTouchEvent(e);

}

}

Используйте эти представления вместо NestedScrollView и RecyclerView в xml

в наборе файлов kotlin recyclerView.setScrollview(binding.scrollView)recyclerView.isNestedScrollingEnabled = false

ItemTouchHelper(object:ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.UP) {override fun onMove(@NonNull recyclerView: RecyclerView,@NonNull viewHolder: RecyclerView.ViewHolder,@NonNull target: RecyclerView.ViewHolder): Boolean {return false}

              override fun onSwiped(@NonNull viewHolder: RecyclerView.ViewHolder, direction: Int) {
            // when user swipe thr recyclerview item to right remove item from favorite list
            if (direction == ItemTouchHelper.UP) {

                val itemToRemove = favList[viewHolder.absoluteAdapterPosition]

            }
        }
    }).attachToRecyclerView(binding.recyclerView)
Другие вопросы по тегам