Reputation: 13109
I have spent hours/days reading around this subject but still can't find something that works. I'm trying to put a fixed-height vertically-scrolling RecyclerView
in the row of another vertically-scrolling RecyclerView
.
Much of the advice is along the lines of "it's a crime to put a vertically-scrolling RecyclerView
inside another vertically-scrolling RecyclerView
"... but I can't figure out why this is so bad.
In fact, the behavior would be almost exactly the same as many pages on StackOverflow (e.g. this one... and indeed this very question, at least when viewed on a mobile device), where the code sections are of a fixed (or max) height, and scroll vertically, and are contained within a page that itself scrolls vertically. What happens is that when the focus is on the code section, scrolling happens within that section, and when it reaches the upper/lower end of the scroll range of the section, then scrolling happens within the outer page. It's quite natural, not evil.
This is my recycler_view_row_outer.xml (a row within the outer RecyclerView
):
<com.google.android.material.card.MaterialCardView
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/MyCardView"
app:cardElevation="4dp"
app:strokeColor="?attr/myCardBorderColor"
app:strokeWidth="0.7dp"
card_view:cardCornerRadius="8dp" >
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >
<TextView
style="@style/MyTextView.Section"
android:id="@+id/list_title" />
<LinearLayout
style="@style/MyLinearLayoutContainer"
android:id="@+id/list_container"
android:layout_below="@+id/list_title" >
</LinearLayout>
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
And this is my recycler_view_row_inner.xml
(a row within the inner RecyclerView
):
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/my_constraint_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/list_container" >
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view_inner"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollbarStyle="outsideOverlay"
android:scrollbars="vertical"
app:layout_constrainedHeight="true"
app:layout_constraintBottom_toBottomOf="@+id/my_constraint_layout"
app:layout_constraintEnd_toEndOf="@+id/my_constraint_layout"
app:layout_constraintHeight_max="750dp"
app:layout_constraintHeight_min="0dp"
app:layout_constraintStart_toStartOf="@+id/my_constraint_layout"
app:layout_constraintTop_toTopOf="@+id/my_constraint_layout" >
</androidx.recyclerview.widget.RecyclerView>
</androidx.constraintlayout.widget.ConstraintLayout>
With the above inner layout, I've tried to follow the approach set out in this post, to create an inner RecyclerView
having a fixed/max height... but it isn't working.
I inflate the inner layout (added to list_container
/containerView
in the outer layout) and set up my inner recyclerView
as follows:
View inflatedView = getLayoutInflater().inflate(R.layout.recycler_view_row_inner, containerView, false);
RecyclerView recyclerView = inflatedView.findViewById(R.id.recycler_view_inner);
// set adapter, row data, etc
But all this does is create a fixed-height inner row that does not scroll within the outer row... overflow content of the inner row is just cut off and I can't reach the lower part of it because it just scrolls the outer row.
Any idea how to make this work?
Upvotes: 8
Views: 10614
Reputation: 17874
Cheticamp's answer works really well for cases where you don't need an ItemTouchHelper
that supports dragging.
For that case, I came up with a messy but functional workaround:
class NestedRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr), NestedScrollingParent3 {
private var nestedScrollTarget: View? = null
private var nestedScrollTargetWasUnableToScroll = false
private val parentHelper by lazy { NestedScrollingParentHelper(this) }
/**
* Set this wherever you have access to your item touch helper instance.
* Using `attachToRecyclerView(null)` resets any long-press timers.
*
* Example:
*
* nestedRecyclerView.nestedScrollingListener = {
* itemTouchHelper.attachToRecyclerView(if (!it) nestedRecyclerView else null)
* }
*/
var nestedScrollingListener: ((Boolean) -> Unit)? = null
/**
* Set this from your item touch helper callback to let the RecyclerView
* know when an item is selected (prevents an inverse nested scrolling issue
* where the nested view scrolls and the item touch helper doesn't receive
* further callbacks).
*
* Example:
* override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
* if (actionState != ItemTouchHelper.ACTION_STATE_IDLE) nestedRecyclerView.selectedItem = true
* ...
* }
* ...
* override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
* nestedRecyclerView.selectedItem = false
* ...
* }
*/
var selectedItem: Boolean = false
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// Nothing special if no child scrolling target.
if (nestedScrollTarget == null || selectedItem) return super.dispatchTouchEvent(ev)
// Inhibit the execution of our onInterceptTouchEvent for now...
requestDisallowInterceptTouchEvent(true)
// ... but do all other processing.
var handled = super.dispatchTouchEvent(ev)
// If the first dispatch yielded an unhandled event or the descendant view is unable to
// scroll in the direction the user is scrolling, we dispatch once more but without skipping
// our onInterceptTouchEvent. Note that RecyclerView automatically cancels active touches of
// all its descendants once it starts scrolling so we don't have to do that.
requestDisallowInterceptTouchEvent(false)
if (ev.action == MotionEvent.ACTION_UP) {
// This is to prevent an issue where the item touch helper receives
// an ACTION_DOWN but then doesn't later get the ACTION_UP event,
// causing it to run any long-press events.
nestedScrollingListener?.invoke(true)
nestedScrollingListener?.invoke(false)
}
if (!handled || nestedScrollTargetWasUnableToScroll) {
handled = super.dispatchTouchEvent(ev)
}
return handled
}
override fun getNestedScrollAxes(): Int {
return parentHelper.nestedScrollAxes
}
// We only support vertical scrolling.
override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int) =
nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
/* Introduced with NestedScrollingParent2. */
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int) =
onStartNestedScroll(child, target, axes)
override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
// A descendant started scrolling, so we'll observe it.
setTarget(target)
}
parentHelper.onNestedScrollAccepted(child, target, axes)
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
// A descendant started scrolling, so we'll observe it.
setTarget(target)
}
parentHelper.onNestedScrollAccepted(child, target, axes, type)
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
super.onNestedPreScroll(target, dx, dy, consumed)
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
onNestedPreScroll(target, dx, dy, consumed)
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int
) {
if (target === nestedScrollTarget && dyUnconsumed != 0) {
// The descendant could not fully consume the scroll. We remember that in order
// to allow the RecyclerView to take over scrolling.
nestedScrollTargetWasUnableToScroll = true
// Let the parent start to consume scroll events.
target.parent?.requestDisallowInterceptTouchEvent(false)
}
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
}
/* Introduced with NestedScrollingParent3. */
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
}
/* From ViewGroup */
override fun onStopNestedScroll(child: View) {
// The descendant finished scrolling. Clean up!
setTarget(null)
parentHelper.onStopNestedScroll(child)
}
/* Introduced with NestedScrollingParent2. */
override fun onStopNestedScroll(target: View, type: Int) {
// The descendant finished scrolling. Clean up!
setTarget(null)
parentHelper.onStopNestedScroll(target, type)
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
return super.onNestedPreFling(target, velocityX, velocityY)
}
/* In ViewGroup for API 21+. */
override fun onNestedFling(
target: View,
velocityX: Float,
velocityY: Float,
consumed: Boolean
) = super.onNestedFling(target, velocityX, velocityY, consumed).also {
// If the nested fling wasn't consumed, then the touch helper can act.
// Otherwise, disable it.
nestedScrollingListener?.invoke(!it)
}
private fun setTarget(target: View?) {
if (target == null) {
// We're not nested scrolling anymore so the touch helper can
// do its thing again.
nestedScrollingListener?.invoke(false)
}
nestedScrollTarget = target
nestedScrollTargetWasUnableToScroll = false
// My specific implementation has nested ListViews (AppWidgetHost), so this is
// also needed. If you have scrollable Views in general, you may need to use
// View#setOnScrollChangeListener and check if the scroll change is non-zero.
(target as? ListView)?.setOnScrollListener(object : AbsListView.OnScrollListener {
override fun onScrollStateChanged(view: AbsListView?, scrollState: Int) {
nestedScrollingListener?.invoke(scrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE)
}
override fun onScroll(
view: AbsListView?,
firstVisibleItem: Int,
visibleItemCount: Int,
totalItemCount: Int
) {
}
})
}
}
You need direct access to the NestedRecyclerView
instance where you have your ItemTouchHelper
and its callback in order to listen for and set different states. I've commented the new properties I added.
Upvotes: 0
Reputation: 62851
Here is an approach that retrofits RecyclerView with the NestedScrollingParent3 interface. The NestedRecyclerView class is used as the outer RecyclerView and a stock RecyclerView is used as an inner item view.
The following code is based upon code from Widgetlabs here which is licensed under the MIT License. The code presented below has been heavily modified from the original.
Operation When the outer RecyclerView is touched, it can be scrolled up and down as expected and the inner RecyclerView is dragged along.
When the inner RecyclerView is touched, it can be scrolled up and down without effecting the outer RecyclerView. When the inner RecyclerView reaches it fullest extent either down or up, the outer RecyclerView starts to scroll. Once the outer RecyclerView starts to scroll, it has captured the gesture and will not release it until another "down" event. This is different from how RecyclerView behaves within a NestedScrollView where the outer RecyclerView scrolls but inner RecyclerView will resume consuming the scroll gesture when the scroll direction changes. In essence, the inner RecyclerView never relinquishes control while here it does.
The following video demonstrates the operation of NestedRecyclerView. In the video, the inner RecyclerView has a white border and orange and red rows. Everthing else is the outer RecyclerView.
NestedRecyclerView
open class NestedRecyclerView @JvmOverloads constructor(
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
) : RecyclerView(context, attrs, defStyleAttr), NestedScrollingParent3 {
private var nestedScrollTarget: View? = null
private var nestedScrollTargetWasUnableToScroll = false
private val parentHelper by lazy { NestedScrollingParentHelper(this) }
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
// Nothing special if no child scrolling target.
if (nestedScrollTarget == null) return super.dispatchTouchEvent(ev)
// Inhibit the execution of our onInterceptTouchEvent for now...
requestDisallowInterceptTouchEvent(true)
// ... but do all other processing.
var handled = super.dispatchTouchEvent(ev)
// If the first dispatch yielded an unhandled event or the descendant view is unable to
// scroll in the direction the user is scrolling, we dispatch once more but without skipping
// our onInterceptTouchEvent. Note that RecyclerView automatically cancels active touches of
// all its descendants once it starts scrolling so we don't have to do that.
requestDisallowInterceptTouchEvent(false)
if (!handled || nestedScrollTargetWasUnableToScroll) {
handled = super.dispatchTouchEvent(ev)
}
return handled
}
// We only support vertical scrolling.
override fun onStartNestedScroll(child: View, target: View, nestedScrollAxes: Int) =
nestedScrollAxes and ViewCompat.SCROLL_AXIS_VERTICAL != 0
/* Introduced with NestedScrollingParent2. */
override fun onStartNestedScroll(child: View, target: View, axes: Int, type: Int) =
onStartNestedScroll(child, target, axes)
override fun onNestedScrollAccepted(child: View, target: View, axes: Int) {
if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
// A descendant started scrolling, so we'll observe it.
setTarget(target)
}
parentHelper.onNestedScrollAccepted(child, target, axes)
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedScrollAccepted(child: View, target: View, axes: Int, type: Int) {
if (axes and View.SCROLL_AXIS_VERTICAL != 0) {
// A descendant started scrolling, so we'll observe it.
setTarget(target)
}
parentHelper.onNestedScrollAccepted(child, target, axes, type)
}
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
dispatchNestedPreScroll(dx, dy, consumed, null)
} else {
super.onNestedPreScroll(target, dx, dy, consumed)
}
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
onNestedPreScroll(target, dx, dy, consumed)
}
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int
) {
if (target === nestedScrollTarget && dyUnconsumed != 0) {
// The descendant could not fully consume the scroll. We remember that in order
// to allow the RecyclerView to take over scrolling.
nestedScrollTargetWasUnableToScroll = true
// Let the parent start to consume scroll events.
target.parent?.requestDisallowInterceptTouchEvent(false)
}
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int
) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed)
}
/* Introduced with NestedScrollingParent3. */
override fun onNestedScroll(
target: View,
dxConsumed: Int,
dyConsumed: Int,
dxUnconsumed: Int,
dyUnconsumed: Int,
type: Int,
consumed: IntArray
) {
onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
}
/* From ViewGroup */
override fun onStopNestedScroll(child: View) {
// The descendant finished scrolling. Clean up!
setTarget(null)
parentHelper.onStopNestedScroll(child)
}
/* Introduced with NestedScrollingParent2. */
override fun onStopNestedScroll(target: View, type: Int) {
// The descendant finished scrolling. Clean up!
setTarget(null)
parentHelper.onStopNestedScroll(target, type)
}
/* Introduced with NestedScrollingParent2. */
override fun onNestedPreFling(target: View, velocityX: Float, velocityY: Float): Boolean {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
false
} else {
super.onNestedPreFling(target, velocityX, velocityY)
}
}
/* In ViewGroup for API 21+. */
override fun onNestedFling(
target: View,
velocityX: Float,
velocityY: Float,
consumed: Boolean
) =
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
false
} else {
super.onNestedFling(target, velocityX, velocityY, consumed)
}
private fun setTarget(target: View?) {
nestedScrollTarget = target
nestedScrollTargetWasUnableToScroll = false
}
}
Now for the caveat: The preceding code has been lightly (every so lightly) tested. The RecyclerView environment is complex and the introduction of nested scrolling to RecyclerView as a parent of that scrolling makes it even more complex and prone to error. Nonetheless, it is an interesting study IMO.
Upvotes: 11
Reputation: 40908
Pros:
Cons:
Well, I won't address the performance issues of vertically nested RecyclerViews
; But notice that:
RecyclerView
probably loses the ability of recycling views; because the shown rows of the outer recyclerView should load their items entirely. (Thankfully it's not a right assumption as per the below UPDATE 1)ViewHolder
not in the onBindViewHolder
to have a better performance by not creating a new adapter instance for the inner RecyclerView
each time views are recycled.The demo app represents the months of the year as the outer RecyclerView
, and the day numbers of each month as inner RecyclerView
.
The outer RecyclerView
registers OnScrollListener
that each time it's scrolled, we do this check on the inner RV:
outerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) //scrolled to BOTTOM
outerAdapter.isOuterScrollingDown(true, dy);
else if (dy < 0) //scrolled to TOP
outerAdapter.isOuterScrollingDown(false, dy);
}
});
In the outer adapter:
public void isOuterScrollingDown(boolean scrollDown, int value) {
if (scrollDown) {
boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
if (!isLastItemShown) onScrollListener.onScroll(-value);
enableOuterScroll(isLastItemShown);
} else {
boolean isFirstItemShown = currentFirstItem == 1;
if (!isFirstItemShown) onScrollListener.onScroll(-value);
enableOuterScroll(isFirstItemShown);
}
if (currentRV != null)
currentRV.smoothScrollBy(0, 10 * value);
}
If the relevant item is not shown, then we decide to disable the outer RV scrolling. This is handled by a listener with a callback that accepts a boolean passed to a customized LinearLayoutManager
class to the outer RV.
Likewise in order to re-enable scrolling of the outer RV: the inner RecyclerView
registers OnScrollListener
to check if the inner first/last item is shown.
innerRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
if (!recyclerView.canScrollVertically(1) // Is it not possible to scroll more to bottom (i.e. Last item shown)
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
enableOuterScroll(true);
} else if (!recyclerView.canScrollVertically(-1) // Is it possible to scroll more to top (i.e. First item shown)
&& newState == RecyclerView.SCROLL_STATE_IDLE) {
enableOuterScroll(true);
}
}
});
Still, there are glitches because disabling/enabling scrolling; we can't pass the scroll order to the other RV, until the next scroll. This is manipulated slightly by reversing the initial outer RV scroll value; and using an arbitrary scroll value to the inner with currentRV.smoothScrollBy(0, 10 * initialScroll)
. I wish if someone can suggest any other alternative to this.
UPDATE 1
- The inner
RecyclerView
probably lose the ability of recycling views; because the shown rows of the outer recyclerView should load their items entirely.
Thankfully it's not the right assumption and the views are recycled by tracking the recycled list of items in the inner adapter using a List
that hold the currently loaded items:
By assuming some month has a 1000 days "Feb as it's always oppressed :)", and scrolling up/down to notice the loaded list and make sure that onViewRecycled()
get called.
public class InnerRecyclerAdapter extends RecyclerView.Adapter<InnerRecyclerAdapter.InnerViewHolder> {
private final ArrayList<Integer> currentLoadedPositions = new ArrayList<>();
@Override
public void onBindViewHolder(@NonNull InnerViewHolder holder, int position) {
holder.tvDay.setText(String.valueOf(position + 1));
currentLoadedPositions.add(position);
Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
}
@Override
public void onViewRecycled(@NonNull InnerViewHolder holder) {
super.onViewRecycled(holder);
currentLoadedPositions.remove(Integer.valueOf(holder.getAdapterPosition()));
Log.d(LOG_TAG, "onViewRecycled: " + days + " " + currentLoadedPositions);
}
// Rest of code is trimmed
}
Logs:
onViewRecycled: 1000 [0]
onViewRecycled: 1000 [0, 1]
onViewRecycled: 1000 [0, 1, 2]
onViewRecycled: 1000 [0, 1, 2, 3]
onViewRecycled: 1000 [0, 1, 2, 3, 4]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
onViewRecycled: 1000 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
onViewRecycled: 1000 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
onViewRecycled: 1000 [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13]
onViewRecycled: 1000 [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14]
onViewRecycled: 1000 [4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
onViewRecycled: 1000 [5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]
onViewRecycled: 1000 [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17]
onViewRecycled: 1000 [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
onViewRecycled: 1000 [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19]
onViewRecycled: 1000 [9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
onViewRecycled: 1000 [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]
onViewRecycled: 1000 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]
onViewRecycled: 1000 [12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
onViewRecycled: 1000 [13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
onViewRecycled: 1000 [14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25]
UPDATE 2
Still there are glitches because disabling/enabling scrolling; we can't pass the scroll order to the other RV, until the next scroll. This is manipulated slightly by reversing the initial outer RV scroll value; and using an arbitrary scroll value to the inner with
currentRV.smoothScrollBy(0, 10 * initialScroll)
. I wish if someone can suggest any other alternative to this.
Using a greater arbitrary value (like 30) makes the grammatical scroll looks smoother >> currentRV.smoothScrollBy(0, 30 * initialScroll)
And scrolling the outer scroll without reversing the scroll, makes it also looks more natural in the same direction of the scroll:
if (scrollDown) {
boolean isLastItemShown = currentLastItem == mMonths.get(currentPosition).dayCount;
if (!isLastItemShown) onScrollListener.onScroll(value);
enableOuterScroll(isLastItemShown);
} else {
boolean isFirstItemShown = currentFirstItem == 1;
if (!isFirstItemShown) onScrollListener.onScroll(value);
enableOuterScroll(isFirstItemShown);
}
UPDATE 3
Issue: glitches during the transition from the outer to inner RecyclerView
because the onScroll()
of the outer gets called before deciding whether we can scroll the inner or not.
By using OnTouchListener
to the outer RecyclerView and overriding onTouch()
and return true
to consume the event (so that onScrolled()
won't get called) until we decide that the inner can take the scroll over.
private float oldY = -1f;
outerRecyclerView.setOnTouchListener((v, event) -> {
Log.d(LOG_TAG, "onTouch: ");
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
oldY = -1;
break;
case MotionEvent.ACTION_MOVE:
float newY = event.getRawY();
Log.d(LOG_TAG, "onTouch: MOVE " + (oldY - newY));
if (oldY == -1f) {
oldY = newY;
return true; // avoid further listeners (i.e. addOnScrollListener)
} else if (oldY < newY) { // increases means scroll UP
outerAdapter.isOuterScrollingDown(false, (int) (oldY - newY));
oldY = newY;
} else if (oldY > newY) { // decreases means scroll DOWN
outerAdapter.isOuterScrollingDown(true, (int) (oldY - newY));
oldY = newY;
}
break;
}
return false;
});
UPDATE 4
RecyclerView
whenever the inner RV scrolled to its top or bottom edge so that it continues scrolling to the outer RV by a proportional speed.The scroll speed is inspired by this post. By applying the first/last item checks in the touched inner RV's OnTouchListener
& OnScrollListener
, and resetting stuff in a brand new touch event i.e. in MotionEvent.ACTION_DOWN
RecyclerView
sPreview:
The main issue of the nested scrolling of a RecyclerView
is that it doesn't implement NestedScrollingParent3 interface which is implemented by NestedScrollView
; So RecyclerView
can't handle nested scrolling of child views. So, trying to compensate that with a NestedScrollView
by wrapping the outer RecyclerView
within a NestedScrollView
, and disable the scrolling of the outer RecyclerView
Pros:
Cons:
RecyclerView
are not recycled and so that they have to be all loaded before showing up on the screen.Reason: Due to the nature of the NestedScrollView
>> Check (1), (2) questions that discussed the recycling issues:
Using a ViewPager2
that functions internally using a RecyclerView
solves the problem of recycling views, but only one page (one outer row) can present at a time.
Pros:
NestedScrollableHost
RecyclerView
in ViewPager2
Cons:
So we probably tackle this by researching either:
Upvotes: 12
Reputation: 6981
Check this Github Sample:
Extend BaseRecyclerAdapter(which you get whole code in github) in recycler adapter and implement below methods.
class ListAdapter : BaseRecyclerAdapter<ListDataModel, ChildDataModel>() {
// Pass parent and child model in BaseRecyclerAdapter
override fun getLayoutIdForType(): Int = R.layout.item_parent // Provide parent recycler item id
override fun getLayoutIdForChild(): Int = R.layout.item_child // Provide child recycler item id
// Click event fot parent recycler item
override fun onParentItemClick(triple: Triple<Int, Any, View>, viewDataBinding: ViewDataBinding) {
val data = triple.second as ListDataModel // Here you can get parent item data for clicked item
val position = triple.first // Get position of clicked item
val view = triple.third // Get view of clicked item
when (view.id) {
R.id.text_movie_year -> {
// Call this function where you want to expand collapse childView
expandCollapse(triple.first, viewDataBinding)
}
}
}
// Click event for child rectycler item
override fun onChildItemClicked(triple: Triple<Int, Any, View>, parentIndex: Int) {
val data = triple.second as ChildDataModel // Here you can get child item data for clicked item
val position = triple.first // Get position of clicked item
val view = triple.third // Get view of clicked item
when (view.id) {
R.id.img_download_movie -> {
// Here you can perform your action
}
}
}
}
Upvotes: 0
Reputation: 1379
Here's an extension to Zain's sample that avoids any custom scroll logic and chooses a combination of NestedScrollView
& RecyclerView
The key difference in approach here is to let the NestedScrollView
handle the complex scroll computation around when to intercept/dispatch scroll events to the nested layouts.
I've added a LinearLayout
container for the nested recyclers with a weight of 1 so they get the uniform height.
fwiw, we can also share view pools across recycler views for a better performance using RecyclerView.setRecycledViewPool()
PS: A definite hit on performance with this solution is that there will be m * n
views loaded upon setup where: m - number of rows, n - visible views for the inner recycler.
Hence, for a large value of m, this won't scale very well.
Upvotes: 1