How to have a collapsing of top view into a smaller sized view?

11,356

Note: Full updated project is available here.

How can I make the scrolling being blocked when the top view is expanded, yet allow to collapse while scrolling ?

Issue #1: The RecyclerView should not be able to scroll at all when the app bar is not collapsed. To fix this, add enterAlways to the scroll flags for the CollapsingToolbarLayout as follows:

<android.support.design.widget.CollapsingToolbarLayout
    android:id="@+id/collapsingToolbarLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipChildren="false"
    android:clipToPadding="false"
    android:fitsSystemWindows="true"
    app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
    app:statusBarScrim="?attr/colorPrimaryDark">

enterAlways will not cause the app bar to open when closed since you are suppressing that functionality but works as desired otherwise.

Issue #2: When the app bar is fully expanded, the RecyclerView should not be allowed to scroll up. This happens to be a distinct issue from issue #1.

[Updated] To correct this, modify the behavior for the RecyclerView to consume scroll when the RecyclerView tries to scroll up and the app bar is fully expanded or will be fully expanded after the scroll(dy) is consumed . The RecyclerView can scroll up, but it never sees that action since its behavior, SlidingPanelBehavior, consumes the scroll. If the app bar is not fully expanded but will be expanded after the current scroll is consumed, the behavior forces the app bar to fully expand by calling modifying dy and calling the super before fully consuming the scroll. (See SlidingPanelBehavior#onNestedPreScroll()). (In the previous answer, the appBar behavior was modified. Putting the behavior change on RecyclerView is a better choice.)

Issue #3: Setting nested scrolling for the RecyclerView to enable/disabled when nested scrolling is already in the required state causes problems. To avoid these issues, only change the state of nested scrolling when a change is really being made with the following code change in ScrollingActivity:

private void setExpandAndCollapseEnabled(boolean enabled) {
    if (mNestedView.isNestedScrollingEnabled() != enabled) {
        mNestedView.setNestedScrollingEnabled(enabled);
    }
}

This is how the test app behaves with the changes from above:

enter image description here

The changed modules with the above-mentioned changes are at the end of the post.

How can I make the top view be replaced with a smaller one when collapsed (and back to large one when expanded), instead of completely disappear ?

[Update] Make the smaller view a direct child of CollapsingToolbarLayout so it is a sibling of Toolbar. The following is a demonstration of this approach. The collapseMode of the smaller view is set to pin. The smaller view's margins as well as the margins of the toolbar are adjusted so the smaller view falls immediately below the toolbar. Since CollapsingToolbarLayout is a FrameLayout, views stack and the height of the FrameLayout just becomes the height of the tallest child view. This structure will avoid the issue where the insets needed adjustment and the problem with the missing click effect.

One final issue remains and that dragging the appbar down should open it with the assumption that dragging the smaller view down should not open the appbar. Permitting the appbar to open upon dragging is accomplished with setDragCallback of AppBarLayout.Behavior. Since the smaller view is incorporated into the appBar, dragging it down will open the appbar. To prevent this, a new behavior called MyAppBarBehavior is attached to the appbar. This behavior, in conjunction with code in the MainActivity prevents dragging of the smaller view to open the appbar but will permit the toolbar to be dragged.

activity_main.xml

<android.support.design.widget.CoordinatorLayout 
    android:id="@+id/coordinatorLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/app_bar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:fitsSystemWindows="true"
        android:stateListAnimator="@null"
        android:theme="@style/AppTheme.AppBarOverlay"
        app:expanded="false"
        app:layout_behavior=".MyAppBarBehavior"
        tools:targetApi="lollipop">

        <android.support.design.widget.CollapsingToolbarLayout
            android:id="@+id/collapsingToolbarLayout"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:clipChildren="false"
            android:clipToPadding="false"
            android:fitsSystemWindows="true"
            app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
            app:statusBarScrim="?attr/colorPrimaryDark">

            <!--large view -->
            <LinearLayout
                android:id="@+id/largeView"
                android:layout_width="match_parent"
                android:layout_height="280dp"
                android:layout_marginTop="?attr/actionBarSize"
                android:orientation="vertical"
                app:layout_collapseMode="parallax"
                app:layout_collapseParallaxMultiplier="1.0">

                <TextView
                    android:id="@+id/largeTextView"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_gravity="center"
                    android:background="?attr/selectableItemBackgroundBorderless"
                    android:clickable="true"
                    android:focusable="true"
                    android:focusableInTouchMode="false"
                    android:gravity="center"
                    android:text="largeView"
                    android:textSize="14dp"
                    tools:background="?attr/colorPrimary"
                    tools:layout_gravity="top|center_horizontal"
                    tools:layout_height="40dp"
                    tools:layout_width="40dp"
                    tools:text="1" />

            </LinearLayout>

            <!--top toolbar-->
            <android.support.v7.widget.Toolbar
                android:id="@+id/toolbar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginBottom="@dimen/small_view_height"
                app:contentInsetStart="0dp"
                app:layout_collapseMode="pin"
                app:popupTheme="@style/AppTheme.PopupOverlay">

                <android.support.constraint.ConstraintLayout
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:clickable="true"
                    android:focusable="true">

                    <LinearLayout
                        android:id="@+id/expandCollapseButton"
                        android:layout_width="match_parent"
                        android:layout_height="?attr/actionBarSize"
                        android:background="?android:selectableItemBackground"
                        android:gravity="center_vertical"
                        android:orientation="horizontal"
                        app:layout_constraintStart_toStartOf="parent"
                        app:layout_constraintTop_toTopOf="parent">

                        <TextView
                            android:id="@+id/titleTextView"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:ellipsize="end"
                            android:gravity="center"
                            android:maxLines="1"
                            android:text="title"
                            android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
                            android:textColor="@android:color/white" />

                        <ImageView
                            android:id="@+id/arrowImageView"
                            android:layout_width="wrap_content"
                            android:layout_height="wrap_content"
                            android:layout_marginLeft="8dp"
                            android:layout_marginStart="8dp"
                            app:srcCompat="@android:drawable/arrow_up_float"
                            tools:ignore="ContentDescription,RtlHardcoded" />
                    </LinearLayout>

                </android.support.constraint.ConstraintLayout>

            </android.support.v7.widget.Toolbar>

            <!--small view-->
            <LinearLayout
                android:id="@+id/smallLayout"
                android:layout_width="match_parent"
                android:layout_height="@dimen/small_view_height"
                android:layout_marginTop="?attr/actionBarSize"
                android:clipChildren="false"
                android:clipToPadding="false"
                android:orientation="horizontal"
                app:layout_collapseMode="pin"
                tools:background="#ff330000"
                tools:layout_height="@dimen/small_view_height">

                <TextView
                    android:id="@+id/smallTextView"
                    android:layout_width="match_parent"
                    android:layout_height="match_parent"
                    android:layout_gravity="center"
                    android:background="?attr/selectableItemBackgroundBorderless"
                    android:clickable="true"
                    android:focusable="true"
                    android:focusableInTouchMode="false"
                    android:gravity="center"
                    android:text="smallView"
                    android:textSize="14dp"
                    tools:background="?attr/colorPrimary"
                    tools:layout_gravity="top|center_horizontal"
                    tools:layout_height="40dp"
                    tools:layout_width="40dp"
                    tools:text="1" />

            </LinearLayout>
        </android.support.design.widget.CollapsingToolbarLayout>

    </android.support.design.widget.AppBarLayout>

    <com.example.expandedtopviewtestupdate.MyRecyclerView
        android:id="@+id/nestedView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"
        tools:context=".SlidingPanelBehavior" />

</android.support.design.widget.CoordinatorLayout>

Finally, in the addOnOffsetChangedListener add the following code to fade out/fade in the smaller view as the app bar expands and contracts. Once the view's alpha is zero (invisible), set its visibility to View.INVISIBLE so it can't be clicked. Once the view's alpha increases above zero, make it visible and clickable by setting its visibility to View.VISIBLE.

mSmallLayout.setAlpha((float) -verticalOffset / totalScrollRange);
// If the small layout is not visible, make it officially invisible so
// it can't receive clicks.
if (alpha == 0) {
    mSmallLayout.setVisibility(View.INVISIBLE);
} else if (mSmallLayout.getVisibility() == View.INVISIBLE) {
    mSmallLayout.setVisibility(View.VISIBLE);
}

Here are the results:

enter image description here

Here are the new modules with all of the above changes incorporated.

MainActivity.java

public class MainActivity extends AppCompatActivity
    implements MyRecyclerView.AppBarTracking {
    private MyRecyclerView mNestedView;
    private int mAppBarOffset = 0;
    private boolean mAppBarIdle = true;
    private int mAppBarMaxOffset = 0;
    private AppBarLayout mAppBar;
    private boolean mIsExpanded = false;
    private ImageView mArrowImageView;
    private LinearLayout mSmallLayout;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        LinearLayout expandCollapse;

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Toolbar toolbar = findViewById(R.id.toolbar);
        expandCollapse = findViewById(R.id.expandCollapseButton);
        mArrowImageView = findViewById(R.id.arrowImageView);
        mNestedView = findViewById(R.id.nestedView);
        mAppBar = findViewById(R.id.app_bar);
        mSmallLayout = findViewById(R.id.smallLayout);

        // Log when the small text view is clicked
        findViewById(R.id.smallTextView).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "<<<<click small layout");
            }
        });

        // Log when the big text view is clicked.
        findViewById(R.id.largeTextView).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Log.d(TAG, "<<<<click big view");
            }
        });

        setSupportActionBar(toolbar);
        ActionBar ab = getSupportActionBar();
        if (ab != null) {
            getSupportActionBar().setDisplayShowTitleEnabled(false);
        }

        mAppBar.post(new Runnable() {
            @Override
            public void run() {
                mAppBarMaxOffset = -mAppBar.getTotalScrollRange();

                CoordinatorLayout.LayoutParams lp =
                    (CoordinatorLayout.LayoutParams) mAppBar.getLayoutParams();
                MyAppBarBehavior behavior = (MyAppBarBehavior) lp.getBehavior();
                // Only allow drag-to-open if the drag touch is on the toolbar.
                // Once open, all drags are allowed.
                if (behavior != null) {
                    behavior.setCanOpenBottom(findViewById(R.id.toolbar).getHeight());
                }
            }
        });

        mNestedView.setAppBarTracking(this);
        mNestedView.setLayoutManager(new LinearLayoutManager(this));
        mNestedView.setAdapter(new RecyclerView.Adapter<RecyclerView.ViewHolder>() {
            @Override
            public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
                return new ViewHolder(
                    LayoutInflater.from(parent.getContext())
                        .inflate(android.R.layout.simple_list_item_1, parent, false));
            }

            @SuppressLint("SetTextI18n")
            @Override
            public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
                ((TextView) holder.itemView.findViewById(android.R.id.text1))
                    .setText("Item " + position);
            }

            @Override
            public int getItemCount() {
                return 200;
            }

            class ViewHolder extends RecyclerView.ViewHolder {
                public ViewHolder(View view) {
                    super(view);
                }
            }
        });

        mAppBar.addOnOffsetChangedListener(new AppBarLayout.OnOffsetChangedListener() {
            @Override
            public void onOffsetChanged(AppBarLayout appBarLayout, int verticalOffset) {
                mAppBarOffset = verticalOffset;
                int totalScrollRange = appBarLayout.getTotalScrollRange();
                float progress = (float) (-verticalOffset) / (float) totalScrollRange;
                mArrowImageView.setRotation(-progress * 180);
                mIsExpanded = verticalOffset == 0;
                mAppBarIdle = mAppBarOffset >= 0 || mAppBarOffset <= mAppBarMaxOffset;
                float alpha = (float) -verticalOffset / totalScrollRange;
                mSmallLayout.setAlpha(alpha);

                // If the small layout is not visible, make it officially invisible so
                // it can't receive clicks.
                if (alpha == 0) {
                    mSmallLayout.setVisibility(View.INVISIBLE);
                } else if (mSmallLayout.getVisibility() == View.INVISIBLE) {
                    mSmallLayout.setVisibility(View.VISIBLE);
                }
            }
        });

        expandCollapse.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                setExpandAndCollapseEnabled(true);
                if (mIsExpanded) {
                    setExpandAndCollapseEnabled(false);
                }
                mIsExpanded = !mIsExpanded;
                mNestedView.stopScroll();
                mAppBar.setExpanded(mIsExpanded, true);
            }
        });
    }

    private void setExpandAndCollapseEnabled(boolean enabled) {
        if (mNestedView.isNestedScrollingEnabled() != enabled) {
            mNestedView.setNestedScrollingEnabled(enabled);
        }
    }

    @Override
    public boolean isAppBarExpanded() {
        return mAppBarOffset == 0;
    }

    @Override
    public boolean isAppBarIdle() {
        return mAppBarIdle;
    }

    private static final String TAG = "MainActivity";
}

