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

Posted 郭霖

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了为任意屏幕尺寸构建 Android 界面相关的知识,希望对你有一定的参考价值。



/   今日科技快讯   /


北京时间2022年1月17日,我国在太原卫星发射中心用长征二号丁运载火箭,成功将试验十三号卫星发射升空,卫星顺利进入预定轨道,发射任务获得圆满成功。


/   前言   /


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


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


/   用户参与度   /


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


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


我们还观察到那些针对所有屏幕尺寸进行优化的应用,在围绕用户互动度、留存率等指标上,都取得了不错的成绩。比如其中的一个成功案例 Candy Camera,它通过优化可折叠设备和大屏幕的布局,使得使用这些设备的用户在应用上花费的时间增加了 10%,7 天用户留存率更是增长了 14%,而这并不是个例。


另一个案例是 Microsoft Outlook,它最近的更新通过使用双窗口布局充分发挥了大屏优势,可以同时查看收件箱和电子邮件内容,并能够在拥有多个显示屏的某个单独窗口中独立撰写电子邮件。这些例子充分表明: 是时候开始摆脱手机这一单一界面限制的束缚,从而自由地进行设计和开发了。


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


/   窗口大小类和ReferenceDevices   /


在多元化的设备生态中,各种 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


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


  1. 为了确保应用在不同设备尺寸上都能够正确展示,请优先针对较小和展开型宽度大小类来优化布局;

  2. 在所有的 Reference Devices 上都测试一遍您的应用,优先采用在中等型下的最佳布局;

  3. 为了提供更好的用户体验,请添加对应用有意义的功能,如支持可折叠设备的折叠状态或针对键盘、鼠标和触控笔输入支持进行优化;


/   适配大屏   /


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


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


△ 更改之前的 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 界面,您会发现有一个新的 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 MainActivityAppCompatActivity() 
...
    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 下的效果


△ 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 的主界面展示


前文中已经介绍了 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,我们可以看到在大屏状态下,侧边的抽屉导航栏会以模态的方式出现,但它会延伸到整个屏幕而出现大量空白区域。根据前文中提到的修改建议,是使用 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


△ 主界面入口点 HomeFeedWithArticleDetailsScreen


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


/   获取更好的用户体验   /


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


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


/   测试和维护   /


现在您已了解如何轻松更新应用,来构建可调整尺寸的新界面。如何测试和维护项目也是一个非常重要的课题。维护并支持所有不同尺寸的界面会大大引入测试复杂性,我们一直努力在不提高工作量的情况下,通过新的自动化测试工具和 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,让您能够将设备置于不同的状态进行测试。例如,您可以测试应用从平折变为半开状态,或在纵向或横向模式之间旋转时的反应。


AndroidX Testing API:

https://developer.android.google.cn/jetpack/androidx/releases/test


/   总结   /


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


推荐阅读:

我的新书,《第一行代码 第3版》已出版!

2021年终总结,我躺平了

MotionLayout,一篇文章带你了解


欢迎关注我的公众号

学习技术或投稿



长按上图,识别图中二维码即可关注

使用乘数的自动布局在 xcode 8 的界面构建器中为不同的屏幕尺寸创建自动可调整大小的视图

【中文标题】使用乘数的自动布局在 xcode 8 的界面构建器中为不同的屏幕尺寸创建自动可调整大小的视图【英文标题】:creating auto resizeable views for different screen size in interface builder of xcode 8 using auto layout using multipliers 【发布时间】:2017-02-06 13:16:13 【问题描述】:

我曾尝试在IB 中使用Autoresizing 来实现这一点,使用constraints

固定, 纵横比, 垂直间距, 水平/垂直间距, 前导/尾随空格;

但似乎没有一个适用于所有屏幕尺寸。

例如

iphone 7 中的 4 个图像视图是它们所需的输出位置,但一旦我在iphone 7 plus 屏幕尺寸或iphone SE 屏幕尺寸中检查它,这些图像视图就会改变位置。

