为任意屏幕尺寸构建 Android 界面

为任意屏幕尺寸构建 Android 界面

在过去的 12 个月内,有约 1 亿台新平板设备被激活,Chrome OS 的激活量增长了 92%,是增长速度最快的桌面平台。这意味着在平板电脑、可折叠设备和 Chrome OS 设备上,有超过 2.5 亿台大屏幕设备运行着 Android 系统,而关于可折叠设备的使用数量也在不断增长,同比增长超过 250%,因此,"大屏" 正在成为 Android 设备中一个重要且增长势头最快的细分市场。这也让设备制造厂商们意识到,针对大屏做优化是让设备在高端手机细分市场中脱颖而出的机会。

随着平板和可折叠设备的迅速发展,是时候停止将手机和平板分开去考虑了,而更应该提供面向一整个生态系统的应用,来提高其在市场中的影响力。本文我们将介绍开发者如何通过我们提供的新 API 和工具快速拥抱并进入这一细分市场。

如果您更喜欢通过视频了解此内容,请在此处查看:

△ 为任意屏幕尺寸构建 Android 界面

用户参与度

在 Android 开发者峰会举办后的几个月,Play 商店推出新的激励措施,包括会按照设备类型对应用进行评级等举措,鼓励开发者将更多目光放到大屏上去。所以目前正是迎接这些变化的绝佳时机,不仅能够迎合之后的市场变化,还能就此解决因为没有适配大屏而造成用户的使用体验欠缺的尴尬。

△ 针对大屏优化的 Microsoft Outlook 应用界面

△ 针对大屏优化的 Microsoft Outlook 应用界面

我们还观察到那些针对所有屏幕尺寸进行优化的应用,在围绕用户互动度、留存率等指标上,都取得了不错的成绩。比如其中的一个成功案例 Candy Camera,它通过优化可折叠设备和大屏幕的布局,使得使用这些设备的用户在应用上花费的时间增加了 10%,7 天用户留存率更是增长了 14%,而这并不是个例。另一个案例是 Microsoft Outlook,它最近的更新通过使用双窗口布局充分发挥了大屏优势,可以同时查看收件箱和电子邮件内容,并能够在拥有多个显示屏的某个单独窗口中独立撰写电子邮件。这些例子充分表明: 是时候开始摆脱手机这一单一界面限制的束缚,从而自由地进行设计和开发了。

但是也别太担心,我们为此已经做了很多的工作,旨在让您在整个开发周期中尽可能更轻松一些,接下来看看我们提供了哪些帮助您更好进行大屏适配的工具吧。

窗口大小类和 Reference Devices

在多元化的设备生态中,各种 Android 设备的形状各异且尺寸不一,这就使得应用的布局需要十分灵活。在不同的设备上运行同一应用,都应该能够灵活适应不同设备的屏幕尺寸。为此,我们深入研究了 Android 设备市场,并从 Web 的自适应和响应式开发的最佳实践中汲取了一些灵感,构建出可动态调整尺寸的新 Android 界面基础,我们将其称为窗口大小类。

窗口大小类是一组主观的视口断点,您可以根据它们来设计、开发和测试可调整大小的应用布局。这些断点将帮助您了解要进行优化的关键尺寸,以便将应用适配于整个生态系统。窗口大小类分为三类,分别是较小型、中等型和展开型,它们旨在平衡布局的简单和灵活性,以针对特殊情况优化您的应用。我们推荐您使用窗口大小断点来做出高级应用布局决策,对于布局网格列的变化,它们还能映射到 Material Design 布局断点。

新的 WindowSizeClass API 会在 Jetpack WindowManager 1.1 中提供,它将让您摆脱易出错的 isTable 逻辑。这些新 API 还将消除设备在横竖屏切换时需要自定义逻辑的需求,在大多数情况下只需针对不同的窗口大小类断点进行设计,应用就会适应正确的布局和各种应用状态。