SlidingPanelBehavior.java

public class SlidingPanelBehavior extends AppBarLayout.ScrollingViewBehavior {
    private AppBarLayout mAppBar;

    public SlidingPanelBehavior() {
        super();
    }

    public SlidingPanelBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    public boolean layoutDependsOn(final CoordinatorLayout parent, View child, View dependency) {
        if (mAppBar == null && dependency instanceof AppBarLayout) {
            // Capture our appbar for later use.
            mAppBar = (AppBarLayout) dependency;
        }
        return dependency instanceof AppBarLayout;
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent, View child, MotionEvent event) {
        int action = event.getAction();

        if (event.getAction() != MotionEvent.ACTION_DOWN) { // Only want "down" events
            return false;
        }
        if (getAppBarLayoutOffset(mAppBar) == -mAppBar.getTotalScrollRange()) {
            // When appbar is collapsed, don't let it open through nested scrolling.
            setNestedScrollingEnabledWithTest((NestedScrollingChild2) child, false);
        } else {
            // Appbar is partially to fully expanded. Set nested scrolling enabled to activate
            // the methods within this behavior.
            setNestedScrollingEnabledWithTest((NestedScrollingChild2) child, true);
        }
        return false;
    }

    @Override
    public boolean onStartNestedScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                       @NonNull View directTargetChild, @NonNull View target,
                                       int axes, int type) {
        //noinspection RedundantCast
        return ((NestedScrollingChild2) child).isNestedScrollingEnabled();
    }

    @Override
    public void onNestedPreScroll(@NonNull CoordinatorLayout coordinatorLayout, @NonNull View child,
                                  @NonNull View target, int dx, int dy, @NonNull int[] consumed,
                                  int type) {
        // How many pixels we must scroll to fully expand the appbar. This value is <= 0.
        final int appBarOffset = getAppBarLayoutOffset(mAppBar);

        // Check to see if this scroll will expand the appbar 100% or collapse it fully.
        if (dy <= appBarOffset) {
            // Scroll by the amount that will fully expand the appbar and dispose of the rest (dy).
            super.onNestedPreScroll(coordinatorLayout, mAppBar, target, dx,
                                    appBarOffset, consumed, type);
            consumed[1] += dy;
        } else if (dy >= (mAppBar.getTotalScrollRange() + appBarOffset)) {
            // This scroll will collapse the appbar. Collapse it and dispose of the rest.
            super.onNestedPreScroll(coordinatorLayout, mAppBar, target, dx,
                                    mAppBar.getTotalScrollRange() + appBarOffset,
                                    consumed, type);
            consumed[1] += dy;
        } else {
            // This scroll will leave the appbar partially open. Just do normal stuff.
            super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed, type);
        }
    }

    /**
     * {@code onNestedPreFling()} is overriden to address a nested scrolling defect that was
     * introduced in API 26. This method prevent the appbar from misbehaving when scrolled/flung.
     * <p>
     * Refer to <a href="https://issuetracker.google.com/issues/65448468"  target="_blank">"Bug in design support library"</a>
     */

    @Override
    public boolean onNestedPreFling(@NonNull CoordinatorLayout coordinatorLayout,
                                    @NonNull View child, @NonNull View target,
                                    float velocityX, float velocityY) {
        //noinspection RedundantCast
        if (((NestedScrollingChild2) child).isNestedScrollingEnabled()) {
            // Just stop the nested fling and let the appbar settle into place.
            ((NestedScrollingChild2) child).stopNestedScroll(ViewCompat.TYPE_NON_TOUCH);
            return true;
        }
        return super.onNestedPreFling(coordinatorLayout, child, target, velocityX, velocityY);
    }

    private static int getAppBarLayoutOffset(AppBarLayout appBar) {
        final CoordinatorLayout.Behavior behavior =
            ((CoordinatorLayout.LayoutParams) appBar.getLayoutParams()).getBehavior();
        if (behavior instanceof AppBarLayout.Behavior) {
            return ((AppBarLayout.Behavior) behavior).getTopAndBottomOffset();
        }
        return 0;
    }

    // Something goes amiss when the flag it set to its current value, so only call
    // setNestedScrollingEnabled() if it will result in a change.
    private void setNestedScrollingEnabledWithTest(NestedScrollingChild2 child, boolean enabled) {
        if (child.isNestedScrollingEnabled() != enabled) {
            child.setNestedScrollingEnabled(enabled);
        }
    }

    @SuppressWarnings("unused")
    private static final String TAG = "SlidingPanelBehavior";
}