以下是 iphone 7 上的预期输出。

iphone7 图片

下面是 不期望的输出,因为 iphone 7 plus 上 4 个图像视图的位置发生了变化。 iphone7plus

我希望再分享一张 iPhone SE 的图片,其中所有 4 张图片视图的位置变化更剧烈,但无法分享导致声誉低于 10

我将解释,例如,从第一张图像,即 iphone 7,这 4 个小图像视图可以与白色背景的图像视图一起作为预期位置,但在其他两种屏幕尺寸(iphone7 plus 和 iphoneSE ) 所有视图(4 个小图像视图和白色背景的视图)都应该与第一个视图相比具有相应的定位缩放

更新:添加代码 我刚刚对@diana_prodan 在这篇文章中得到的答案之一进行了一些更改,以获得预期的输出。

import UIKit

class ViewController: UIViewController 

@IBOutlet weak var imgOne: UIImageView!

@IBOutlet weak var imgTwo: UIImageView!

@IBOutlet weak var imgThree: UIImageView!

@IBOutlet weak var imgFour: UIImageView!

let icon_small : Float = 45.0
let icon_medium : Float = 50.0

override func viewDidLoad() 
    super.viewDidLoad()
    
    let viewSize = UIScreen.main.bounds.size 
    print(viewSize)
    //image positioned
    //for 4' screen
    if (viewSize.height == 568.0)
        addImageAtPosition(imgOne, icon_small,41, 106 )
        addImageAtPosition(imgTwo, icon_small,105,91 )
        addImageAtPosition(imgThree, icon_small,105, 147)
        addImageAtPosition(imgFour, icon_small,139, 197 )
    //for 4.7' screen
    else if(viewSize.height == 667.0)
        addImageAtPosition(imgOne, icon_medium,52, 135 )
        addImageAtPosition(imgTwo, icon_medium,127,106 )
        addImageAtPosition(imgThree, icon_medium,127, 172 )
        addImageAtPosition(imgFour, icon_medium,167, 230 )
     //for 5.5' screen   
    else if(viewSize.height == 736.0)
        //add code woth cordinates for 5.5 screen size iPhone
    

//method to positon image views
func addImageAtPosition(_ img:UIImageView, _ size: Float, _ y: Float, _ x: Float) 
    let imageView = img
    imageView.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(imageView)
    
    let imageSize: CGFloat = CGFloat(size)
    
    let topConstraint = NSLayoutConstraint(item: imageView, attribute: .top, relatedBy: .equal, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1, constant: CGFloat(y))
    
    let leftContraint = NSLayoutConstraint(item: imageView, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: CGFloat(x))
    
    let widthConstraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: imageSize)
    
    let heightConstraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: imageSize)
    
    view.addConstraints([topConstraint, leftContraint, widthConstraint, heightConstraint])

此代码的问题是我必须获取不同屏幕尺寸上所有图像视图视图的坐标并对其进行硬编码,我认为这不是一个好习惯

【问题讨论】:

添加您的预期输出。 @the_dahiya_boy : 已提供链接。 请像我一样在这里添加图片,不要提供链接,因为它会降低问题质量。添加您所做的方法,以便我们检查您做错了什么。 @the_dahiya_boy 我已经做到了。 【参考方案1】:

以给定半高约束的视图 A 为主视图的高度。之后给所有图像相同的高度和宽度约束。给一个图像的 x,y 约束与视图 A 的中心。并链接所有图像设置 defullts 约束。

【讨论】:

【参考方案2】:

您必须为 ImageView 的顶部和左侧约束设置随机值。

