Implementing Linked Scrolling Between RecyclerView and Bottom Sheet in Android
This article describes how the Snowball Android team solved the slow‑frame issue on the fund detail page by implementing a split‑screen loading layout using RecyclerView, CoordinatorLayout, and ViewPager, focusing on achieving a linked scrolling effect for the bottom discussion overlay through custom BottomSheetBehavior extensions and nested scrolling mechanisms.
Background The Snowball Android team needed to fix a slow‑frame problem on the fund detail page. One major optimization was to load the page in split screens using RecyclerView , CoordinatorLayout and ViewPager , with the key challenge of implementing a linked scrolling effect for the bottom discussion overlay.
Implementation Overview Before diving into the solution, the article recommends reading three reference articles about Android custom Behavior , the usage of BottomSheetBehavior , and nested scrolling mechanisms.
The overall layout consists of a CoordinatorLayout that contains the discussion overlay (implemented as a BottomSheetBehavior ) and a RecyclerView underneath. When the RecyclerView reaches its bottom, it should drive the overlay to scroll together, and the overlay should also be freely draggable.
Free Drag Implementation
The XML layout that enables free dragging of the overlay is shown below. The key attributes are app:behavior_peekHeight and app:layout_behavior pointing to a custom BottomSheetBehavior subclass.
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="match_parent">
<...SNBObservableNestedRecycleView android:layout_width="match_parent" android:layout_height="match_parent"/>
<androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent">
<LinearLayout android:orientation="horizontal" app:behavior_peekHeight="54dp" app:layout_behavior="...BottomSheetBehavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</RelativeLayout>The overlay is a BottomSheetBehavior that can be dragged up and down. By itself it provides free dragging, but it does not synchronize scrolling with the RecyclerView , so a custom behavior is required.
Behavior Extension
The custom behavior extends the system BottomSheetBehavior and adds two main capabilities:
When the RecyclerView reaches the bottom and the user continues dragging, the overlay starts scrolling together with the list.
When the combined scrolling reaches a certain threshold, the link is broken, allowing the RecyclerView to continue scrolling independently.
Key overridden methods include:
@Override
public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View directTargetChild, @NonNull View target, int axes, int type) {
return super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type);
}
@Override
public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dx, int dy, @NonNull int[] consumed, int type) {
super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
}
@Override
public void onNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int type, @NonNull int[] consumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed);
}
@Override
public void onStopNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child, @NonNull View target, int type) {
super.onStopNestedScroll(coordinatorLayout, child, target, type);
}Nested Scrolling Mechanism
Android’s nested scrolling works with two interfaces: NestedScrollingChild (implemented by the RecyclerView ) and NestedScrollingParent (implemented by CoordinatorLayout ). The child initiates scrolling via startNestedScroll , the parent can pre‑consume motion in onNestedPreScroll , and any remaining distance is handled by the child in onNestedScroll . When the gesture ends, onStopNestedScroll is called.
Specific Implementation
Case 1 – RecyclerView drives the overlay An OnScrollListener on the RecyclerView detects when the list reaches the bottom. It then calls a custom setSlideHeight method, which triggers a layout pass where ViewCompat.offsetTopAndBottom(child, slideOffset) moves the overlay. The slide offset is calculated as:
int slideHeight = measuredHeight - (RecyclerViewHeight - scrollY()) + peekHeight;
slideOffset = parentHeight - slideHeight;Case 2 – Overlay drives the RecyclerView In the custom behavior’s onNestedPreScroll , the vertical distance dy is obtained. After adjusting the overlay’s position with ViewCompat.offsetTopAndBottom , the same distance is passed to the list via recyclerView.scrollBy(0, dy) . The snippet below shows the core logic:
@Override
public void onNestedPreScroll(...) {
int currentTop = child.getTop();
int newTop = currentTop - dy;
if (dy > 0) { // scrolling up
if (newTop < getExpandedOffset()) {
consumed[1] = currentTop - getExpandedOffset();
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
recyclerView.scrollBy(0, consumed[1]);
setStateInternal(STATE_EXPANDED);
} else {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
recyclerView.scrollBy(0, dy);
setStateInternal(STATE_DRAGGING);
}
} else { // scrolling down
if (!target.canScrollVertically(-1) || (slideByOutsideView && state == STATE_DRAGGING)) {
if (newTop <= collapsedOffset || hideable) {
consumed[1] = dy;
ViewCompat.offsetTopAndBottom(child, -dy);
recyclerView.scrollBy(0, dy);
setStateInternal(STATE_DRAGGING);
} else {
consumed[1] = currentTop - collapsedOffset;
ViewCompat.offsetTopAndBottom(child, -consumed[1]);
recyclerView.scrollBy(0, consumed[1]);
setStateInternal(STATE_COLLAPSED);
}
}
}
}Conclusion
The article reviews the overall structure, the free‑drag implementation of the discussion overlay, the principles of Behavior and nested scrolling, and the concrete code that links RecyclerView and CoordinatorLayout scrolling. The provided demo repository (https://github.com/liuyak/NestedBehaviorDemo.git) contains a working example.
Snowball Engineer Team
Proactivity, efficiency, professionalism, and empathy are the core values of the Snowball Engineer Team; curiosity, passion, and sharing of technology drive their continuous progress.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.