如何将顶视图折叠为较小尺寸的视图?

时间:2017-12-17 15:20:18

标签: android android-collapsingtoolbarlayout android-appbarlayout

此问题之前曾以过于宽泛和不明确的方式提出here,所以我已经提出了更具体的问题,并对我所尝试过的内容进行了全面的解释和编码。

背景

我需要模仿Google日历在顶部有一个视图的方式,它可以动画并向下推动底部的视图,但它有更多不同的行为。我总结了我在3个特征上要做的事情:

  1. 按工具栏上的按钮始终有效,可切换顶视图的展开/折叠,同时具有更改其旋转的箭头图标。这类似于Google日历应用。
  2. 顶部视图将始终快照,就像在Google日历应用上一样。
  3. 折叠顶视图时,只需在工具栏上按下即可展开它。这类似于Google日历应用
  4. 展开顶视图时,在底部视图中滚动只允许折叠。如果您尝试向另一个方向滚动,则不会发生任何事情,甚至不会出现在底部视图中。这类似于Google日历应用
  5. 折叠后,顶部视图将替换为较小的视图。这意味着它总是需要一些空间,在底部视图上方。这与Google日历应用不同,因为在日历应用中,顶视图在折叠后会完全消失。
  6. 以下是Google日历应用的外观:

    enter image description here

    滚动底部视图也会慢慢隐藏顶部的视图:

    enter image description here

    问题

    使用我过去发现的各种解决方案,我成功地只实现了所需行为的一部分:

    1. 工具栏中有一些UI是通过在其中包含一些视图来完成的,包括箭头视图。对于手动展开/折叠,我在setExpanded视图上使用AppBarLayout。对于箭头的旋转,我使用AppBarLayout来监听addOnOffsetChangedListener已调整大小的数量。

    2. 通过将snap值添加到layout_scrollFlags的{​​{1}}属性中,可以轻松完成捕捉。但是,为了使其真正有效,没有奇怪的问题(报告here),我使用了this solution

    3. 滚动时阻止影响顶视图可以使用我在#2(here)上使用的相同代码,通过在那里调用CollapsingToolbarLayout来完成。 这适用于顶视图折叠时。

    4. 与#3类似,但遗憾的是,由于它使用setExpandEnabled(两个方向),因此只有在顶视图折叠时才能正常工作。当它扩展时,它仍然允许底部视图向上滚动,而不是日历应用程序。展开后,我需要它只允许折叠,而不允许真正滚动。

    5. 这是好的和坏的示范:

      enter image description here

      1. 我完全失败了。我已经尝试了很多我想过的解决方案,用不同的标志在各个地方放置视图。
      2. 简而言之,我成功地做了1-3,但不是4-5。

        代码

        以下是当前代码(也可作为整个项目here使用):

        ScrollingActivity.kt

        setNestedScrollingEnabled

        MyRecyclerView.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
        }
        

        ScrollingCalendarBehavior.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
            }
        
        }
        

        activity_scrolling.xml

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

        问题

        1. 如何在展开顶视图时阻止滚动,但在滚动时允许折叠?

        2. 如何在折叠时将顶视图替换为较小的视图(在展开时返回大视图),而不是完全消失?

        3. 更新

          即使我已经了解了我所询问的内容,但当前代码仍有2个问题(可在Github上找到,here):

          1. 小视图(在折叠状态下看到的视图)具有需要对其进行单击效果的内部视图。在它们上使用<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> 并在展开时单击此区域时,将在小视图上单击完成。我通过将小视图放在不同的工具栏上来处理它,但是点击效果根本没有显示出来。我已经写过这个here,包括示例项目。
          2. 以下是修复:

            android:background="?attr/selectableItemBackgroundBorderless"
            1. Google日历允许在工具栏上执行向下滚动手势,以触发显示月视图。我成功只在那里添加了一个点击事件,但没有滚动。这是它的样子:
            2. enter image description here

1 个答案:

答案 0 :(得分:8)

注意:完整更新的项目可用here

  

如何在展开顶视图时阻止滚动,但在滚动时允许折叠?

问题#1:当应用栏未折叠时,RecyclerView根本无法滚动。要解决此问题,请将enterAlways添加到CollapsingToolbarLayout的滚动标记中,如下所示:

<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不会导致应用栏在关闭时打开,因为您正在抑制该功能,但会按预期工作。

问题#2:当应用栏完全展开后,不应允许RecyclerView向上滚动。这恰好是问题#1的一个独特问题。

[已更新] 要更正此问题,请在RecyclerView尝试向上滚动并且应用栏完全展开或将完全展开时修改RecyclerView使用滚动的行为在使用滚动(dy)后完全展开。 RecyclerView可以向上滚动,但它从不会看到该操作,因为它的行为SlidingPanelBehavior会消耗滚动。如果应用栏未完全展开但在当前滚动消耗后将展开,则行为会通过调用修改dy并在完全使用滚动之前调用超级来强制应用栏完全展开。 (见SlidingPanelBehavior#onNestedPreScroll())。 (在上一个答案中,appBar行为已被修改。将行为更改置于RecyclerView是更好的选择。)

问题#3:当嵌套滚动已处于所需状态时,设置RecyclerView的嵌套滚动以启用/禁用会导致问题。要避免这些问题,只有在ScrollingActivity中使用以下代码更改进行更改时才更改嵌套滚动的状态:

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

这是测试应用程序与上述更改的行为:

enter image description here

具有上述更改的已更改模块位于帖子的末尾。

  

如何在折叠时将顶视图替换为较小的视图(在展开时返回大视图),而不是完全消失?

[更新] 让较小的视图成为CollapsingToolbarLayout的直接子视图,因此它是Toolbar的兄弟。以下是此方法的演示。较小视图的collapseMode设置为pin。调整较小视图的边距以及工具栏的边距,以使较小的视图位于工具栏的正下方。由于CollapsingToolbarLayoutFrameLayout,因此视图堆栈和FrameLayout的高度只会成为最高子视图的高度。 此结构将避免插件需要调整的问题以及缺少点击效果的问题。

最后一个问题仍然存在,并且拖动应用栏应该打开它,并假设拖动较小的视图不应该打开应用栏。允许在拖动时打开appbar是通过AppBarLayout.Behavior MyAppBarBehavior完成的。由于较小的视图已合并到appBar中,因此将其向下拖动将打开appbar。为防止这种情况,名为MainActivity的新行为会附加到应用栏。此行为与<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> 中的代码结合使用可防止拖动较小的视图以打开应用栏,但可以拖动工具栏。

<强> activity_main.xml中

addOnOffsetChangedListener

最后,在View.INVISIBLE中添加以下代码,以便在应用栏展开和收缩时淡出/淡出较小的视图。一旦视图的alpha值为零(不可见),请将其可见性设置为View.VISIBLE,以便无法点击它。一旦视图的alpha增加到零以上,就可以通过将其可见性设置为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); } 使其可见并可点击。

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";
}

结果如下:

setDragCallback

以下是包含所有上述更改的新模块。

<强> MainActivity.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";
}

<强> SlidingPanelBehavior.java

/**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)
    }
}

<强> MyRecyclerView.kt

/**
 * 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;
    }
}

<强> MyAppBarBehavior.java

{{1}}