Android 应用大小调整

1. 简介

Android 设备生态系统始终在不断发展变化。从内置硬件键盘的早期时代,到可翻转设备、可折叠设备、平板电脑和可调整大小的自由窗口的现代阶段,Android 应用从未像现在这样在更加多样化的设备上运行。

虽然这对开发者来说是个好消息,但应用需要进行某些优化,才能满足用户的使用预期,并在不同尺寸的屏幕上提供卓越的用户体验。响应式/自适应界面和弹性架构有助于让您的应用在现有和未来用户的任何设备上(无论尺寸和形状如何)都能提供出色的外观和运行效果,而不是逐一面向每部新设备分发应用!

引入可调整大小的自由式 Android 环境是对响应式/自适应界面进行压力测试的绝佳方式,从而使应用能适合任何设备。此 Codelab 将引导您了解调整大小的影响,以及落实一些最佳做法,使应用可以轻松可靠地调整大小。

构建内容

您将探索自由调整窗口大小的影响,并优化 Android 应用,以便落实调整大小的最佳实践。您的应用将:

拥有兼容的清单

  • 取消导致应用无法自由调整大小的限制

调整大小时保持状态不变

  • 使用 rememberSaveable 在调整大小时维持界面状态不变
  • 避免为初始化界面而重复不必要的后台工作

所需条件

  1. 了解开发基本 Android 应用的相关知识
  2. 了解 Compose 中的 ViewModel 和状态
  3. 支持自由调整窗口大小的测试设备,例如以下任一类型:

如果在此 Codelab 的操作过程中遇到任何问题(代码错误、语法错误、措辞含义不明等),欢迎通过 Codelab 左下角的“报告错误”链接向我们报告相应问题。

2. 使用入门

GitHub 克隆代码库。

git clone https://github.com/android/large-screen-codelabs/

…或者下载代码库的 ZIP 文件并解压缩

导入项目

  • 打开 Android Studio
  • 选择 Import ProjectFile->New->Import Project
  • 转到您克隆或解压缩项目的位置
  • 打开 resizing 文件夹。
  • 打开 start 文件夹中的项目。此文件包含起始代码。

尝试应用

  • 构建并运行应用
  • 尝试调整应用大小

您有何看法?

根据测试设备的兼容性支持,您可能会发现用户体验并不理想。应用无法调整大小,并且固定在初始宽高比上。发生了什么?

清单限制

如果查看应用的 AndroidManifest.xml 文件,您可以看到添加了一些限制,阻止应用在自由调整窗口大小的环境中正常运行。

AndroidManifest.xml

            android:maxAspectRatio="1.4"
            android:resizeableActivity="false"
            android:screenOrientation="portrait">

请尝试从清单中移除这三行有问题的代码,重新构建应用,然后在测试设备上重试。您会发现,应用不再无法自由调整大小。如需优化应用以实现自由窗口大小调整,从清单中取消此类限制非常重要。

3. 调整大小的配置变更

调整应用窗口的大小时,应用的配置会更新。这些更新会对应用产生影响。了解并预测这些影响有助于为用户提供良好的体验。最明显的更改是应用窗口的宽度和高度,但这些更改也会对宽高比和屏幕方向产生影响。

观察配置变更

如需在使用 Android View 系统构建的应用中自行查看这些变化,您可以替换 View.onConfigurationChanged。在 Jetpack Compose 中,我们可以访问 LocalConfiguration.current,它会在调用 View.onConfigurationChanged 时自动更新。

如需在示例应用中查看这些配置更改,请向您的应用添加一个用于显示 LocalConfiguration.current 值的可组合项,或者使用此类可组合项创建新的示例项目。显示这些更改的示例界面如下所示:

val configuration = LocalConfiguration.current
val isPortrait = configuration.orientation ==
    Configuration.ORIENTATION_PORTRAIT
val screenLayoutSize =
        when (configuration.screenLayout and
                Configuration.SCREENLAYOUT_SIZE_MASK) {
            SCREENLAYOUT_SIZE_SMALL -> "SCREENLAYOUT_SIZE_SMALL"
            SCREENLAYOUT_SIZE_NORMAL -> "SCREENLAYOUT_SIZE_NORMAL"
            SCREENLAYOUT_SIZE_LARGE -> "SCREENLAYOUT_SIZE_LARGE"
            SCREENLAYOUT_SIZE_XLARGE -> "SCREENLAYOUT_SIZE_XLARGE"
            else -> "undefined value"
        }