class WindowMetrics {
    class WindowSizeClass(val name: String) {
        companion object {
            val COMPACT = WindowSizeClass(“COMPACT”)
            val MEDIUM = WindowSizeClass(“MEDIUM”)
            val EXPANDED = WindowSizeClass(“EXPANDED”)
        }
    }

    val widthClass: WindowSizeClass
        get() {...}
    val heightClass: WindowSizeClass
        get() {...}
}

有一点比较重要的是,从 Android 12 开始,将允许应用任意调整尺寸,且允许所有应用都以多窗口模式运行。以 Samsung Galaxy Fold 系列来看,其提供的分屏模式使得屏幕利用率提高了 7 倍,而分屏允许用户根据自己的偏好对尺寸进行调整,这也进一步突出了构建可动态调整尺寸界面的重要性。

从设备和配置的角度来对布局进行考量,我们让每个窗口大小类都代表了一些典型设备的配置 (如下图所示),当您考虑基于断点对布局进行设计时,这将会是一个很有用的参考。其中,较小型代表了竖屏模式下手机的典型模式,中等型代表了大部分平板电脑和更大的可折叠设备的尺寸,展开型则代表了平板电脑或更大的可折叠设备,或是桌面设备在横屏模式下的显示情况。

△ 基于宽度的窗口大小类的表示

△ 基于宽度的窗口大小类的表示

除了以上三种基于宽度的断点外,我们还引入了具有相同类别名称的基于高度的断点,以便适用于更高级别的布局场景,并赋予更多的灵活性。假设我们需要使用较小的高度断点来对横屏手机界面进行布局优化,虽然这听起来很复杂,但是别担心,根据我们同许多 Android 开发者进行深谈后,大部分情况下只需要根据宽度进行布局适配就可以了。

△ 基于高度的窗口大小类的表示

△ 基于高度的窗口大小类的表示

总而言之,窗口大小类的出现,代表了 Android 在自适应和响应式布局开发中的一大进步,包括更新和优化的指南、Jetpack WindowManager 中的新 API 以及 Android Studio 中的新工具。

谈到 Android Studio,我们将在 Android Studio Bumblebee 中引入一种新的工具类别,我们将其称为 Reference Devices,它的引入是为了让 Android 应用的构建能够响应和适应所有设备类别。我们在对市场数据进行充分研究之后,提供了四种 Reference Devices,分别代表了手机、可折叠设备、平板电脑和桌面设备。它们既可以覆盖目前市场上的主流设备,又涵盖到了快速增长的细分市场,还可以确保应用在大部分窗口大小类中都能够正常运行。

△ 四种 Reference Devices

△ 四种 Reference Devices

在本文对大屏幕适配的介绍中,若您只想快速知晓要注意的点,那请记住以下几点:

  1. 为了确保应用在不同设备尺寸上都能够正确展示,请优先针对较小和展开型宽度大小类来优化布局;
  2. 在所有的 Reference Devices 上都测试一遍您的应用,优先采用在中等型下的最佳布局;
  3. 为了提供更好的用户体验,请添加对应用有意义的功能,如支持可折叠设备的折叠状态或针对键盘、鼠标和触控笔输入支持进行优化;

如需了解更多有关窗口大小类的详细信息,请查阅 窗口大小类

如需查看关于窗口大小类的实际应用示例,请参阅 JetNews Compose 示例

适配大屏

设计美观且响应迅速的界面是开发应用的第一步,但如何实现和维护这种设计绝对是个挑战,为了简化您的工作,我们会致力于提供高效的工具。现在便会介绍如何通过新的 Jetpack API 和 Android Studio 功能,来对现有应用进行更新,以针对所有屏幕尺寸进行优化。

我们将会使用 Trackr 作为示例,这是一个开源的任务管理应用,我们最近对该应用进行了更新,使其更好地支持更大屏幕的设备。Trackr 的开发曾是为了展示如何在 Android 中支持无障碍功能体验的最佳实践,随着最近针对大屏幕的更新,它无疑是一个很好的示例。

