在NestedScrollView中使用RecyclerView的ItemTouchHelper:拖动滚动不起作用

18
我已经按照这篇文章实现了ItemTouchHelper:https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf#.k7xm7amxi 如果RecyclerView是CoordinatorLayout的子项,则一切正常。但是,如果RecyclerView是嵌套在CoordinatorLayout中的NestedScrollView的子项,则拖动滚动不再起作用。将一个项目拖动到屏幕的顶部或底部时,RecyclerView不会像它不是NestedScrollView的子项时那样滚动。
有任何想法吗?

9
你有任何解决方案吗? - Hemant
4个回答

2

您需要禁用recyclerViewnestedScrolling

recyclerView.setIsNestedScrollingEnabled(false);

很遗憾没有帮上忙。 - Vladimir Marton

1

我遇到了同样的问题,花了近一整天的时间来解决它。

前置条件:

首先,我的XML布局如下:

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

为了使滚动行为正常,我还让RecyclerViewnestedScrolling失效了:RecyclerView.setIsNestedScrollingEnabled(false);

原因:

但是使用ItemTouchHelper时,当我在Recyclerview中拖动项目时,仍然无法使其自动滚动。造成无法滚动的原因在于ItemTouchHelperscrollIfNecessary()方法中:

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:当将RecyclerViewnestedScrolling设置为false时,实际上有效的滚动对象是NestedScrollView,它是RecyclerView的父级。因此,RecyclerView.scrollBy(x, y)在这里根本不起作用!
  • 原因2:mRecyclerView.getHeight()NestedScrollView.getHeight()大得多。所以,当我将RecyclerView中的项目拖到底部时,scrollIfNecessary()的结果也为false。
  • 原因3:mSelectedStartY并不像我们的情况中预期的那样。因为我们需要计算NestedScrollViewscrollY

因此,我们需要重写这个方法来满足我们的期望。以下是解决方案:

解决方案:

第一步:

为了覆盖此 scrollIfNecessary()(此方法不是public),您需要在与ItemTouchHelper相同的包下新建一个类。像这样: Example codes

步骤2:

除了覆盖scrollIfNecessary()之外,我们还需要覆盖select()以便在开始拖动时获取mSelectedStartYNestedScrollViewscrollY值。
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()
    }
}

注意:mSelectedStartYmSelectedStartScrollY对于上下滚动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展示我的工作:

Result


我在一个 BottomSheetLayout 中的 NestedScrollView 内嵌套了 RecyclerView 并尝试了此解决方案,成功了。 - Pedro Henrique
但是使用这种解决方案,我发现了一个问题。RecyclerView中的项将在初始化时全部膨胀(这将导致onBindViewHolder()被调用很多次)。我建议如果您可以仅使用RecyclerView而不在NestedScrollView中使用它,则在此情况下不应考虑NestedScrollView。 - Vensent Wang
@Vensent Wang 你能分享一下这个的示例代码吗? 我正在处理这个问题,但是无法解决。 我的项目也遇到了同样的问题。 希望能得到帮助。 - Allay Khalil

0
这是适用于我的解决方案。
创建2个自定义类
1> LockableScrollView public class LockableScrollView extends 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);

}

}

在xml中使用这些视图,而不是NestedScrollView和RecyclerView

在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)

-1
android:descendantFocusability="blocksDescendants"
添加到 NestedScrollView 中,并在子布局中添加
android:focusableInTouchMode="true"
看起来像下面这样
   <androidx.core.widget.NestedScrollView 
        android:descendantFocusability="blocksDescendants"> 

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

</androidx.core.widget.NestedScrollView>

检查一下这个 GitHub 仓库:https://github.com/khambhaytajaydip/Drag-Drop-recyclerview

这对我不起作用。而且这似乎不是一个合乎逻辑的修复方法。如果您将NestedScrollView设置为阻止后代的可聚焦性,为什么要将其后代设置为可聚焦呢? - Matt Robertson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接