MyRecyclerView.kt

/**A RecyclerView that allows temporary pausing of casuing its scroll to affect appBarLayout, based on https://stackoverflow.com/a/45338791/878126 */
class MyRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) {
    private var mAppBarTracking: AppBarTracking? = null
    private var mView: View? = null
    private var mTopPos: Int = 0
    private var mLayoutManager: LinearLayoutManager? = null

    interface AppBarTracking {
        fun isAppBarIdle(): Boolean
        fun isAppBarExpanded(): Boolean
    }

    override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?, type: Int): Boolean {
        if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle()
                && isNestedScrollingEnabled) {
            if (dy > 0) {
                if (mAppBarTracking!!.isAppBarExpanded()) {
                    consumed!![1] = dy
                    return true
                }
            } else {
                mTopPos = mLayoutManager!!.findFirstVisibleItemPosition()
                if (mTopPos == 0) {
                    mView = mLayoutManager!!.findViewByPosition(mTopPos)
                    if (-mView!!.top + dy <= 0) {
                        consumed!![1] = dy - mView!!.top
                        return true
                    }
                }
            }
        }
        if (dy < 0 && type == ViewCompat.TYPE_TOUCH && mAppBarTracking!!.isAppBarExpanded()) {
            consumed!![1] = dy
            return true
        }

        val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
        if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
            offsetInWindow[1] = 0
        return returnValue
    }

    override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
        super.setLayoutManager(layout)
        mLayoutManager = layoutManager as LinearLayoutManager
    }

    fun setAppBarTracking(appBarTracking: AppBarTracking) {
        mAppBarTracking = appBarTracking
    }

    override fun fling(velocityX: Int, velocityY: Int): Boolean {
        var velocityY = velocityY
        if (!mAppBarTracking!!.isAppBarIdle()) {
            val vc = ViewConfiguration.get(context)
            velocityY = if (velocityY < 0) -vc.scaledMinimumFlingVelocity
            else vc.scaledMinimumFlingVelocity
        }

        return super.fling(velocityX, velocityY)
    }
}

MyAppBarBehavior.java

/**
 * Attach this behavior to AppBarLayout to disable the bottom portion of a closed appBar
 * so it cannot be touched to open the appBar. This behavior is helpful if there is some
 * portion of the appBar that displays when the appBar is closed, but should not open the appBar
 * when the appBar is closed.
 */
public class MyAppBarBehavior extends AppBarLayout.Behavior {

    // Touch above this y-axis value can open the appBar.
    private int mCanOpenBottom;

    // Determines if the appBar can be dragged open or not via direct touch on the appBar.
    private boolean mCanDrag = true;

    @SuppressWarnings("unused")
    public MyAppBarBehavior() {
        init();
    }

    @SuppressWarnings("unused")
    public MyAppBarBehavior(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    private void init() {
        setDragCallback(new AppBarLayout.Behavior.DragCallback() {
            @Override
            public boolean canDrag(@NonNull AppBarLayout appBarLayout) {
                return mCanDrag;
            }
        });
    }

    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent,
                                         AppBarLayout child,
                                         MotionEvent event) {

        if (event.getAction() == MotionEvent.ACTION_DOWN) {
            // If appBar is closed. Only allow scrolling in defined area.
            if (child.getTop() <= -child.getTotalScrollRange()) {
                mCanDrag = event.getY() < mCanOpenBottom;
            }
        }
        return super.onInterceptTouchEvent(parent, child, event);
    }

    public void setCanOpenBottom(int bottom) {
        mCanOpenBottom = bottom;
    }
}
Share:
11,356

Related videos on Youtube

android developer
Author by

android developer

Really like to develop Android apps &amp; libraries on my spare time. Github website: https://github.com/AndroidDeveloperLB/ My spare time apps: https://play.google.com/store/apps/developer?id=AndroidDeveloperLB

Updated on June 04, 2022

Comments

  • android developer
    android developer almost 2 years

    This question was asked before in a too broad and unclear way here, so I've made it much more specific, with full explanation and code of what I've tried.

    Background

    I'm required to mimic the way the Google Calendar has a view at the top, that can animate and push down the view at the bottom, yet it has extra, different behavior. I've summarized what I'm trying to do on 3 characteristics:

    1. Pressing on the toolbar will always work, to toggle expanding/collapsing of the top view, while having an arrow icon that changes its rotation. This is like on Google Calendar app.
    2. The top view will always snap, just like on Google Calendar app.
    3. When the top view is collapsed, only pressing on the toolbar will allow to expand it. This is like on Google Calendar app
    4. When the top view is expanded, scrolling at the bottom view will only allow to collapse. If you try to scroll the other direction, nothing occurs, not even to the bottom view. This is like on Google Calendar app
    5. Once collapsed, the top view will be replaced with a smaller view. This means it will always take some space, above the bottom view. This is not like on Google Calendar app, because on the Calendar app, the top view completely disappears once you collapse it.

    Here's how Google Calendar app look like:

    enter image description here

    Scrolling on the bottom view also slowly hides the view at the top:

    enter image description here

    The problem

    Using various solutions I've found in the past, I've succeeded to implement only a part of the needed behavior:

    1. Having some UI in the toolbar is done by having some views in it, including the arrow view. For manual expanding/collapsing I use setExpanded on the AppBarLayout view. For the rotation of the arrow, I use a listener of how much the AppBarLayout has resized, using addOnOffsetChangedListener on it.

    2. Snapping is easily done by adding snap value into layout_scrollFlags attribute of the CollapsingToolbarLayout. However, to make it really work well, without weird issues (reported here), I used this solution.

    3. Blocking of affecting the top view when scrolling can be done by using the same code I've used on #2 (here), by calling setExpandEnabled there. This works fine for when the top view is collapsed.

    4. Similar to #3, but sadly, since it uses setNestedScrollingEnabled, which is in both directions, this works well only when the top view is collapsed. When it's expanded, it still allows the bottom view to scroll up, as opposed to Calendar app. When expanded, I need it to only allow to collapse, without allowing to really scroll.

    Here's a demonstration of the good, and the bad:

    enter image description here

    1. This I've failed completely to do. I've tried a lot of solutions I've thought about, putting views in various places with various flags.

    In short, I've succeeded doing 1-3, but not 4-5.

    The code

    Here's the current code (also available as whole project here) :

    ScrollingActivity.kt

    class ScrollingActivity : AppCompatActivity(), AppBarTracking {
    
        private var mNestedView: MyRecyclerView? = null
        private var mAppBarOffset: Int = 0
        private var mAppBarIdle = false
        private var mAppBarMaxOffset: Int = 0
    
        private var isExpanded: Boolean = false
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_scrolling)
            val toolbar = findViewById<Toolbar>(R.id.toolbar)
            setSupportActionBar(toolbar)
            mNestedView = findViewById(R.id.nestedView)
            app_bar.addOnOffsetChangedListener({ appBarLayout, verticalOffset ->
                mAppBarOffset = verticalOffset
                val totalScrollRange = appBarLayout.totalScrollRange
                val progress = (-verticalOffset).toFloat() / totalScrollRange
                arrowImageView.rotation = 180 + progress * 180
                isExpanded = verticalOffset == 0;
                mAppBarIdle = mAppBarOffset >= 0 || mAppBarOffset <= mAppBarMaxOffset
                if (mAppBarIdle)
                    setExpandAndCollapseEnabled(isExpanded)
            })
    
            app_bar.post(Runnable { mAppBarMaxOffset = -app_bar.totalScrollRange })
    
            mNestedView!!.setAppBarTracking(this)
            mNestedView!!.layoutManager = LinearLayoutManager(this)
            mNestedView!!.adapter = object : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
                override fun getItemCount(): Int = 100
    
                override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
                    return object : ViewHolder(LayoutInflater.from(parent.context).inflate(android.R.layout.simple_list_item_1, parent, false)) {}
                }
    
                override fun onBindViewHolder(holder: ViewHolder, position: Int) {
                    (holder.itemView.findViewById<View>(android.R.id.text1) as TextView).text = "item $position"
                }
            }
    
            expandCollapseButton.setOnClickListener({ v ->
                isExpanded = !isExpanded
                app_bar.setExpanded(isExpanded, true)
            })
        }
    
        private fun setExpandAndCollapseEnabled(enabled: Boolean) {
            mNestedView!!.isNestedScrollingEnabled = enabled
        }
    
        override fun isAppBarExpanded(): Boolean = mAppBarOffset == 0
        override fun isAppBarIdle(): Boolean = mAppBarIdle
    }
    

    MyRecyclerView.kt

    /**A RecyclerView that allows temporary pausing of casuing its scroll to affect appBarLayout, based on https://stackoverflow.com/a/45338791/878126 */
    class MyRecyclerView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0) : RecyclerView(context, attrs, defStyle) {
        private var mAppBarTracking: AppBarTracking? = null
        private var mView: View? = null
        private var mTopPos: Int = 0
        private var mLayoutManager: LinearLayoutManager? = null
    
        interface AppBarTracking {
            fun isAppBarIdle(): Boolean
            fun isAppBarExpanded(): Boolean
        }
    
        override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?,
                                             type: Int): Boolean {
            if (type == ViewCompat.TYPE_NON_TOUCH && mAppBarTracking!!.isAppBarIdle()
                    && isNestedScrollingEnabled) {
                if (dy > 0) {
                    if (mAppBarTracking!!.isAppBarExpanded()) {
                        consumed!![1] = dy
                        return true
                    }
                } else {
                    mTopPos = mLayoutManager!!.findFirstVisibleItemPosition()
                    if (mTopPos == 0) {
                        mView = mLayoutManager!!.findViewByPosition(mTopPos)
                        if (-mView!!.top + dy <= 0) {
                            consumed!![1] = dy - mView!!.top
                            return true
                        }
                    }
                }
            }
    
            val returnValue = super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
            if (offsetInWindow != null && !isNestedScrollingEnabled && offsetInWindow[1] != 0)
                offsetInWindow[1] = 0
            return returnValue
        }
    
        override fun setLayoutManager(layout: RecyclerView.LayoutManager) {
            super.setLayoutManager(layout)
            mLayoutManager = layoutManager as LinearLayoutManager
        }
    
        fun setAppBarTracking(appBarTracking: AppBarTracking) {
            mAppBarTracking = appBarTracking
        }
    
    }
    

    ScrollingCalendarBehavior.kt

    class ScrollingCalendarBehavior(context: Context, attrs: AttributeSet) : AppBarLayout.Behavior(context, attrs) {
        override fun onInterceptTouchEvent(parent: CoordinatorLayout?, child: AppBarLayout?, ev: MotionEvent): Boolean = false
    }
    

    activity_scrolling.xml

    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/coordinatorLayout" xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ScrollingActivity">
    
        <android.support.design.widget.AppBarLayout
            android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content"
            android:fitsSystemWindows="true" android:stateListAnimator="@null" android:theme="@style/AppTheme.AppBarOverlay"
            app:expanded="false" app:layout_behavior="com.example.user.expandingtopviewtest.ScrollingCalendarBehavior"
            tools:targetApi="lollipop">
    
            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent"
                android:layout_height="match_parent" android:fitsSystemWindows="true"
                android:minHeight="?attr/actionBarSize" app:contentScrim="?attr/colorPrimary"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" app:statusBarScrim="?attr/colorPrimaryDark">
    
                <LinearLayout
                    android:layout_width="match_parent" android:layout_height="250dp"
                    android:layout_marginTop="?attr/actionBarSize" app:layout_collapseMode="parallax"
                    app:layout_collapseParallaxMultiplier="1.0">
    
                    <TextView
                        android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="10dp"
                        android:paddingRight="10dp" android:text="some large, expanded view"/>
                </LinearLayout>
    
                <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar" android:layout_width="match_parent"
                    android:layout_height="?attr/actionBarSize" app:layout_collapseMode="pin"
                    app:popupTheme="@style/AppTheme.PopupOverlay">
    
                    <android.support.constraint.ConstraintLayout
                        android:id="@+id/expandCollapseButton" android:layout_width="match_parent"
                        android:layout_height="?attr/actionBarSize" android:background="?android:selectableItemBackground"
                        android:clickable="true" android:focusable="true" android:orientation="vertical">
    
                        <TextView
                            android:id="@+id/titleTextView" android:layout_width="wrap_content"
                            android:layout_height="wrap_content" android:layout_marginBottom="8dp"
                            android:layout_marginLeft="8dp" android:layout_marginStart="8dp" android:ellipsize="end"
                            android:gravity="center" android:maxLines="1" android:text="title"
                            android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
                            android:textColor="@android:color/white" app:layout_constraintBottom_toBottomOf="parent"
                            app:layout_constraintStart_toStartOf="parent"/>
    
                        <ImageView
                            android:id="@+id/arrowImageView" android:layout_width="wrap_content" android:layout_height="0dp"
                            android:layout_marginLeft="8dp" android:layout_marginStart="8dp"
                            app:layout_constraintBottom_toBottomOf="@+id/titleTextView"
                            app:layout_constraintStart_toEndOf="@+id/titleTextView"
                            app:layout_constraintTop_toTopOf="@+id/titleTextView"
                            app:srcCompat="@android:drawable/arrow_down_float"
                            tools:ignore="ContentDescription,RtlHardcoded"/>
    
                    </android.support.constraint.ConstraintLayout>
                </android.support.v7.widget.Toolbar>
    
            </android.support.design.widget.CollapsingToolbarLayout>
    
        </android.support.design.widget.AppBarLayout>
    
        <com.example.user.expandingtopviewtest.MyRecyclerView
            android:id="@+id/nestedView" android:layout_width="match_parent" android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".ScrollingActivity"/>
    
    </android.support.design.widget.CoordinatorLayout>
    

    The questions

    1. How can I make the scrolling being blocked when the top view is expanded, yet allow to collapse while scrolling ?

    2. How can I make the top view be replaced with a smaller one when collapsed (and back to large one when expanded), instead of completely disappear ?


    Update

    Even though I've got the basic of what I asked about, there are still 2 issues with the current code (available on Github, here) :

    1. The small view (the one you see on collapsed state) has inner views that need to have a clicking effect on them. When using the android:background="?attr/selectableItemBackgroundBorderless" on them, and clicking on this area while being expanded, the clicking is done on the small view. I've handled it by putting the small view on a different toolbar, but then the clicking effect doesn't get shown at all. I've written about this here, including sample project.

    Here's the fix:

    <android.support.design.widget.CoordinatorLayout
        android:id="@+id/coordinatorLayout" xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity">
    
        <android.support.design.widget.AppBarLayout
            android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content"
            android:fitsSystemWindows="true" android:stateListAnimator="@null" android:theme="@style/AppTheme.AppBarOverlay"
            app:expanded="false" app:layout_behavior="com.example.expandedtopviewtestupdate.ScrollingCalendarBehavior"
            tools:targetApi="lollipop">
    
            <android.support.design.widget.CollapsingToolbarLayout
                android:id="@+id/collapsingToolbarLayout" android:layout_width="match_parent"
                android:layout_height="match_parent" android:clipChildren="false" android:clipToPadding="false"
                android:fitsSystemWindows="true" app:contentScrim="?attr/colorPrimary"
                app:layout_scrollFlags="scroll|exitUntilCollapsed|snap|enterAlways"
                app:statusBarScrim="?attr/colorPrimaryDark">
    
                <!--large view -->
                <LinearLayout
                    android:id="@+id/largeView" android:layout_width="match_parent" android:layout_height="280dp"
                    android:layout_marginTop="?attr/actionBarSize" android:orientation="vertical"
                    app:layout_collapseMode="parallax" app:layout_collapseParallaxMultiplier="1.0">
    
                    <TextView
                        android:id="@+id/largeTextView" android:layout_width="match_parent"
                        android:layout_height="match_parent" android:layout_gravity="center"
                        android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true"
                        android:focusable="true" android:focusableInTouchMode="false" android:gravity="center"
                        android:text="largeView" android:textSize="14dp" tools:background="?attr/colorPrimary"
                        tools:layout_gravity="top|center_horizontal" tools:layout_height="40dp" tools:layout_width="40dp"
                        tools:text="1"/>
    
                </LinearLayout>
    
                <!--top toolbar-->
                <android.support.v7.widget.Toolbar
                    android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content"
                    android:layout_marginBottom="@dimen/small_view_height" app:contentInsetStart="0dp"
                    app:layout_collapseMode="pin" app:popupTheme="@style/AppTheme.PopupOverlay">
    
                    <android.support.constraint.ConstraintLayout
                        android:layout_width="match_parent" android:layout_height="wrap_content" android:clickable="true"
                        android:focusable="true">
    
                        <LinearLayout
                            android:id="@+id/expandCollapseButton" android:layout_width="match_parent"
                            android:layout_height="?attr/actionBarSize"
                            android:background="?android:selectableItemBackground" android:gravity="center_vertical"
                            android:orientation="horizontal" app:layout_constraintStart_toStartOf="parent"
                            app:layout_constraintTop_toTopOf="parent">
    
                            <TextView
                                android:id="@+id/titleTextView" android:layout_width="wrap_content"
                                android:layout_height="wrap_content" android:ellipsize="end" android:gravity="center"
                                android:maxLines="1" android:text="title"
                                android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
                                android:textColor="@android:color/white"/>
    
                            <ImageView
                                android:id="@+id/arrowImageView" android:layout_width="wrap_content"
                                android:layout_height="wrap_content" android:layout_marginLeft="8dp"
                                android:layout_marginStart="8dp" app:srcCompat="@android:drawable/arrow_up_float"
                                tools:ignore="ContentDescription,RtlHardcoded"/>
                        </LinearLayout>
    
                    </android.support.constraint.ConstraintLayout>
    
                </android.support.v7.widget.Toolbar>
    
                <android.support.v7.widget.Toolbar
                    android:id="@+id/smallLayoutContainer" android:layout_width="match_parent"
                    android:layout_height="wrap_content" android:layout_marginTop="?attr/actionBarSize"
                    android:clipChildren="false" android:clipToPadding="false" app:contentInsetStart="0dp"
                    app:layout_collapseMode="pin">
                    <!--small view-->
                    <LinearLayout
                        android:id="@+id/smallLayout" android:layout_width="match_parent"
                        android:layout_height="@dimen/small_view_height" android:clipChildren="false"
                        android:clipToPadding="false" android:orientation="horizontal" tools:background="#ff330000"
                        tools:layout_height="@dimen/small_view_height">
    
                        <TextView
                            android:id="@+id/smallTextView" android:layout_width="match_parent"
                            android:layout_height="match_parent" android:layout_gravity="center"
                            android:background="?attr/selectableItemBackgroundBorderless" android:clickable="true"
                            android:focusable="true" android:focusableInTouchMode="false" android:gravity="center"
                            android:text="smallView" android:textSize="14dp" tools:background="?attr/colorPrimary"
                            tools:layout_gravity="top|center_horizontal" tools:layout_height="40dp"
                            tools:layout_width="40dp" tools:text="1"/>
    
                    </LinearLayout>
                </android.support.v7.widget.Toolbar>
            </android.support.design.widget.CollapsingToolbarLayout>
    
        </android.support.design.widget.AppBarLayout>
    
        <com.example.expandedtopviewtestupdate.MyRecyclerView
            android:id="@+id/nestedView" android:layout_width="match_parent" android:layout_height="match_parent"
            app:layout_behavior="@string/appbar_scrolling_view_behavior" tools:context=".ScrollingActivity"/>
    
    </android.support.design.widget.CoordinatorLayout>
    
    1. The Google Calendar allows to perform a scroll-down gesture on the toolbar itself, to trigger showing the month view. I've succeeded only adding a clicking event there, but not scrolling. Here's how it looks like:

    enter image description here

    • Nicolas
      Nicolas over 6 years
      For the first question, you could extend the LinearLayoutManager to disable scrolling at will, as done here. Then you could extend the ReyclerView to detect a swipe up gesture and collapse the view. Just an idea, I have not tried it.
    • android developer
      android developer over 6 years
      @NicolasMaltais The function "canScrollVertically" is a complete block, similar to what I wrote before. I want a block only in one way, just like on Google Calendar app: Scrolling will be allowed to collapse the top area, but not in the opposite direction.
    • Nicolas
      Nicolas over 6 years
      So you only want to block the recyclerview from scrolling down. You can try setting up a scroll listener and when dy is positive, scroll back with the same value with RecyclerView#scrollBy(0, -dy) Again I have not tried it.
    • android developer
      android developer over 6 years
      @NicolasMaltais I suspect it would have bugs and lags because of 2 conflicting behaviors.
  • android developer
    android developer over 6 years
    For some reason, I don't see the shadow below the top area (below the small/large view) . Any idea how to fix this? I've added elevation to AppBarLayout , but this works only from Lollipop...
  • android developer
    android developer over 6 years
    Another issue I've found: since the small view is inside the toolbar, it is restricted in its width constraints to the one of the toolbar. This means that if the toolbar has a toggle of the navigation drawer, the small view will move aside. How can I fix this?
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Why does the small view move aside if there is a toggle? Does the whole toolbar shift? I can't picture what is happening.
  • android developer
    android developer over 6 years
    That's because it's a child of the toolbar. All its content moves to the right a bit, to make room for the "hamburger" button. Anyway, I've found a workaround: have another toolbar, that has just the small view in it (doesn't have anything else), and is below the normal toolbar.
  • android developer
    android developer over 6 years
    It seems the solution has another issue: clicking effect+events of the small view are on top of the large view, even when expanded . I've written about it here: github.com/Cheticamp/ExpandedTopViewTestUpdate/issues/1 . Trying to fix it, I got a different issue. Please read it and let me know there if you can see how to fix it.
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Set android:background="?android:selectableItemBackground" in smallLayoutContainer. You will see your click effect.
  • android developer
    android developer over 6 years
    Please write in the thread I created. Also, I don't want it on the container. on the real app, there are multiple views within the container.
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Updated answer.
  • android developer
    android developer over 6 years
    The scrolling trigger works great, but the clicking issue I've reported still exists: If you replace the ImageView you've added with a clickable view (even a TextView like the smallTextView one), and in expanded mode, you click the same area of the small one, the small one handles the clicking : clicking there actually clicks the small one, even though it's behind it. It's the same issue I've reported here: github.com/Cheticamp/ExpandedTopViewTestUpdate/issues/1 . See sample project and video: ufile.io/qwdgc
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper I see what you mean. I updated the code to change the visibility of the small view to track its alpha so it won't be clicked when its alpha == 0. MainActivity has been modified to do this. If you replace the ImageVIew with a TextView, you will see how this works.
  • android developer
    android developer over 6 years
    Please don't use an ImageView with a static image, because it doesn't show how well it works ( with clicking effects). Use TextView as I've shown, or an alternative. Anyway, after updating to your code, I still see issues of clicking. Not always it prints to the log (meaning callback doesn't always get called), whether I press the large view or the small view. I've tested on both a real device (Pixel 2) and emulator, both with Android 8.1
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper There are issues with your layout. I am using the one posted at github.com/Cheticamp/ExpandedTopViewTestUpdate/issues/1. First, remove the following from CollapsingToolbarLayout: app:contentScrim="?attr/colorPrimary". Then remove the second toolbar and promote smallLayout in its place. Add the following to smallLayout: app:layout_collapseMode="pin" and android:layout_marginTop="?attr/actionBarSize". That should do it.
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Updated the answer as well. This was a tough one to troubleshoot.
  • android developer
    android developer over 6 years
    I used the same layout you wrote... The one on Github was a workaround to handle the issue I've presented. You haven't updated all of your code. The ids in the MainActivity do no use the correct ones of the layout file. I've edited your answer to have them. Anyway, I still see the same issue that the onClickListener sometimes doesn't get called. Sometimes it seems as if it has a delay of handling it. See project&video here: ufile.io/y3xjn . This behavior occurs on both emulator and real device (Pixel 2 with Android 8.1)
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Are you verifying with just logcat? I see similar behavior with significant delays in the log file. I am on AS 3.0.1. I thought it was just my setup but maybe not. I had to resort to setting breakpoints on the Log.d() statements to verify that everything is working; otherwise, it appeared that it was only working intermittently as you state.
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Take your project at ufile.io/y3xjn and remove app:contentScrim="?attr/colorPrimary" from the CollapsingToolbarLayout to see the click effect. See this video (ufile.io/qesqz) of what I am seeing after doing this. The effects and all clicks seem to work OK. What am I missing?
  • android developer
    android developer over 6 years
    I used the same exact layout you've written. There were no "contentScrim" in it. Anyway, tested again, and I don't see this issue. How odd. Can you please update the Github project with latest code ? Also, can you please explain what exactly caused the issues , and how you've fixed them?
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper You mean ExpandedTopViewTestUpdate. I can update that. As for the changes, I think that the tool bar was interfering with the behavior of the text views. Other than that, I can't say specifically what was going on.
  • Cheticamp
    Cheticamp over 6 years
    @androiddeveloper Updated answer with current code and updated GitHub project.
  • android developer
    android developer over 6 years
    Thank you very much. The code works great now. Can you please check the other issue I've written here: stackoverflow.com/questions/47514072/… ?