如何让顶部视图在收缩时变成较小的视图?

14

这个问题之前在这里被问过,但是那个问题比较宽泛和不清晰,所以我现在更加具体地解释并给出了我尝试的完整代码。

背景

我需要模仿谷歌日历的顶部视图,并能够动画滑动和推下底部视图,但它具有额外的不同行为。我将我要实现的3个特征总结如下:

  1. 按工具栏将始终有效,可切换展开/折叠顶部视图,并带有更改旋转的箭头图标。这就像谷歌日历应用程序上的效果一样。
  2. 顶部视图将始终快照,就像谷歌日历应用程序上的效果一样。
  3. 当顶部视图折叠时,只有按工具栏才允许展开它。这就像谷歌日历应用程序上的效果一样。
  4. 当顶部视图展开时,只有在底部视图上滚动才会导致其折叠。如果尝试向另一个方向滚动,则没有任何反应,甚至对底部视图也没有影响。这就像谷歌日历应用程序上的效果一样。
  5. 一旦折叠,顶部视图将被较小的视图所代替。这意味着它总是会占据一些空间,在底部视图的上方。这与谷歌日历应用程序不同,因为在日历应用程序中,一旦你折叠了它,顶部视图就完全消失了。

这是谷歌日历应用程序的外观:

enter image description here

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

enter image description here

问题

使用我之前找到的各种解决方案,我已经成功实现了所需行为的一部分:

  1. 将一些UI放在工具栏中,包括箭头视图,就可以在其中完成。对于手动展开/折叠,我在AppBarLayout视图上使用setExpanded。对于箭头的旋转,我使用了一个监听器来确定AppBarLayout的大小变化程度,使用addOnOffsetChangedListener
  2. 只需将snap值添加到CollapsingToolbarLayoutlayout_scrollFlags属性中即可轻松完成捕捉。然而,为了使其真正有效,避免出现奇怪的问题(报告在此处),我使用了此解决方案
  3. 通过在#2中使用的相同代码(此处),调用setExpandEnabled,可以阻止滚动时影响顶部视图。这对于顶部视图折叠时效果很好。
  4. 与#3类似,但不幸的是,由于它使用了setNestedScrollingEnabled,该方法在两个方向上都起作用,只有在顶部视图折叠时才能正常工作。当它展开时,它仍然允许底部视图向上滚动,而不是像日历应用程序那样。当展开时,我需要它只允许折叠,而不允许真正滚动。

这是好和坏的演示:

enter image description here

  1. 我完全失败了。我尝试了很多我想到的解决方案,在各种位置放置视图并使用各种标志。

简而言之,我成功地完成了1-3,但没有完成4-5。

代码

以下是当前的代码(也可在整个项目此处中找到):

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://dev59.com/oFcO5IYBdhLWcg3w1Eym#45338791 */
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>

问题

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

  2. 如何在折叠时用较小的视图替换顶部视图(并在展开时恢复为大视图),而不是完全消失?


更新

虽然我已经得到了我所问的基本内容,但当前代码仍存在两个问题(可在Github上找到,这里):

  1. 小视图(在折叠状态下看到的视图)具有需要在其上单击的内部视图。当在展开状态下单击此区域时,使用android:background="?attr/selectableItemBackgroundBorderless"会在小视图上执行点击操作。我通过将小视图放在不同的工具栏上来处理它,但这样做后,点击效果就完全不显示了。我在这里写了关于此问题的详细信息,包括示例项目。

以下是解决方法:

<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. 谷歌日历允许在工具栏上执行向下滚动手势,以触发显示月视图。我只成功添加了点击事件,但没有滚动事件。以下是它的外观:

enter image description here


对于第一个问题,您可以扩展LinearLayoutManager以随意禁用滚动,就像这里所做的那样。然后,您可以扩展ReyclerView以检测向上滑动手势并折叠视图。只是一个想法,我还没有尝试过。 - Nicolas
@NicolasMaltais 函数“canScrollVertically”是一个完整的块,类似于我之前写的内容。我只想要一个单向的块,就像在 Google 日历应用程序中一样:滚动将允许折叠顶部区域,但不允许相反的方向。 - android developer
所以你只想阻止RecyclerView向下滚动。您可以尝试设置滚动监听器,当dy为正时,使用相同的值向后滚动RecyclerView#scrollBy(0, -dy)。再次声明,我没有尝试过。 - Nicolas
@NicolasMaltais 我怀疑它会有错误和延迟,因为存在两种冲突的行为。 - android developer
1个回答

11
注意:完整的更新项目可以在这里找到。
问题#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 的行为,使其在尝试向上滚动且应用栏完全展开或将在消耗滚动 (dy) 后完全展开时消耗滚动。 RecyclerView 可以向上滚动,但它从未看到该操作,因为它的行为,SlidingPanelBehavior 消耗了滚动。如果应用栏没有完全展开,但将在消耗当前滚动后扩展,则该行为通过调用修改 dy 并在完全消耗滚动之前调用 super 强制应用栏完全展开。 (请参见 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。调整了较小视图以及工具栏的边距,使较小视图立即位于工具栏下方。由于 CollapsingToolbarLayout 是一个 FrameLayout,视图堆叠,FrameLayout 的高度就变成了最高子视图的高度。此结构将避免需要调整插图和缺少点击效果的问题。

还有一个问题需要解决,就是当向下拖动应用栏时,应该打开它,并假设向下拖动较小的视图不应该打开应用栏。通过AppBarLayout.BehaviorsetDragCallback实现了允许拖动时打开应用栏。由于较小的视图被合并到了应用栏中,因此将其向下拖动将打开应用栏。为了防止这种情况发生,需要将名为MyAppBarBehavior的新行为附加到应用栏上。这个行为与MainActivity中的代码一起防止拖动较小的视图以打开应用栏,但允许拖动工具栏。

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>

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

这里是结果:

enter image description here

以下是已经整合了以上所有更改的新模块。

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://dev59.com/oFcO5IYBdhLWcg3w1Eym#45338791 */
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;
    }
}

由于某种原因,我在顶部区域(小/大视图下方)下面看不到阴影。有什么办法可以解决这个问题吗?我已经将高度添加到“AppBarLayout”,但这只适用于Lollipop版本以上... - android developer
我发现另一个问题:由于小视图位于工具栏内部,它的宽度约束受到工具栏的限制。这意味着如果工具栏有导航抽屉的切换,小视图将会被移开。我该如何解决这个问题? - android developer
@androiddeveloper 如果有一个切换按钮,为什么小视图会移开?整个工具栏会移动吗?我无法想象发生了什么。 - Cheticamp
这是因为它是工具栏的子元素。所有内容都会向右移动一点,以腾出位置给“汉堡”按钮。无论如何,我找到了一个解决方法:再建一个工具栏,只放小视图(没有其他内容),并放在普通工具栏下面。 - android developer
看起来这个解决方案还有一个问题: 小视图的点击效果和事件在扩展时仍然位于大视图的顶部。我已经在这里写过了: https://github.com/Cheticamp/ExpandedTopViewTestUpdate/issues/1 。尝试修复它,我遇到了另一个问题。请阅读并告诉我,如果您知道如何修复它,请在那里留言。 - android developer
显示剩余16条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接