Column(
    horizontalAlignment = Alignment.CenterHorizontally,
    modifier = Modifier.fillMaxWidth()
) {
    Text("screenWidthDp: ${configuration.screenWidthDp}")
    Text("screenHeightDp: ${configuration.screenHeightDp}")
    Text("smallestScreenWidthDp: ${configuration.smallestScreenWidthDp}")
    Text("orientation: ${if (isPortrait) "portrait" else "landscape"}")
    Text("screenLayout SIZE: $screenLayoutSize")
}

您可以在 observing-configuration-changes 项目文件夹中查看实现示例。请尝试将此实现代码添加到应用的界面中,在测试设备上运行,然后观察界面随着应用的配置更改而更新。

在应用调整大小时,更改的配置信息会实时显示在应用界面中

通过对应用配置进行这些更改,您可以快速模拟以下极端情况:从小型手机分屏模式到平板电脑或桌面设备上的全屏模式。这不仅是针对各种屏幕测试应用布局的好方法,还可测试应用处理快速配置更改事件的能力。

4. 记录 activity 生命周期事件

自由调整应用窗口大小的另一个影响是,应用将发生各种 Activity 生命周期变化。如需实时查看这些变化,请向 onCreate 方法添加生命周期观察器,并通过替换 onStateChanged 来记录每个新的生命周期事件。

lifecycle.addObserver(object : LifecycleEventObserver {
        override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
        Log.d("resizing-codelab-lifecycle", "$event was called")
    }
})

设置此日志记录后,再次在测试设备上运行应用,并在尝试最小化应用并将其再次置于前台时查看 logcat

观察发现,当应用在最小化时处于暂停状态,然后在其进入前台时再次恢复。这对应用有影响,您将在本 Codelab 的下一部分(重点介绍连续性)中探讨这一点。

logcat 显示在调整大小时调用了 activity 生命周期方法

现在,请查看 Logcat,了解当您将应用从可能的最小大小调整到可能的最大大小时调用了哪些 activity 生命周期回调

根据测试设备,您可能会观察到不同的行为,但您可能会注意到,当应用窗口大小发生显著变化(而非略微变化)时,activity 会被销毁并重新创建。这是因为,在 API 24 及更高级别上,只有发生显著的大小变更才会重新创建 Activity

您已经了解了在自由窗口大小调整环境中会出现的一些常见配置变更,但其他变更也需要注意。例如,如果您将外部显示器连接到测试设备,就会发现 Activity 被销毁并重新创建,以反映配置更改(例如显示密度)。

如要抽象化与配置更改相关的部分复杂性,请使用更高级别的 API(例如 WindowSizeClass)来实现自适应界面。另请参阅支持不同的屏幕尺寸

5. 连续性 - 调整大小后保持可组合项的内部状态

在上一部分中,您已经了解了应用在自由窗口大小调整环境中可能会发生的一些配置变更。在本部分中,您将使应用的界面状态在所有这些更改中保持连续。

首先,使 NavigationDrawerHeader 可组合函数(可在 ReplyHomeScreen.kt 中找到)展开,在用户点击时显示电子邮件地址。

@Composable
private fun NavigationDrawerHeader(
    modifier: Modifier = Modifier
) {
    var showDetails by remember { mutableStateOf(false) }
    Column(
        modifier = modifier.clickable {
                showDetails = !showDetails
            }
    ) {


        Row(
            horizontalArrangement = Arrangement.SpaceBetween,
            verticalAlignment = Alignment.CenterVertically
        ) {
            ReplyLogo(
                modifier = Modifier
                    .size(dimensionResource(R.dimen.reply_logo_size))
            )
            ReplyProfileImage(
                drawableResource = LocalAccountsDataProvider
                    .userAccount.avatar,
                description = stringResource(id = R.string.profile),
                modifier = Modifier
                    .size(dimensionResource(R.dimen.profile_image_size))
            )
        }
        AnimatedVisibility (showDetails) {
            Text(
                text = stringResource(id = LocalAccountsDataProvider
                        .userAccount.email),
                style = MaterialTheme.typography.labelMedium,
                modifier = Modifier
                    .padding(
                        start = dimensionResource(
                            R.dimen.drawer_padding_header),
                        end = dimensionResource(
                            R.dimen.drawer_padding_header),
                        bottom = dimensionResource(
                            R.dimen.drawer_padding_header)
                ),


            )
        }
    }
}