△ 更改之前的 Trackr 样式

△ 更改之前的 Trackr 样式

上图是我们进行更改之前的 Trackr 样式,您会发现不管在什么设备或屏幕下,都会有一个单窗口任务列表以及用于导航到归档或设置页面的底部应用栏。Trackr 有几个主要界页,包括任务列表、任务详情、任务创建或编辑页面。接下来,就让我们对 Trackr 进行大屏优化。

NavigationRailView

我们正在 Android Studio Chipmunk 中开发一个新的工具 Visual Linting。可以通过它在 Layout Validation 中对界面进行检查,并显示一些警告和相关建议。我们使用 Visual Linting 对 Trackr 的布局进行检查,来通过工具找出一些潜在的大屏幕显示的相关问题。我们可以打开 main_activity 布局,然后打开 Layout Validation 工具 (还可以通过 View - Tools Window 路径找到该选项)。

△ Layout Validation 中对界面进行检查

△ Layout Validation 中对界面进行检查

在 Layout Validation 界面,您会发现有一个新的 Reference Devices 的类别,通过它您可以在 Android Studio 中使用新的 Reference Devices 功能。在 Layout Validation 右上角可以发现一个警告图标,单击此图标可以打开警告窗口,点击每个警告会显示哪些设备会受到影响。如上图所示,我们会发现两个跟大屏显示相关的警告: 底部应用栏只推荐用于较小屏幕以及 MaterialTextView 的部分行包含超过 120 个字符。

△ 警告窗口

△ 警告窗口

展开警告可以查看到 Android Studio 是否提供了修改建议,这里关于底部应用栏警告的修改建议就是使用 Navigation Rail、抽屉式导航栏,或使用顶部应用栏代替。对于 Trackr,我认为使用导航路由更有建设性。而针对 MaterialTextView 的修改建议是要么减少 TextView 的宽度,要么考虑使用多列布局,这里使用多列布局更适合我们的应用。对于 Trackr,我们将会使用典型的列表加详情窗口的样式来解决这些警告,针对有着中等或较大宽度的设备,我们将使用 NavRail,而非底部应用栏,对于展开型宽度的设备我们将使用双窗口布局来展示任务和相关详情。

我们先来进行第一项优化,使用 NavRail 而非底部应用栏,首先我们要考虑的是导航模型,所幸我们不会更改很多具体的视图,仅仅只会更改导航方式,因为 NavRail 会一直存在于整个视图体系中,可以通过它导航到任何其他视图。为了实现这一模式,我们可以将 Navigation Rail View 添加到 main_activity 布局中,如下代码所示:

// main_activity.xml
<androidX.coordinatorlayout.widget.CorrdinatorLayout
…>
<com.google.android.material.navigationrail.NavigationRailView
    android:id="@+id/navigation_rail"
    android :layout_width="wrap_content"
    android:layout_height="0dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app: layout_constraintTop_toTopOf="parent"
    app:headerLayout="@layout/navigation_rail_header"
    app:labelVisibilityMode="unlabeled"
    app:menu="@menu/navigation_rail" />
</androidX.coordinatorlayout.widget.CorrdinatorLayout>

在此之前,main_activity 仅由 FragmentContainerView 和 CoordinatorLayout 组成,并通过 NavHostFragment 来托管其他 Fragment。而将 NavigationRailView 放置在 main_activity 布局级别后,它将在所有视图中持久存在。尽管如此,我只想要 NavigationRail 用于宽度为 600dp 或者更大的屏幕尺寸,要实现这一点,一个简单的方法是添加资源限定 (resource-qualified) 的 main_activity 布局,并在包含 NavHostFragment 的 FragmentContainerView 的同一级别上添加 NavigationRailView:

// w600dp/tasks_fragment.xml
<layout...>
   <data.../>
   <androidx.coordinatorlayout.widget.CoordinatorLayout...>
   <com.google.android.material.appbar.AppBarLayout.../>
   <androidx.recyclerview.widget.RecyclerView
       android:id="@+id/tasks_list"
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:clipToPadding="false"
       android:paddingLeft="@dimen/pane_margin"
       android:paddingRight="@dimen/pane_margin"
       tools:ignore="SpeakableTextPresentCheck"
       tools:listitem="@layout/task_summary"/>
-    <com.google.android.material.bottomappbar.BottomAppBar.../>
-    <com.google.android.material.floatingactionbutton.FloatingActionButton.../>
   </androidx.coordinatorlayout.widget.CoordinatorLayout>
</layout>

我们还需要更新 tasks_fragments.xml,从宽度为 600dp 或更大的显示屏中移除底部应用栏。与实现 NavRail 的方式类似,可以为 tasks_fragments 添加资源限定 (resource-qualified) 的布局,然后就可以移除底部应用栏和相关的悬浮操作按钮,其他一切保持不变从而让任务列表继续按照预期工作。最后,在设置 NavRail 菜单栏的 ID 来匹配现有导航目的视图的 ID,再在 MainActivity 中为 NavRail 设置 NavController:

<!=-NavRail Menu -->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        android:id="@+id/nav_tasks"
        android:icon="@drawable/ic_task" .../>
    <item
        android:id="@+id/nav_archives"
        android:icon="@drawable/ic_archive" .../>
</menu>
// MainActivity NavController
class MainActivity: AppCompatActivity() {
...
    override func onCreate(saveInstanceState: Bundle?) {
    ...
        binding.navigationRail?.apply{
            setupWithNavController(navController)
            setOnItemReselectedListener()
            headerView?.setOnClickListener {
                navController.navigate(R.id.nav_task_edit_graph)
            }
        }
    }
}

这样就完成了,可以在 Android Studio 查看显示是否一切正常,通过在各种 Reference Devices 中来回切换查看布局是否按照我们的预期进行。当查看 Phone Reference Device 时,依然能够看到底部应用栏,而切换到更大的屏幕后,我们发现它开始使用 NavRail 了,一切按照我们的预期进行。

△ Phone Reference Device 下的效果

△ Phone Reference Device 下的效果

△ Tablet Reference Device 下的效果

△ Tablet Reference Device 下的效果

SlidingPanelLayout

接下来让我们继续基于展开型宽度设备来实现双窗口视图布局。支持这一布局方式的一个简单方法是使用 SlidingPaneLayout,它的优势在于可以轻松复用现有的布局代码,以下是目前更新后的导航图:

△ 更新后的导航图

△ 更新后的导航图

我们可以通过 NavigationRailView 导航到应用任意一个顶层布局,但仍然可以通过选择界面中某个单项任务而导航到详情页面的 Fragment。这种模式在实现 SlidingPanelLayout 时会发生一些变化,我们将添加一个新布局 TwoPaneTasks 来包含 SlidingPaneLayout,此布局将同时包含任务列表和详情的 Fragment。通过这种方式更新应用导航,无论屏幕尺寸如何都能够拥有相同的导航图,这意味着调整屏幕尺寸不会产生导航的变化,从而让用户感到困惑。

由于任务和详情都呈现在 SlidingPaneLayout 中的同一个新的 Fragment 中,因此我们为该 Fragment 的导航交互专门添加一个新的子导航层次结构。这样,当我选择一项任务并且应用从双窗口变成单窗口时,该项目将位于导航栈的顶部,并是可见的状态。

简单说,我们将使用 SlidingPaneLayout 和 FragmentContainerView 来添加一个新 Fragment 来托管任务和详情窗格,这样不必对现有代码进行大的重构。

