I'm trying to build a following layout using CoordinatorLayout and AppBarLayout:
|View 1 (Header)|
|View 2 ------------|
|RecyclerView--- |
The behavior I want to achieve is as the following:
This is the testing layout I created as a proof of concept.
<android.support.design.widget.CoordinatorLayout
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">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/view1"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:text="TEST TITLE"
android:textSize="50sp"
app:layout_scrollFlags="scroll|enterAlways|snap" />
<TextView
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="TEST TEST TEST TEST TEST TEST TEST TEST"
android:textSize="70sp"
android:minHeight="50dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</android:support.design.widget.CoordinatorLayout>
I created a test adapter to add few TextView items in RV (nothing fancy here). When I run the code, It does not actually work as expected.
As I scroll down, I confirmed that View 1 is completely collapsed.
Scroll down more. View 2 collapses until it reaches minHeight. RV started scrolling after that. This is working as expected so far.
I looked into the AppBarLayout implementation and the issue seems to be because the AppBarLayout calculates the scroll range of the whole view based on the scrollFlags, and offsetting the whole view based on the scroll offset, rather than updating each child View.
Does anyone know if there's any workaround or open source lib to resolve this issue? It doesn't have to be CoordinatorLayout/AppBarLayout approach, but I need to produce the behavior.
Thank you in advance.
Okay, I found a solution by myself, and decided to post my solution for people with similar problems.
The solution was to create a NestedCoordinatorLayout that extends CoordinatorLayout by implementing NestedScollingChild so that we can interact between two AppBarLayouts. I referenced NestedScrollView source code and the answer in this post, https://stackoverflow.com/a/36881816/6272520, but I had to make few changes to make it work the way I want to.
Here's the code for the NestedCoordinatorLayout.
public class NestedCoordinatorLayout extends CoordinatorLayout implements NestedScrollingChild {
private final NestedScrollingChildHelper scrollingChildHelper;
private final int[] parentOffsetInWindow = new int[2];
private final int[] parentScrollConsumed = new int[2];
public NestedCoordinatorLayout(Context context) {
this(context, null);
}
public NestedCoordinatorLayout(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public NestedCoordinatorLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
scrollingChildHelper = new NestedScrollingChildHelper(this);
setNestedScrollingEnabled(true);
}
//NestedScrollingChild
@Override
public void setNestedScrollingEnabled(boolean enabled) {
scrollingChildHelper.setNestedScrollingEnabled(enabled);
}
@Override
public boolean isNestedScrollingEnabled() {
return scrollingChildHelper.isNestedScrollingEnabled();
}
@Override
public boolean startNestedScroll(int axes) {
return scrollingChildHelper.startNestedScroll(axes);
}
@Override
public void stopNestedScroll() {
scrollingChildHelper.stopNestedScroll();
}
@Override
public boolean hasNestedScrollingParent() {
return scrollingChildHelper.hasNestedScrollingParent();
}
@Override
public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed,
int dyUnconsumed, int[] offsetInWindow) {
return scrollingChildHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow);
}
@Override
public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow) {
return scrollingChildHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow);
}
@Override
public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed) {
return scrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
}
@Override
public boolean dispatchNestedPreFling(float velocityX, float velocityY) {
return scrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
}
//NestedScrollingParent
@Override
public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
scrollingChildHelper.startNestedScroll(nestedScrollAxes);
return super.onStartNestedScroll(child, target, nestedScrollAxes);
}
@Override
public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
final int[] parentConsumed = parentScrollConsumed;
//This is where the most important change happens.
//During the prescroll, we want to decrease dx/dy.
//This will make sure the top bar gets the scroll event first.
if (dispatchNestedPreScroll(dx, dy, parentConsumed, null)) {
dx -= parentConsumed[0];
dy -= parentConsumed[1];
consumed[0] += parentConsumed[0];
consumed[1] += parentConsumed[1];
}
super.onNestedPreScroll(target, dx, dy, consumed);
}
@Override
public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed) {
dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, parentOffsetInWindow);
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
}
@Override
public void onStopNestedScroll(View target) {
scrollingChildHelper.onStopNestedScroll(target);
super.onStopNestedScroll(target);
}
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
scrollingChildHelper.dispatchNestedFling(velocityX, velocityY, consumed);
return super.onNestedFling(target, velocityX, velocityY, consumed);
}
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
scrollingChildHelper.dispatchNestedPreFling(velocityX, velocityY);
return super.onNestedPreFling(target, velocityX, velocityY);
}
@Override
public void onNestedScrollAccepted(View child, View target, int axes) {
super.onNestedScrollAccepted(child, target, axes);
startNestedScroll(axes & ViewCompat.SCROLL_AXIS_VERTICAL);
}
}
And the updated xml file looks like this:
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout 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">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout_for_view1"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/view1"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:text="TEST TITLE"
android:background="@android:color/white"
android:textSize="50sp"
app:layout_scrollFlags="scroll|enterAlways|snap" />
</android.support.design.widget.AppBarLayout>
<!-- Consider this like a NestedScrollView.
You need to have a scrolling behavior -->
<NestedCoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<android.support.design.widget.AppBarLayout
android:id="@+id/app_bar_layout_for_view2"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/view2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="50dp"
android:text="TEST TEST TEST TEST TEST TEST TEST TEST"
android:textSize="70sp"
app:layout_scrollFlags="scroll|exitUntilCollapsed" />
</android.support.design.widget.AppBarLayout>
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</NestedCoordinatorLayout>
</android.support.design.widget.CoordinatorLayout>