I have run into this same problem and I spent nearly a whole day to solve it.
Precondition:
First of all, my xml layout looks like this:
<CoordinatorLayout>
<com.google.android.material.appbar.AppBarLayout
...
</com.google.android.material.appbar.AppBarLayout>
<NestedScrollView>
<RecyclerView/>
</NestedScrollView>
</CoordinatorLayout>
And to make the scrolling behavior normal, I also let the nestedScrolling
for the RecyclerView
disabled by: RecyclerView.setIsNestedScrollingEnabled(false);
Reason:
But with ItemTouchHelper
I still cannot make the Recyclerview
auto scroll as expected when I drag the item in it. The reason why IT CANNOT SCROLL is in the method scrollIfNecessary()
of ItemTouchHelper
:
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;
}
- Reason 1: when
nestedScrolling
for the RecyclerView
is set to false, actually the effective scrolling object is the NestedScrollView
, which is the parent of RecyclerView
. So RecyclerView.scrollBy(x, y)
here does not work at all!
- Reason 2:
mRecyclerView.getHeight()
is much bigger than NestedScrollView.getHeight()
. So when I drag the item in RecyclerView
to bottom, the result of scrollIfNecessary()
is also false.
- Reason 3:
mSelectedStartY
does not seem like the expected value when in our case. Because we need to calculate the scrollY
of NestedScrollView
in our case.
Therefore, we need to override this method to fullfill our expectation. Here comes the solution:
Solution:
Step 1:
In order to override this scrollIfNecessary()
(This method is not public
), you need to new a class under a package named the same as ItemTouchHelper
's. Like this:
Step 2:
Besides overriding scrollIfNecessary()
, we also need to override select()
in order to get the value of mSelectedStartY
and the scrollY
of NestedScrollView
when starting draging.
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()
}
}
Notice: mSelectedStartY
and mSelectedStartScrollY
are both very important for scrolling the NestedScrollView
up or down.
Step 3:
Now we can override scrollIfNecessary()
, and you need to pay attention to the comments below:
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
}
Result:
I can just show you my work through the Gif below: