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

前端 未结 1 1748
执念已碎
执念已碎 2021-01-31 22:58

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

相关标签:
1条回答
  • 2021-01-31 23:30

    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:

    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:

    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;
        }
    }
    
    0 讨论(0)
提交回复
热议问题