ItemTouchHelper with RecyclerView in NestedScrollView: Drag scrolling not work
Asked Answered
J

4

18

I have implemented ItemTouchHelper like descriped in this articel: https://medium.com/@ipaulpro/drag-and-swipe-with-recyclerview-b9456d2b1aaf#.k7xm7amxi

All works fine if the RecyclerView is a child of the CoordinatorLayout.

But if the RecyclerView is a child of NestedScrollView in CoordinatorLayout, the drag scrolling not working anymore. Draging an item and move it to the top or bottom of the screen, the RecyclerView not scrolling like it do if its not a child of NestedScrollView.

Any ideas?

Jute answered 27/9, 2016 at 21:30 Comment(1)
Have you got any solution?Assr
P
2

You have to disable the nestedScrolling for the recyclerView:

recyclerView.setIsNestedScrollingEnabled(false);
Pedate answered 11/9, 2018 at 1:26 Comment(1)
Didn't help unfortunately.Uta
S
2

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: Example codes

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:

Result

Sequin answered 13/1, 2022 at 16:34 Comment(3)
I try this solution in a RecyclerView inside a NestedScrollView inside a BottomSheetLayout and works.Nightie
But with this solution, I have found a problem. The items in the RecyclerView would be inflated all at the initialization(which will lead onBindViewHolder() be called many many times.). I suggest if you can use just a RecyclerView without in a NestedScrollView, you should not consider the NestedScrollView in this situation.Sequin
@Vensent Wang can you share the sample code of this I'm working on this but unable to do this I have the same problem with my project help will be appreciatedChristoper
U
0

This is the solution that works for me.

Create 2 custom classes

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 extends 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);

}

}

Use this views instead of NestedScrollView and RecyclerView in xml

in kotlin file set 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)
Unilocular answered 2/2, 2022 at 7:4 Comment(0)
A
-1

android:descendantFocusability="blocksDescendants"

add in NestedScrollView and add

android:focusableInTouchMode="true"

in child layout it look like below

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

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

</androidx.core.widget.NestedScrollView>

check this github repo https://github.com/khambhaytajaydip/Drag-Drop-recyclerview

Apparitor answered 10/7, 2019 at 6:13 Comment(1)
This isn't working for me. Also it doesn't seem like a logical fix. Why would you set NestedScrollView to block descendants' focusability if you are setting it's descendant to be focusable?Limicoline

© 2022 - 2024 — McMap. All rights reserved.