这个问题之前在这里被问过,但是那个问题比较宽泛和不清晰,所以我现在更加具体地解释并给出了我尝试的完整代码。
背景
我需要模仿谷歌日历的顶部视图,并能够动画滑动和推下底部视图,但它具有额外的不同行为。我将我要实现的3个特征总结如下:
- 按工具栏将始终有效,可切换展开/折叠顶部视图,并带有更改旋转的箭头图标。这就像谷歌日历应用程序上的效果一样。
- 顶部视图将始终快照,就像谷歌日历应用程序上的效果一样。
- 当顶部视图折叠时,只有按工具栏才允许展开它。这就像谷歌日历应用程序上的效果一样。
- 当顶部视图展开时,只有在底部视图上滚动才会导致其折叠。如果尝试向另一个方向滚动,则没有任何反应,甚至对底部视图也没有影响。这就像谷歌日历应用程序上的效果一样。
- 一旦折叠,顶部视图将被较小的视图所代替。这意味着它总是会占据一些空间,在底部视图的上方。这与谷歌日历应用程序不同,因为在日历应用程序中,一旦你折叠了它,顶部视图就完全消失了。
这是谷歌日历应用程序的外观:
底部视图上的滚动还会慢慢隐藏顶部的视图:
问题
使用我之前找到的各种解决方案,我已经成功实现了所需行为的一部分:
- 将一些UI放在工具栏中,包括箭头视图,就可以在其中完成。对于手动展开/折叠,我在
AppBarLayout
视图上使用setExpanded
。对于箭头的旋转,我使用了一个监听器来确定AppBarLayout
的大小变化程度,使用addOnOffsetChangedListener
。 - 只需将
snap
值添加到CollapsingToolbarLayout
的layout_scrollFlags
属性中即可轻松完成捕捉。然而,为了使其真正有效,避免出现奇怪的问题(报告在此处),我使用了此解决方案。 - 通过在#2中使用的相同代码(此处),调用
setExpandEnabled
,可以阻止滚动时影响顶部视图。这对于顶部视图折叠时效果很好。 - 与#3类似,但不幸的是,由于它使用了
setNestedScrollingEnabled
,该方法在两个方向上都起作用,只有在顶部视图折叠时才能正常工作。当它展开时,它仍然允许底部视图向上滚动,而不是像日历应用程序那样。当展开时,我需要它只允许折叠,而不允许真正滚动。
这是好和坏的演示:
- 我完全失败了。我尝试了很多我想到的解决方案,在各种位置放置视图并使用各种标志。
简而言之,我成功地完成了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>
问题
如何使顶部视图展开时阻止滚动,但允许在滚动时折叠?
如何在折叠时用较小的视图替换顶部视图(并在展开时恢复为大视图),而不是完全消失?
更新
虽然我已经得到了我所问的基本内容,但当前代码仍存在两个问题(可在Github上找到,这里):
- 小视图(在折叠状态下看到的视图)具有需要在其上单击的内部视图。当在展开状态下单击此区域时,使用
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>
- 谷歌日历允许在工具栏上执行向下滚动手势,以触发显示月视图。我只成功添加了点击事件,但没有滚动事件。以下是它的外观:
dy
为正时,使用相同的值向后滚动RecyclerView#scrollBy(0, -dy)
。再次声明,我没有尝试过。 - Nicolas