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)