// tasks_two_pane_fragment.xml
<layout...>
   <androidx.slidingpanelayout.widget.SlidingPaneLayout
       android:id="@+id/sliding_pane_layout"...>
       <androidx.fragment.app.FragmentContainerView
           android:id="@+id/list_pane"
           android:name="com.example.android.trackr.ui.tasks.TasksFragment"
           android:layout_width="@dimen/list_pane_width"
           android:layout_height="match_parent"
           android:layout_weight="@dimen/list_pane_weight"
           tools:layout="@layout/tasks_fragment" />
   <androidx.fragment.app.FragmentContainerView
           android:id="@+id/detail_pane"
           android:name="androidx.navigation.fragment.NavHostFragment"
           android:layout_width="@dimen/detail_pane_width"
           android:layout_height="match_parent"
           android:layout_weight="@dimen/detail_pane_weight"
           app:navGraph="@navigation/task_detail"
           tools:layout="@layout/task_detail_fragment" />
   </androidx.slidingpanelayout.widget.SlidingPaneLayout>
</layout>

然后,继续更新应用的顶层导航层次结构,使新的双窗口 Fragment 成为应用的起始目的页面,并从应用的导航图中移除详情目的页面。

<!-- 顶层导航图 -->
<navigation app: startDestination="@+id/nav_tasks"...>
   <fragment
       android:id="@+id/nav_tasks"
       android:name="..trackr.ui.tasks.TasksTwoPaneFragment" ...>
       <action
           android:id="@+id/to_task_edit"
           app: destination="@id/nav_task_edit_graph' />
   </fragment>
<!--Remove the 'details' destination-->
...
</navigation>

<!-SlidingPaneLayout 导航图-->
<navigation...
       app:startDestination="@id/nav_task_detail_placeholder">
       <action
           android:id="@+id/to_task_detail"
           app:destination="@id/nav_task_detail"
           app:popUpTo="@id/nav_task_detail"
           app :popUpToInclusive="true"
       <fragment
           android:id="@+id/nav_task_detail"
           android:name="..trackr.ui..TaskDetailFragment"...>
       <argument
           android:name="taskId"
           app:argType="long" />
   </fragment>
<!-- 为其实目的页面使用一个 placeholder-->
   <fragment
       android:id="@+id/nav_task_detail_placeholder"
       android:name="..trackr.ui.PlaceholderFragment"
       tools:layout="@layout/placeholder_fragment"/>
</navigation>

最后,再为 SlidingPaneLayout 专门添加一个新导航图,并在 TasksTwoPaneFragment Kotlin 代码中处理 SlidingPaneLayoutNavController 配置逻辑。通过这两项更改应用在不同设备不同外形下的布局会更加合理。完成这些后,我们再次通过在 Android Studio 中的 Reference Devices 工具,就能看到新的布局在所有的设备屏幕中都能够完美布局了。而为了在应用运行时进行测试,Android Studio Chipmunk 提供了可支持尺寸调整的模拟器,通过它可以在相同的 Reference Devices 之间切换,来快速验证应用布局是否正确。

另外,SlidingPaneLayout 提供了另一个重要特性是它不仅适用于大屏幕设备,而且适用于多屏幕设备。Microsoft 最近为 SlidingPaneLayout 提供了一个支持铰链检测的功能,让其自动能够支持跨屏幕拆分窗口,而无需更改任何代码。这意味着应用的新列表/详情布局将适用于所有设备,包括多屏幕设备。

虽然上述提到的方法对于优化大屏显示非常有用,但是许多开发者的应用都基于多个 Activity,对于这些应用,12L 中发布的新 Activity Embedding API 将使支持双窗口视图等新界面范式变得容易,敬请期待。

Jetpack Compose

Jetpack Compose 在 2021 年 7 月发布了 1.0 版本后,在 Android 开发者社区产生了巨大反响,成千上万的应用已经在生产环境中使用了 Compose,包括 Play 商店应用本身。Jetpack Compose 本身是一种声明式的界面工具包,通过它您可以根据页面状态进行描述,Compose 会自行进行所有必要的更新。所有的界面都是通过在代码中描述而成,这样也就很容易在运行时做出关于界面样式的决策,而在传统的视图系统中,我们通过对不同屏幕配置进行编译,从而实现对视图的配置,这两者有着巨大的不同。这也让 Compose 可以轻松解决不同屏幕尺寸而带来的界面更改。

