<!doctype html>手势导航和全屏体验 | Google Developers
手势导航和全屏体验
1. 简介
对于 Android 10 或更高版本,支持导航手势这种新模式。在此模式中,您的应用可使用整个 屏幕,提供更身临其境的显示体验。当用户从屏幕下边缘向上滑动时,可转到 Android 主屏幕。当用户从左边缘或右边缘向内滑动时,可转到上一屏幕。
使用这两种手势,您的应用即可充分利用屏幕底部的实际空间。但是,如果您的应用在系统手势区域使用手势或具有控件,则可能与系统级手势发生冲突。
此 Codelab 旨在说明如何使用边衬区避免手势冲突。此外,此 Codelab 还将说明如何对需要驻留在手势区的拖动手柄等控件使用 手势排除 API。
您将学习的内容
- 如何在视图上使用边衬区监听器
- 如何使用手势排除 API
- 在激活手势时,沉浸模式有何表现
此 Codelab 旨在确保您的应用可与系统手势相兼容。对于无关紧要的概念和代码块,本文不作详细介绍,仅提供相关内容以供您进行复制和粘贴。
您将构建的应用
Universal Android Music Player (UAMP) 是一款展示用的 Android 音乐播放器应用,采用 Kotlin 编写而成。您将针对手势导航功能设置 UAMP。
- 使用边衬区从手势区域移开控件
- 使用手势排除 API 停用"返回"手势,以保留与之冲突的控件
- 使用您的版本,探索沉浸模式的行为随应用手势导航发生的变化
您需要用到的工具
- 运行 Android 10 或更高版本的设备或模拟器
- Android Studio
2. 应用概览
Universal Android Music Player (UAMP) 是一款展示用的 Android 音乐播放器应用,采用 Kotlin 编写而成。此应用支持多种功能(包括后台播放、音频焦点处理、Google 助理集成),并可在多种平台上使用(如 Wear、TV 和 Auto)。
图 1:UAMP 中的流程
UAMP 会从远程服务器中加载音乐目录,用户可使用此应用浏览专辑和歌曲。用户点按歌曲后,此应用会通过连接的扬声器或头戴式耳机进行播放。此应用在设计时,不支持使用系统手势。因此,在运行 Android 10 或更高版本的设备上运行 UAMP 时,您会在开始时遇到一些问题。
3. 开始设置
如要获取此应用示例,可克隆 GitHub 中的代码库,然后切换到初学者分支:
$ git clone https://github.com/googlecodelabs/android-gestural-navigation/
或者,您也可以 zip 文件形式下载代码库,将其解压缩,并在 Android Studio 中打开。
完成以下步骤:
- 在 Android Studio 中打开并构建应用。
- 创建新的虚拟设备,然后选择 API 级别 29。或者,您也可以连接运行 29 级或更高级别 API 的实际设备。
- 运行应用。系统会在出现的列表中,将歌曲分组显示在 **Recommended(推荐)**和 **Albums(专辑)**选项下。
- 点击 Recommended(推荐),然后从歌曲列表中选择一首歌曲。
- 应用开始播放此歌曲。
启用手势导航
如果您在运行使用 API 级别 29 的新模拟器实例,默认情况下,系统将不会开启手势导航功能。如要启用手势导航功能,请选择 System settings(系统设置)> System(系统)> System Navigation(系统导航)> Gesture Navigation(手势导航)。
运行启用手势导航的应用
如果您在运行启用手势导航的应用,并开始播放歌曲,您可能会发现,播放器控件非常接近"主屏幕"和"返回"手势区域。
4. 进入全屏模式
什么是全屏?
不管是启用手势还是按钮进行导航,在 Android 10 或更高版本中运行的应用都可以为您带来全屏体验。如要提供全屏体验,您必须将应用移至透明的导航栏和状态栏后方。
移到导航栏后方
您必须先将导航栏背景设置为透明背景,然后您的应用才能在导航栏下面渲染内容。然后,必须将状态栏设置为透明。这样,您的应用才能按屏幕的全高进行显示。
**注意:**对于运行 Android 10 或更高版本的设备,强烈建议实行全屏体验。对于运行旧版 Android 的设备,全屏为可选项,但仍建议使用。
如要更改导航栏和状态栏的颜色,请执行以下步骤:
- **导航栏:**打开
res/values-29/styles.xml
,并将navigationBarColor
设置为color/transparent
。 - **状态栏:**同样,将
statusBarColor
设置为color/transparent
。
查看 res/values-29/styles.xml
的以下代码示例:
<!-- change navigation bar color -->
<item name="android:navigationBarColor">
@android:color/transparent
</item>
<!-- change status bar color -->
<item name="android:statusBarColor">
@android:color/transparent
</item>
系统界面可见度标记
您还必须设置系统界面可见度标记,才能让系统将应用置于系统栏下方。您可使用 View
类的 systemUiVisibility
API 设置各种标记。请执行以下步骤:
- 打开
MainActivity.kt
类,并查找onCreate()
方法。获取fragmentContainer
的实例。 - 将以下内容设置为
content.systemUiVisibility
:
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
查看 MainActivity.kt
的以下代码示例:
val content: FrameLayout = findViewById(R.id.fragmentContainer)
content.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_STABLE or
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or
View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
同时设置这些标记后,即可让系统以全屏模式显示您的应用,就像导航栏和状态栏不存在一样。请执行以下步骤:
- 运行应用,并导航至播放器屏幕,选择要播放的歌曲。
- 验证系统是否已将播放器控件移到导航栏下方,使其难以访问:
- 导航至"System settings"(系统设置),切换回三键导航模式,然后返回应用。
- 验证这些控件是否因应用三键导航栏而更难以使用:请注意,系统已将
SeekBar
隐藏到导航栏后方,而且 **Play/Pause(播放/暂停)**基本上已由导航栏所遮盖。 - 探索并试验一下。完成操作后,导航至"System settings"(系统设置), 切换回手势导航:
此应用现在会以全屏模式显示在您的面前,但其中存在应用控件冲突和重叠的易用性问题,而我们必须解决这些问题。
5. 边衬区
通过使用 WindowInsets
,应用可得知系统界面出现在内容顶层的什么位置,以及在屏幕的哪些区域内,系统手势会优先于应用内手势。边衬区将由 Jetpack 中的 WindowInsets
类和 WindowInsetsCompat
类表示。我们强烈建议使用 WindowInsetsCompat
,以便在所有 API 级别中都保持行为一致。
系统边衬区和强制系统边衬区
以下边衬区 API 是最常用的边衬区类型:
- **系统窗口边衬区:**您可通过这些边衬区,了解系统界面会显示在应用上方的什么位置。我们将讨论如何使用系统边衬区从系统栏移开控件。
- **系统手势边衬区:**这些边衬区可返回所有手势区域。这些区域的所有应用内滑动控件均可意外触发系统手势。
- **强制手势边衬区:**这些边衬区是系统手势边衬区的子集,不得覆盖。您可借此了解到在哪些屏幕区域内,系统手势的行为会始终优先于应用内手势。
使用边衬区移动应用控件
您现在已经了解边衬区 API 的详细信息,可以按以下步骤所述修复应用控件:
- 从
view
对象实例中获取playerLayout
实例。 - 将
OnApplyWindowInsetsListener
添加到playerView
。 - 从手势区域移开视图:找到底部的系统边衬区值,然后按该数量增加视图的边距。如要将视图的边距相应地更新为 [与应用底部边距关联的值],请添加 [与系统边衬区底部值关联的值]。
查看 NowPlayingFragment.kt
的以下代码示例:
playerView = view.findViewById(R.id.playerLayout)
playerView.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(
bottom = insets.systemWindowInsetBottom + view.paddingBottom
)
insets
}
- 运行应用,并选择歌曲。请注意,播放器控件似乎没有变化。如果在调试中添加断点并运行应用,您会看到监听器尚未调用。
- 要修复此问题,请切换至
FragmentContainerView
,以便其自动处理此问题。打开activity_main.xml
,并将FrameLayout
更改为FragmentContainerView
。
查看 activity_main.xml
的以下代码示例:
<androidx.fragment.app.FragmentContainerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragmentContainer"
tools:context="com.example.android.uamp.MainActivity"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
- 再次运行应用,并导航至播放器屏幕。系统已将底部播放器控件从底部手势区域移开。
应用控件现在可与手势导航功能一起发挥作用,但这些控件的移动距离超出预期。您必须解决此问题。
保留当前内边距和外边距
如果在不关闭此应用的情况下切换至其他应用或转到主屏幕,然后返回此应用,您会发现播放器控件每次都会上移。
这是因为该活动每次开始时,应用都会触发 requestApplyInsets()
。即使您没有执行此 调用,系统也会在视图的生命周期内随时多次分派 WindowInsets
。
首次将边衬区底部值数量添加到 activity_main.xml
中声明的应用底部边距值时,playerView
上的当前 InsetListener
会正常运行。但是,后续调用会将边衬区底部值继续添加到已更新视图的底部边距中。
要解决此问题,请执行以下步骤:
- 记录视图初始边距值。创建新的值,并存储
playerView
视图初始边距值,然后再存储监听器代码。
查看 NowPlayingFragment.kt
的以下代码示例:
val initialPadding = playerView.paddingBottom
- 使用此初始值更新视图的底部边距,这样可避免使用应用的当前底部边距值。
查看 NowPlayingFragment.kt
的以下代码示例:
playerView.setOnApplyWindowInsetsListener { view, insets ->
view.updatePadding(bottom = insets.systemWindowInsetBottom + initialPadding)
insets
}
- 再次运行应用。在应用之间导航,然后转到主屏幕。当返回应用时,播放器控件刚好在手势区域上方的位置。
重新设计应用控件
播放器拖动条太靠近底部手势区域,意味着用户在完成水平滑动手势时会意外触发主屏幕手势。如果增大边距,则可解决此问题,但也可能会将播放器移动得过高,超出预期高度。
尽管可通过使用边衬区解决手势冲突问题,但有时在设计时稍作改变,就可以完全避免手势冲突问题。如要重新设计播放器控件以避免手势冲突,请执行以下步骤:
- 打开
fragment_nowplaying.xml
。切换至"Design"(设计)视图,然后选择最底部的SeekBar
:
- 切换至"Code"(代码)视图。
- 如要将
SeekBar
移至playerLayout
顶部,请将拖动条的layout_constraintTop_toBottomOf
更改为parent
。 - 如要将
playerView
中的其他项目限定至SeekBar
的底部,请在media_button
、title
和position
中将layout_constraintTop_toTopOf
从 parent 更改为@+id/seekBar
。
查看 fragment_nowplaying.xml
的以下代码示例:
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="8dp"
android:layout_gravity="bottom"
android:background="@drawable/media_overlay_background"
android:id="@+id/playerLayout">
<ImageButton
android:id="@+id/media_button"
android:layout_width="@dimen/exo_media_button_width"
android:layout_height="@dimen/exo_media_button_height"
android:background="?attr/selectableItemBackground"
android:scaleType="centerInside"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="@+id/seekBar"
app:srcCompat="@drawable/ic_play_arrow_black_24dp"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Title"
app:layout_constraintTop_toTopOf="@+id/seekBar"
app:layout_constraintLeft_toRightOf="@id/media_button"
app:layout_constraintRight_toLeftOf="@id/position"
tools:text="Song Title" />
<TextView
android:id="@+id/subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
app:layout_constraintTop_toBottomOf="@+id/title"
app:layout_constraintLeft_toRightOf="@id/media_button"
app:layout_constraintRight_toLeftOf="@id/position"
tools:text="Artist" />
<TextView
android:id="@+id/position"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Title"
app:layout_constraintTop_toTopOf="@+id/seekBar"
app:layout_constraintRight_toRightOf="parent"
tools:text="0:00" />
<TextView
android:id="@+id/duration"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/text_margin"
android:layout_marginEnd="@dimen/text_margin"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="@style/TextAppearance.Uamp.Subtitle"
app:layout_constraintTop_toBottomOf="@id/position"
app:layout_constraintRight_toRightOf="parent"
tools:text="0:00" />
<SeekBar
android:id="@+id/seekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
- 运行应用,并与播放器和拖动条交互。
这些极小的设计改变可显著改进应用。
6. 手势排除 API
与主屏幕手势区域手势冲突的播放器控件问题已解决。"返回"手势区域也会与应用控件发生冲突。以下屏幕截图显示的是播放器拖动条当前驻留在左侧和右侧的"返回"手势区域:
SeekBar
可自动处理手势冲突问题。但您可能需要使用会触发手势冲突的其他界面组件。在这些情况下,您可以使用 Gesture Exclusion API
分部分地停用"返回"手势。
**注意:**每侧手势排除 API 的限制为 200 dp,而且仅在必要时才可使用。如果在视图中或在应用的某些部分禁用"返回"手势,则会导致系统及其他应用出现不一致问题。
使用手势排除 API
要创建手势排除区域,请使用 rect
对象列表对视图调用 setSystemGestureExclusionRects()
。这些 rect
对象会映射至已排除的矩形区域的坐标。您必须采用视图的 onLayout()
或 onDraw()
方法完成此调用。为此,请执行以下步骤:
- 创建名为
view
的新软件包。 - 要调用此 API,请创建一个名为
MySeekBar
的新类,并扩展AppCompatSeekBar
。
查看 MySeekBar.kt
的以下代码示例:
class MySeekBar @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = android.R.attr.seekBarStyle
) : androidx.appcompat.widget.AppCompatSeekBar(context, attrs, defStyle) {
}
- 创建一个名为
updateGestureExclusion()
的新方法。
查看 MySeekBar.kt
的以下代码示例:
private fun updateGestureExclusion() {
}
- 添加一项检查,以便在使用 28 级或更低级别 API 时跳过此调用。
查看 MySeekBar.kt
的以下代码示例:
private fun updateGestureExclusion() {
// Skip this call if we're not running on Android 10+
if (Build.VERSION.SDK_INT < 29) return
}
- 由于手势排除 API 限制为 200 dp,所以我们只能排除小块的拖动条。复制拖动条的边框,并将每个对象添加到可变列表中。
查看 MySeekBar.kt
的以下代码示例:
private val gestureExclusionRects = mutableListOf<Rect>()
private fun updateGestureExclusion() {
// Skip this call if we're not running on Android 10+
if (Build.VERSION.SDK_INT < 29) return
thumb?.also { t ->
gestureExclusionRects += t.copyBounds()
}
}
- 使用创建的
gestureExclusionRects
列表调用systemGestureExclusionRects()
。
查看 MySeekBar.kt
的以下代码示例:
private val gestureExclusionRects = mutableListOf<Rect>()
private fun updateGestureExclusion() {
// Skip this call if we're not running on Android 10+
if (Build.VERSION.SDK_INT < 29) return
thumb?.also { t ->
gestureExclusionRects += t.copyBounds()
}
// Finally pass our updated list of rectangles to the system
systemGestureExclusionRects = gestureExclusionRects
}
- 在
onDraw()
或onLayout()
中调用updateGestureExclusion()
方法。覆盖onDraw()
,并向updateGestureExclusion
中添加调用。
查看 MySeekBar.kt
的以下代码示例:
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
updateGestureExclusion()
}
- 必须更新
SeekBar
引用。如要开始更新,请打开fragment_nowplaying.xml
。 - 将
SeekBar
更改为com.example.android.uamp.view.MySeekBar
。
查看 fragment_nowplaying.xml
的以下代码示例:
<com.example.android.uamp.view.MySeekBar
android:id="@+id/seekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="parent" />
- 如要在
NowPlayingFragment.kt
中更新SeekBar
引用,请打开NowPlayingFragment.kt
,并将positionSeekBar
的类型更改为MySeekBar
。如要使变量类型一致,请将findViewById
调用的SeekBar
泛型更改为MySeekBar
。
查看 NowPlayingFragment.kt
的以下代码示例:
val positionSeekBar: MySeekBar = view.findViewById<MySeekBar>(
R.id.seekBar
).apply { progress = 0 }
- 运行应用,并与
SeekBar
交互。如果手势冲突问题仍然存在,则可尝试修改MySeekBar
的小块边框。注意,不要创建超过必需大小的手势排除区域,这样会限制其他潜在手势排除调用,并会导致用户行为出现不一致的问题。
7. 恭喜
恭喜!您已学会如何避免与系统手势冲突以及解决此问题!
在扩展全屏并使用边衬区从手势区域移开应用控件后,您可确保应用使用全屏模式。此外,您已学会如何在使用应用控件时禁用系统"返回"手势。
现在您已了解让应用使用系统手势所需的关键步骤!
其他材料
参考文档
如未另行说明,那么本页面中的内容已根据知识共享署名 4.0 许可获得了许可,并且代码示例已根据 Apache 2.0 许可获得了许可。有关详情,请参阅 Google 开发者网站政策。Java 是 Oracle 和/或其关联公司的注册商标。
[] [] 标签:控件,导航,测试,手势,API,应用,衬区 From: https://www.cnblogs.com/hzwesky/p/17158018.html