func addImageAtRadomPosition() 
    let imageView = UIImageView()
    imageView.backgroundColor = UIColor.red
    imageView.translatesAutoresizingMaskIntoConstraints = false
    self.view.addSubview(imageView)

    let imageSize: CGFloat = 50.0
    let screenHeight = UIScreen.main.bounds.size.height
    let screenWidth = UIScreen.main.bounds.size.width

    // Get random values which will be used for top and left constraints
    let randomYPosition = Int(arc4random_uniform(UInt32(screenHeight - imageSize)))
    let randomXPosition = Int(arc4random_uniform(UInt32(screenWidth - imageSize)))

    let topConstraint = NSLayoutConstraint(item: imageView, attribute: .top, relatedBy: .equal, toItem: self.topLayoutGuide, attribute: .bottom, multiplier: 1, constant: CGFloat(randomYPosition))
    let leftContraint = NSLayoutConstraint(item: imageView, attribute: .left, relatedBy: .equal, toItem: self.view, attribute: .left, multiplier: 1, constant: CGFloat(randomXPosition))
    let widthConstraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.width, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: imageSize)
    let heightConstraint = NSLayoutConstraint(item: imageView, attribute: NSLayoutAttribute.height, relatedBy: NSLayoutRelation.equal, toItem: nil, attribute: NSLayoutAttribute.notAnAttribute, multiplier: 1, constant: imageSize)

    view.addConstraints([topConstraint, leftContraint, widthConstraint, heightConstraint])

调用此方法的次数与您的 ImageViews 数一样多

for _ in 0...5 
    addImageAtRadomPosition()

【讨论】:

感谢您的回答,我想我的问题问错了,我已经编辑了它,再次感谢。【参考方案3】:

如果这是您的预期输出,请通知我,以便我描述如何实现此目标,如果不是,请告诉我您还期望什么。

在下方评论。

编辑

当我在iPhone 6+ 中运行相同的 VC 时,我得到了以下输出。所以考虑一下这个输出。

【讨论】:

感谢您的回复,这不是预期的输出。例如,我将解释您的第一张图片,即 iphone 6s,这 4 个小图片视图可以与白色背景的图片视图一起作为预期位置,但在其他两种屏幕尺寸(iphoneSE 和 iphone6S plus)中都是与第一个视图相比,视图(4 个小图像视图和具有白色背景的视图)应具有相应的 定位 缩放。 @the_dahiya_boy @Raj 我没有得到你的定位缩放点。对不起。 再次感谢您的回复,我已经用代码更新了我的帖子,我也会在这里解释。在白色背景的图像视图旁边的四个小图像视图的位置和大小缩放应在所有屏幕上保持相对恒定。并且白色背景的位置应该与所有屏幕的超级视图相对恒定。 @Raj 首先你的约束没有被激活。因此,除非它们未被激活,否则它不起作用。检查编辑我的答案。 #the_dahiya_boy 我使用相同的代码来获得预期的输出:[link] (i.stack.imgur.com/HePON.png) [link] (i.stack.imgur.com/XsZU0.png) [link] (i.stack.imgur.com/ZrL1R.png)【参考方案4】:

为了达到预期的输出我去了这个参考[参考]:https://developer.apple.com/library/content/documentation/UserExperience/Conceptual/AutolayoutPG/AnatomyofaConstraint.html,所以我所要做的就是

选择其中一个视图并按住 ctrl+ 拖动到外部视图 然后按住 shift 并选择 等宽 & 等高, 居中 在容器中水平放置 & 在容器中垂直居中然后选择每个约束线(黄色)或从视图控制器场景列并修改乘数,直到黄色线变为蓝色,即直到 它满足了我最初将图像视图放置在视图控制器布局中的位置。

here is the screen shot link it shows where to select constraints one by one(on left) and modify multiplier(on right).

【讨论】:

以上是关于为任意屏幕尺寸构建 Android 界面的主要内容,如果未能解决你的问题,请参考以下文章

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

具有基于屏幕尺寸的固定方向

flutter通过 GlobalKey 获取界面任意元素坐标尺寸

确定多个 android 屏幕尺寸/密度的图像尺寸

Android5.1关机充电界面尺寸修改

android屏幕适配做哪几个尺寸