接下来,让我们通过 JetNews 来向您展示如何通过 Compose 来进行不同屏幕尺寸的适配。JetNews 的主界面展示了一长串滚动的文章,在针对大屏幕进行优化之前,它的界面如下图所示,可以发现,并没有很好地利用额外的屏幕空间。

△ JetNews 的主界面展示

△ JetNews 的主界面展示

前文中已经介绍了 WindowManager API,目前我们正在将其集成到 Compose 中去,以便更轻松地从 Compose 中访问这些信息。在此期间,我们可以创建一个 composable 函数来处理与 WindowManager 的集成,然后轻松将当前 Activity 的窗口信息转换为最终的窗口大小类,代码如下所示:

@Composable
fun Activity.rememberWindowSizeClass(): WindowSize {
   val configuration = LocalConfiguration.current
   val windowMetrics = remember (configuration) {
       WindowMetricsCalculator.getOrCreate()
           .computeCurrentWindowMetrics(this)
   }
   val windowDpSize = with (LocalDensity.current) {
       windowMetrics.bounds.toComposeRect().size.toDpSize()
   }
   when {
       windowDpSize.width < 600.dp -> WindowSize.Compact
       windowDpSize.width < 840.dp
       else -> WindowSize.Expanded
   }
}

WindowManager 库很快就会推出直接使用这些类的 API,Compose 也会很快支持更方便的功能来完成此项工作,敬请期待。目前,您可暂时借用这一代码来完成这一功能上的需要。

△ JetNews 侧边抽屉导航栏展示

△ JetNews 侧边抽屉导航栏展示

回到 JetNews,我们可以看到在大屏状态下,侧边的抽屉导航栏会以模态的方式出现,但它会延伸到整个屏幕而出现大量空白区域。根据前文中提到的修改建议,是使用 Navigation Rail,而 Compose 则直接支持,我们仅需要对其进行设置并将内容传入即可。

NavigationRail(
    header = {
        JetnewsIcon()
    }
) {
    Column(verticalArrangement = Arrangement.Center) {
        Icon(
            icon = Icons.Filled.Home,
            action = navigateToHome
        )
        Icon(
            icon = Icons.Filled.ListAlt,
            action = navigateToInterests
        )
    }
}

标题图标和两个导航项图标,一个用于主页面,一个用于 Interests 页面,并添加它们对应的导航操作。为了将 Navigation Rail 集成到应用中,我们对顶层应用组件做了一些更改。首先,我们获取当前的窗口大小类,以及显示较小尺寸上的 ModalDrawer,然后确保设置了 ModalDrawer 让其只响应该尺寸中的手势。再将 Navigation Rail 与包含应用中所有屏幕的主导航图并排放置:

@Composable
fun JetnewsApp() {
    val windowSize = rememberWindowSizeState()
    val isDrawerActive = windowSize == WindowSize.Compact
    ModalDrawer(
        gesturesEnabled = isDrawerActive
        drawerContent = {...}
    ) {
        val showNavRail = isDrawerActive
        Row() {
            if (showNavRail) {
                AppNavRail()
            }
            JetnewsNavGraph()
        }
    }
}

然后我们发现由于文章列表依然在大屏下没有充分利用空间,因此我们决定在大屏下构建列表/详情布局,这一布局方式是 Material Design 中推荐的大屏幕规范布局之一,让我们将文章列表与打开的文章并排显示。JetNews 应用有两个我们可以复用的组件: PostList 和 PostContent,这种在一开始就将界面拆分为组件的做法,不仅能让测试更加容易,还能让我们轻松对布局进行改进。

为了并排显示 Feed 和 Post,JetNews 简单地使用 Row 包裹两个组件,第一个组件具有固定宽度,第二个组件填充屏幕的其余部分。详情组件包裹在交叉渐变动画中,这让用户点击列表打开文章时看到带有动画过渡的转换效果。

