Android CollapsingToolbar/ AppBarLayout 在状态栏行为顶部填充后面滚动
Android CollapsingToolbar/ AppBarLayout scroll behind status bar behaviour top padding
我想使用 CoordinatoryLayout
、AppBarLayout
和 CollapsingToolbarLayout
来创建类似于以下来自 Google 日历示例的布局。
我要复制的关键内容:
- 滚动状态栏后面的内容
- 滚动容器顶部的圆角
- 屏幕顶部有足够的空间 header 不会被压扁
问题
Google 当用户滚动时,日历似乎会增大滚动容器。我将如何着手做这个或类似的事情来实现我所追求的外观?
我整理了一个我正在尝试构建的示例:
activity_scrolling.xml
<androidx.coordinatorlayout.widget.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"
android:animateLayoutChanges="true"
tools:context="uk.co.exampleapplication.ScrollingActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include
android:id="@+id/lay_header"
layout="@layout/layout_header" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/scroll_header_background"
android:elevation="16dp"
android:paddingBottom="12dp">
<TextView
android:id="@+id/sectionTitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/filter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:layout_marginEnd="16dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".ScrollingActivity"
tools:showIn="@layout/activity_scrolling">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:text="@string/large_text" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
layout_header.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="60dp"
android:paddingBottom="40dp"
app:layout_collapseMode="parallax"
tools:ignore="HardcodedText">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="Title"
android:textColor="#FFF"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Subtitle"
android:textColor="#FFF"
android:textSize="16sp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
scroll_header_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
<solid android:color="#FFFFFF" />
<size
android:width="64dp"
android:height="64dp" />
</shape>
我的尝试包含在下面。 header 根据需要在工具栏后面滚动,但我希望在我的视图上方有一些额外的顶部填充(大约顶部插图/状态栏的高度就足够了)。 Google 日历似乎通过让容器随着用户滚动而增长来解决这个问题。
实施一个 AppBarLayout.OnOffsetChangedListener 来调整 ConstraintLayout 上的顶部填充,它包含 TextView 和 按钮。 (将此视图称为 "viewToGrow"。)您还可以在侦听器中执行其他操作,例如在应用栏滚动时更改可绘制对象的角半径。
以下示例调整顶部内边距,为页眉留出更多空间。此填充随着应用栏向上滚动而增加,并随着向下滚动而减少。演示应用程序还在应用栏滚动的最后 15% 期间删除了可绘制对象的角半径。
滚动活动
class ScrollingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)
val viewToGrow: View = findViewById(R.id.viewToGrow)
val baseTopPadding = viewToGrow.paddingTop
// Determine how much top padding has to grow while the app bar scrolls.
var maxDeltaPadding = 0
val contentView = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(contentView) { _, insets ->
maxDeltaPadding = insets.systemWindowInsetTop
insets
}
// Get key metrics for corner radius shrikage.
var backgroundRadii: FloatArray? = null
var maxRadius: FloatArray? = null
val backgroundDrawable = (viewToGrow.background as GradientDrawable?)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && backgroundDrawable != null) {
backgroundRadii = backgroundDrawable.cornerRadii
maxRadius = floatArrayOf(backgroundRadii!![0], backgroundRadii[1])
}
// Set up the app bar and the offset change listener.
val appBar: AppBarLayout = findViewById(R.id.app_bar_layout)
val appBarTotalScrollRange: Float by lazy {
appBar.totalScrollRange.toFloat()
}
appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
// Add/remove padding gradually as the appbar scrolls.
val percentOfScrollRange = (-verticalOffset / appBarTotalScrollRange)
val deltaPadding = maxDeltaPadding * percentOfScrollRange
val newTopPadding = baseTopPadding + deltaPadding.toInt()
if (newTopPadding != viewToGrow.paddingTop) {
viewToGrow.setPadding(
viewToGrow.paddingLeft,
newTopPadding,
viewToGrow.paddingRight,
viewToGrow.paddingBottom
)
// Change the drawable radius as the appbar scrolls.
if (backgroundRadii != null && maxRadius != null) {
val radiusShrinkage = if (percentOfScrollRange > (1.0f - CORNER_SHRINK_RANGE)) {
(1.0f - percentOfScrollRange) / CORNER_SHRINK_RANGE
} else {
1.0f
}
backgroundRadii[0] = maxRadius[0] * radiusShrinkage
backgroundRadii[1] = maxRadius[1] * radiusShrinkage
backgroundRadii[2] = maxRadius[0] * radiusShrinkage
backgroundRadii[3] = maxRadius[1] * radiusShrinkage
backgroundDrawable!!.cornerRadii = backgroundRadii
}
}
})
}
companion object {
const val CORNER_SHRINK_RANGE = 0.15f
}
}
我想使用 CoordinatoryLayout
、AppBarLayout
和 CollapsingToolbarLayout
来创建类似于以下来自 Google 日历示例的布局。
我要复制的关键内容:
- 滚动状态栏后面的内容
- 滚动容器顶部的圆角
- 屏幕顶部有足够的空间 header 不会被压扁
问题 Google 当用户滚动时,日历似乎会增大滚动容器。我将如何着手做这个或类似的事情来实现我所追求的外观?
我整理了一个我正在尝试构建的示例:
activity_scrolling.xml
<androidx.coordinatorlayout.widget.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"
android:animateLayoutChanges="true"
tools:context="uk.co.exampleapplication.ScrollingActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsing_toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<include
android:id="@+id/lay_header"
layout="@layout/layout_header" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/scroll_header_background"
android:elevation="16dp"
android:paddingBottom="12dp">
<TextView
android:id="@+id/sectionTitleText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="Title"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/filter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="22dp"
android:layout_marginEnd="16dp"
android:text="Button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".ScrollingActivity"
tools:showIn="@layout/activity_scrolling">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/text_margin"
android:text="@string/large_text" />
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
layout_header.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:id="@+id/header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="16dp"
android:paddingTop="60dp"
android:paddingBottom="40dp"
app:layout_collapseMode="parallax"
tools:ignore="HardcodedText">
<TextView
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="Title"
android:textColor="#FFF"
android:textSize="20sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Subtitle"
android:textColor="#FFF"
android:textSize="16sp"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/title" />
</androidx.constraintlayout.widget.ConstraintLayout>
scroll_header_background.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners
android:bottomLeftRadius="0dp"
android:bottomRightRadius="0dp"
android:topLeftRadius="20dp"
android:topRightRadius="20dp" />
<solid android:color="#FFFFFF" />
<size
android:width="64dp"
android:height="64dp" />
</shape>
我的尝试包含在下面。 header 根据需要在工具栏后面滚动,但我希望在我的视图上方有一些额外的顶部填充(大约顶部插图/状态栏的高度就足够了)。 Google 日历似乎通过让容器随着用户滚动而增长来解决这个问题。
实施一个 AppBarLayout.OnOffsetChangedListener 来调整 ConstraintLayout 上的顶部填充,它包含 TextView 和 按钮。 (将此视图称为 "viewToGrow"。)您还可以在侦听器中执行其他操作,例如在应用栏滚动时更改可绘制对象的角半径。
以下示例调整顶部内边距,为页眉留出更多空间。此填充随着应用栏向上滚动而增加,并随着向下滚动而减少。演示应用程序还在应用栏滚动的最后 15% 期间删除了可绘制对象的角半径。
滚动活动
class ScrollingActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_scrolling)
val viewToGrow: View = findViewById(R.id.viewToGrow)
val baseTopPadding = viewToGrow.paddingTop
// Determine how much top padding has to grow while the app bar scrolls.
var maxDeltaPadding = 0
val contentView = findViewById<View>(android.R.id.content)
ViewCompat.setOnApplyWindowInsetsListener(contentView) { _, insets ->
maxDeltaPadding = insets.systemWindowInsetTop
insets
}
// Get key metrics for corner radius shrikage.
var backgroundRadii: FloatArray? = null
var maxRadius: FloatArray? = null
val backgroundDrawable = (viewToGrow.background as GradientDrawable?)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && backgroundDrawable != null) {
backgroundRadii = backgroundDrawable.cornerRadii
maxRadius = floatArrayOf(backgroundRadii!![0], backgroundRadii[1])
}
// Set up the app bar and the offset change listener.
val appBar: AppBarLayout = findViewById(R.id.app_bar_layout)
val appBarTotalScrollRange: Float by lazy {
appBar.totalScrollRange.toFloat()
}
appBar.addOnOffsetChangedListener(AppBarLayout.OnOffsetChangedListener { _, verticalOffset ->
// Add/remove padding gradually as the appbar scrolls.
val percentOfScrollRange = (-verticalOffset / appBarTotalScrollRange)
val deltaPadding = maxDeltaPadding * percentOfScrollRange
val newTopPadding = baseTopPadding + deltaPadding.toInt()
if (newTopPadding != viewToGrow.paddingTop) {
viewToGrow.setPadding(
viewToGrow.paddingLeft,
newTopPadding,
viewToGrow.paddingRight,
viewToGrow.paddingBottom
)
// Change the drawable radius as the appbar scrolls.
if (backgroundRadii != null && maxRadius != null) {
val radiusShrinkage = if (percentOfScrollRange > (1.0f - CORNER_SHRINK_RANGE)) {
(1.0f - percentOfScrollRange) / CORNER_SHRINK_RANGE
} else {
1.0f
}
backgroundRadii[0] = maxRadius[0] * radiusShrinkage
backgroundRadii[1] = maxRadius[1] * radiusShrinkage
backgroundRadii[2] = maxRadius[0] * radiusShrinkage
backgroundRadii[3] = maxRadius[1] * radiusShrinkage
backgroundDrawable!!.cornerRadii = backgroundRadii
}
}
})
}
companion object {
const val CORNER_SHRINK_RANGE = 0.15f
}
}