将展开式标题添加到应用后,

  1. 在测试设备上运行应用
  2. 点按标题将其展开
  3. 尝试调整窗口大小

您会发现,大幅调整大小后,标题会丢失状态。

点按应用抽屉式导航栏上的标题后它会展开,但在调整应用大小后它会收起

界面状态会丢失,因为 remember 可帮助您在重组后保留状态,而不是在重新创建 activity 或进程后保留状态。常见的做法是使用状态提升,即将状态移至可组合项的调用方,使可组合项变为无状态,这样可以完全避免此问题。尽管如此,在将界面元素状态保持在可组合函数内部时,您也可以在某些位置使用 remember

如需解决这些问题,请将 remember 替换为 rememberSaveable。这之所以有效,是因为 rememberSaveable 会保存记住的值并恢复为 savedInstanceState。将 remember 更改为 rememberSaveable,在测试设备上运行应用,然后再次尝试调整应用的大小。您会发现,展开式标题的状态会在整个调整过程中按预期保留。

6. 避免不必要的重复后台工作

您已经了解了如何使用 rememberSaveable 在配置更改时保留可组合项的内部界面状态,而配置更改可能会因自由窗口大小调整而频繁发生。不过,应用通常应从可组合项中提升界面状态和逻辑将状态的所有权转移给 ViewModel 是在调整大小期间保留状态的最佳方式之一。将状态提升到 ViewModel 时,您可能会遇到长时间运行的后台工作方面的问题,例如大量的文件系统访问或初始化屏幕所需的网络调用。

如需查看您可能遇到的问题类型的示例,请将日志语句添加到 ReplyViewModel 中的 initializeUIState 方法。

fun initializeUIState() {
    Log.d("resizing-codelab", "initializeUIState() called in the viewmodel")
    val mailboxes: Map<MailboxType, List<Email>> =
        LocalEmailsDataProvider.allEmails.groupBy { it.mailbox }
    _uiState.value =
        ReplyUiState(
            mailboxes = mailboxes,
            currentSelectedEmail = mailboxes[MailboxType.Inbox]?.get(0)
                ?: LocalEmailsDataProvider.defaultEmail
        )
}

现在,在测试设备上运行应用,并尝试多次调整应用窗口的大小。

查看 Logcat 时,您会发现应用显示初始化方法已运行多次。如果您只想运行一次以初始化界面的工作,这可能会带来问题。额外的网络调用、文件 I/O 或其他工作可能会影响设备的性能,并导致其他意外问题。

为了避免不必要的后台工作,请从 activity 的 onCreate() 方法中移除对 initializeUIState() 的调用。请改为在 ViewModelinit 方法中初始化数据。这样可确保初始化方法仅在首次实例化 ReplyViewModel 时运行一次:

init {
    initializeUIState()
}

尝试再次运行应用,您会看到不必要的模拟初始化任务只会运行一次,无论您调整了多少次应用窗口的大小。这是因为 ViewModel 会在 Activity 的生命周期过后持续存在。通过在创建 ViewModel 时仅运行一次初始化代码,我们可以将其与任何 Activity 重新创建区分开来,从而避免不必要的工作。如果这实际上是一项成本高昂的服务器调用或大量文件 I/O 操作来初始化界面,就能节省大量资源并改善用户体验。

7. 恭喜!

大功告成!太棒了!现在,您已落实了一些最佳实践,让 Android 应用在 ChromeOS 和其他多窗口、多屏幕环境中能够很好地调整大小。

示例源代码

从 GitHub 克隆代码库

git clone https://github.com/android/large-screen-codelabs/

…或者下载代码库的 ZIP 文件并解压缩