要正确构建列表/详情结构,除了实际布局之外我们还需要解决几个问题。其中比较有趣的一点是思考应用如何在不同尺寸布局之间转换,例如对于可折叠手机,应用可能会从较大的屏幕变为较小的屏幕。

△ 可折叠手机上布局转换

△ 可折叠手机上布局转换

为了正确处理如何将列表和详情窗口折叠成单窗口层次结构,当在较小的屏幕上时,我们需要知道用户最后与哪个窗口交互,为此,我们实现了一个简单的自定义修饰符来记录最后一次交互,并以此决定,在不同的折叠状态下应该显示什么内容,从而进一步提升层次结构。

@Composable
fun HomeFeedWithArticleDetailsScreen(...) {
    Row() {
        PostList(
            modifier
                .width(334.dp)
                .notifyInput(onInteractWithList))
        Crossfade(...) {
            PostContent(
                modifier
                    .fillMaxSize()
                    .notifyInput {
                        onInteractWithDetail(detailPost.id)
                    }
            )
        }
    )}
}

我们还需要知道,我们是从多大尺寸的屏幕将一次只显示其中一个窗口转变为显示列表/详情布局的。在 JetNews 中我们首先获取窗口大小类的信息,在较小和中等型宽度显示单窗口,而在展开型宽度显示列表/详情布局。

val windowSize = rememberWindowSizeState()

val homeScreenType = when (windowSize) {
    WindowSize.Compact,
    WindowSize.Medium -> HomeScreenType.Feed
    WIndowSize.Expanded -> HomeScreenType.FeeWithArticleDetails
}

然后,开始针对 JetNews 的导航进行更改。JetNews 最初以主页面和文章页面构建而成,每个页面都有自己的 ViewModel,导航和 ViewModel 之间的集成意味着两个页面始终在不同的导航路径上。但是,为了将页面重组成列表/详情布局,我们需要将这两个屏幕并排显示,此处我们有两种可选方案。一是在详情页面嵌套 NavHost,另外一种方案是统一 ViewModel,由于详情页面内并没有下一级别的导航入口而只会显示一篇打开的文章,我们决定采用第二种方式,将两个 ViewModel 合二为一来简化结构。

我们创建了三个主界面入口点,一个是 HomeFeedScreen,它只负责展示 PostList;一个是 ArticleScreen 负责展示 PostContent;以及新的 HomeFeedWithArticleDetailsScreen 负责显示包含 PostList 和 PostContent 的列表/详情布局。

△ 图左: 主界面入口点 HomeFeedScreen 图右: 主界面入口点 ArticleScreen

△ 图左: 主界面入口点 HomeFeedScreen 图右: 主界面入口点 ArticleScreen

△ 主界面入口点 HomeFeedWithArticleDetailsScreen

△ 主界面入口点 HomeFeedWithArticleDetailsScreen

上图是我们适配之前和适配之后的页面样式,可以发现对屏幕空间的利用有了非常大的改善。但这次更改是针对屏幕尺寸做的决策,我们是不是可以让单个组件自身根据页面而拥有不同尺寸呢?例如我们有一张卡片,当在列表中因为空间的限制只展示标题和副标题,而有更多空间时,则调整为显示图像。对于此类情况我们可以使用 Box With Constraints,它类似于框布局,能够根据范围内的测量信息来用于决策。

获取更好的用户体验

在前文中,我们提到为了提供更好的用户体验,请添加对应用有意义的功能,如支持可折叠设备。同 WindowManager API 类似,我们可以轻松地将 Compose 与针对可折叠设备的 API 进行集成。通过这些 API,能够获取到该设备是否且何时触发了铰链或折叠等功能,以及当前设备处于何种姿态。Compose 可以轻松观察这些 API 赋予的状态,从而轻松对界面进行转换。同样,关于此功能的 API 即将在 Compose 中提供,敬请期待。

除了目前提到的 API 之外,我们一直努力开发 Compose 的内部构件,以增强包括键盘和鼠标支持在内的输入设备,这对于在 Chrome OS 上运行的应用尤其有用。如需了解 Chrome OS 和输入详细信息,敬请关注我们近期的文章发布。

如需了解更多 Compose 示例详情,请查阅 Compose 示例代码

新的 Compose 和大屏幕指南——构建自适应布局,希望能够对您的开发有所帮助。

测试和维护

现在您已了解如何轻松更新应用,来构建可调整尺寸的新界面。如何测试和维护项目也是一个非常重要的课题。维护并支持所有不同尺寸的界面会大大引入测试复杂性,我们一直努力在不提高工作量的情况下,通过新的自动化测试工具和 API,让您能够配置更多设备来增加测试覆盖率。我们将会通过 Gradle 托管设备,从而实现在各种屏幕尺寸和 API 级别上运行虚拟设备来运行现有的 instrumentation 测试。您只需描述要在其上运行测试的设备的配置,其余均由 Gradle 负责,包括设备预先配置和测试工作的运行。

只需在构建脚本过程中定义设备,并将其添加到设备组:

testOptions
    devices {
        pixel2api29 (com.android.build.api.dsl.ManagedVirtualDevice) {
        nexus9api30 (com.android.build.api.dsl.ManagedVirtualDevice) {
            device = "Nexus 9"
            apiLevel = 30
            systemImageSource = "google"
            abi = "×86"
        }
    }
    deviceGroups {
        mediumAndExpandedWidth{
            targetDevices.addAll(devices.pixel2api29)
            targetDevices.addAll(devices.nexus9api30)
    }
}

然后使用 Gradle 托管设备组来运行测试:

$ gradlew -Pandroid.experimental.androidTest.useUnifiedTestPlatform=true mediumAndExpandedWidthGroupDebugAndroidTest

由于 Gradle 同时管理设备配置和测试作业,Gradle 托管设备还支持测试分片,让您能够跨指定数量的相同设备来分割测试从而减少总体测试作业时间。只需要指定以下参数即可指定要分片的数量:

$ gradlew -Pandroid.experimental.androidTest.numManagedDeviceShards=2 deviceDebugAndroidTest

但我们知道运行大量虚拟设备会占用 CPU 和内存,这可能会限制 Gradle 托管设备和测试分片的用处。为了解决此问题,Gradle 托管设备引入了一种针对 instrumentation 测试而优化的新型虚拟设备,称为自动化测试设备,这些设备以 headless 模式运行,禁用了自动化测试通常不需要的后台进程和服务,从而降低了每台设备的总体 CPU 和内存使用率,这将让您能够同时针对代表不同屏幕尺寸的多台设备运行测试。当前,这一功能可在 Android 10 上使用,随着时间的推移将支持更高的 API 级别,以确保现有的屏幕截图测试能够继续与自动化测试设备配合运行。

我们还在开发一组全新 AndroidX Testing API,让您能够将设备置于不同的状态进行测试。例如,您可以测试应用从平折变为半开状态,或在纵向或横向模式之间旋转时的反应。

总结

今天我们讨论了很多内容,从新的设计指南和窗口大小类,到用于更新现有应用的特定 API。大屏幕和可折叠设备代表 Android 的一个庞大且不断增长的细分市场,为了抓住这一增长机会,现在是时候为这些设备构建和设计界面,以便为使用最高级设备的用户获得出色的体验。

欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

版权声明

禁止一切形式的转载-禁止商用-禁止衍生 申请授权

脉脉不得语
脉脉不得语
Zhengzhou Website
Android Developer | https://androiddevtools.cn and https://androidweekly.io WebMaster | GDG Zhengzhou Funder & Ex Organizer | http://Toast.show(∞) Podcast Host

你已经成功订阅到 Android 开发技术周报
太棒了!接下来,完成检验以获得全部访问权限 Android 开发技术周报
欢迎回来!你已经成功登录了。
Unable to sign you in. Please try again.
成功!您的帐户已完全激活,您现在可以访问所有内容。
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.