可折叠感知的应用程序布局
#android #jetpackcompose #foldable

欢迎来到的最后一部分,了解可折叠设备。随着该系列的持续时间,让我们从简短的回顾开始。

在前三期中,我向您展示了为什么折叠率可能是或可能不是大屏幕设备,折叠和铰链如何影响UI元素的布局,以及可以使用哪些工具来测试应用程序在不同设备上的行为班级和形式。

第四部分提供了使您需要的所有代码使您的撰写应用程序在智能手机,平板电脑,折叠板和自由形式体验上表现良好。我的样本的重要部分利用 jetpack窗口管理器收集有关屏幕,应用程序窗口和 - 如果存在的信息。一点点信息称为窗口大小类。我用它来定义内部应用程序布局。由于实际上有两个窗口大小的类实现,因此在它们上反映了上一个(第五)。

我的示例代码确实通过使用Scaffold()TopAppBar()提供了基本的应用程序框架,但它缺乏一个重要方面:导航。在第六部分中,我们将研究相应的组合。如您所见,设备类别会影响您应该使用哪个。我还会向您展示,不同的导航合成件会影响内容可用的空间及其在屏幕上的位置。

听起来很有趣吗?很棒,请阅读。

布局解剖学

应用程序中最重要的部分是其内容。内容是您的用户运行应用程序的原因。请回想一下,根据设备类别和可用空间,我建议一列,两个甚至更多列。但是这些专栏里有什么?我们将在 content 节中转到此。顺便说一句,材料设计文档将内容称为 body 。但是我觉得内容更具表现力。

屏幕上的另一个重要区域保留用于导航。查看常见的Android应用程序,您会发现一些可能发生导航的地方:

  • 在应用程序窗口的顶部(TopAppBar(),�)
  • 在应用程序窗口的底部(NavigationBar(),�)
  • 在左App Window Border(PermanentNavigationDrawer()NavigationRail(),â)

如果您想知道为什么我使用术语 app窗口而不是屏幕â€,而应用程序可用的空间受屏幕尺寸的限制,则这确实是不是的含义,而不是该应用程序可以完全占据该区域。在自由形式的体验中,我们具有可重新设置的窗口,即使在智能手机上,我们也可以在拆分屏幕模式下并排运行两个应用程序。因此,屏幕上的区域 可以随着时间的推移而变化。

当应用程序栏显示信息和操作时,这些操作也可能触发导航。因此,我将应用程序栏视为导航组件。

这里有一些收获:

  1. 有两个主要布局区域,导航 content
  2. 我们有几个导航组合可供选择。
  3. 术语布局可以参考外部布局(导航组件和内容的位置和大小)和 ninter Layout (如何提出了内容)。

外部布局的一部分由提供的组合物处理(例如,Scaffold())。但是有些需要由您的应用程序管理。以下是我的 foldabledemo 示例的最新版本在不同情况下的样子:

FoldableDemo running in portrait smartphone mode

在肖像模式下的智能手机上,我们通常在顶部和底部都有应用程序栏。顶部的应用程序栏也可能包括模态导航抽屉(为了保持简单, foldabledemo 没有一个)。

FoldableDemo running in landscape smartphone mode

如果设备足够宽(您会很快看到如何计算),则该应用程序可能会切换到导航栏。在更大的屏幕上,这也可能是一个永久性导航抽屉(为了保持简单, foldabledemo 不这样做)。

FoldableDemo running in portrait foldable mode

在肖像模式下的开放折叠设备上,我们可能会使用导航导轨或永久导航抽屉。该应用程序为内容使用两列布局。请注意,由于导航导轨,左列稍小。

FoldableDemo running in landscape foldable mode

如果我们旋转可折叠(折叠或铰链水平运行),我建议使用一列,但要对内容进行两行,尤其是在铰链阻塞的情况下。正如您从前面的部分中知道的那样,我的示例代码会考虑到障碍物和铰链,因此您的内容的任何部分都不可见。但是如何使用两列或行?我们将在 content 中解决此问题。

现在,您可能正在思考好吧,这看起来很棒,但是修订的代码看起来像?

窗口大小类是定义应用程序布局的关键元素。以前的版本的 foldabledemo 已经将它们用于内部应用程序布局,以区分智能手机,折叠式,大屏幕设备和自由形式体验。它仍然可以,但是现在样本还使用这些 switches 在之间切换。

  • 底部栏导航和导航轨或导航抽屉
  • 显示顶部应用程序栏并且不显示顶部应用程序栏

看看:

setContent {
  val layoutInfo by WindowInfoTracker.getOrCreate(this@FoldableDemoActivity)
    .windowLayoutInfo(this@FoldableDemoActivity).collectAsState(
      initial = null
    )
  val windowMetrics = WindowMetricsCalculator.getOrCreate()
    .computeCurrentWindowMetrics(this@FoldableDemoActivity)
  // might become part of some UIState - kept here for simplicity
  val foldDef = createFoldDef(layoutInfo, windowMetrics)
  val hasTopBar =
    foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
  val hasBottomBar =
    foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT
  val hasNavigationRail = !hasBottomBar
  val index = rememberSaveable { mutableStateOf(0) }
  MaterialTheme(
    content = {
      Scaffold(
        topBar = { MyTopBar(hasTopBar = hasTopBar) },
        bottomBar = {
          MyBottomBar(
            hasBottomBar = hasBottomBar,
            index = index
          )
        }
      ) { padding ->
        Content(
          foldDef = foldDef,
          paddingValues = padding,
          hasNavigationRail = hasNavigationRail,
          index = index
        )
      }
    },
    colorScheme = defaultColorScheme()
  )
}

基于foldDef,我们定义了一些变量,hasTopBarhasBottomBarhasNavigationRail,然后将它们传递到几个组合。这是MyBottomBar()的作用:

@Composable
fun MyBottomBar(hasBottomBar: Boolean, index: MutableState<Int>) {
  if (hasBottomBar)
    NavigationBar {
      for (i in 0..2)
        NavigationBarItem(selected = i == index.value,
                 onClick = { index.value = i },
                 icon = {
                   Icon(
                     painter = painterResource(id = R.drawable.ic_android_black_24dp),
                     contentDescription = null
                   )
                 },
                 label = {
                   MyText(index = i)
                 }
        )
    }
}

因此,仅当hasBottomBartrue时,MyBottomBar()才调用NavigationBar {}。您还记得我说过窗口尺寸的类像开关一样?看看:

val hasBottomBar =
    foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.COMPACT

在您的应用程序中,您可能需要将这些交换机放入某些UI状态类中,并将该状态传递给您的组合。我决定保留裸露的变量,以使事情变得更加透明和简单。

内容

以下是更新的Content()组合形式的样子:

@Composable
fun Content(
  foldDef: FoldDef,
  paddingValues: PaddingValues,
  hasNavigationRail: Boolean,
  index: MutableState<Int>
) {
  Row(modifier = Modifier.fillMaxSize()) {
    if (hasNavigationRail)
      NavigationRail {
        for (i in 0..2)
          NavigationRailItem(selected = i == index.value,
                    onClick = {
                      index.value = i
                    },
                    icon = {
                      Icon(
                        painter = painterResource(id = R.drawable.ic_android_black_24dp),
                        contentDescription = null
                      )
                    },
                    label = {
                      MyText(index = i)
                    })
      }
    BoxWithConstraints(
      modifier = Modifier
        .fillMaxSize()
        .padding(paddingValues = paddingValues)
    ) {
      if (foldDef.hasFold) {
        FoldableScreen(
          foldDef = foldDef
        )
      } else if (foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED) {
        LargeScreen(
          foldDef = foldDef
        )
      } else {
        SmartphoneScreen(
          foldDef = foldDef
        )
      }
    }
  }
}

您是否注意到,除了组合的名称之外,我的NavigationRailItemNavigationBarItem Creation Loop是相同的?我真的很希望,Google会添加一个常见的组合,NavigationItem。这将极大地减轻重复使用并避免代码重复。反正。尽管BoxWithConstraints()已经存在于先前版本中,但现在它是Row()的孩子。如果hasNavigationRailtrue,还有一个NavigationRail ()。因此,根据该开关的不同,Row()有一个或两个孩子。顺便说一句,这就是我说外部布局的某些部分需要由您处理的原因。为了使您的应用程序布局更加复杂,您可以在大屏幕上显示永久性导航抽屉而不是导航栏。为此,您首先需要像这样定义开关:

val hasNavigationRail =
      foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.MEDIUM
val hasPermanentNavigationDrawer =
      foldDef.windowSizeClass.windowWidthSizeClass == WindowWidthSizeClass.EXPANDED

然后,在Content()中,您只需根据该开关调用相应的合成件(NavigationRail()PermanentNavigationDrawer()

当我增加对导航导轨的支持时,我在以前的实现中发现了一个小故障。你能猜出这里发生了什么吗?

Screenshot of a source code diff

您可能还记得,至少在带有折叠或铰链的设备上,我建议使用同样大小的区域的两列布局。但是,导航导轨缩小了可用于左列中内容的空间。我们需要考虑到这一点。虽然我们可以尝试计算导航导轨的宽度并减去它,但让JetPack撰写工作更容易。

最后一句话

foldabledemo 使用彩色框来表示应用内容。基于设备的类别和外形因素,内容区域可以分为两个矿石列。请回想一下我的示例代码处理折叠和铰链,因此您只需要填充列 fill 即可。什么是取决于应用程序的目的。 Google查看了许多应用程序,并确定了三种一般布局类型:

  • feed 在网格中安排卡片或其他内容元素
  • list-detail 显示项目列表和一个(选定的,当前)项目并排的细节
  • 支持窗格将应用程序内容组织到主要和次要显示区域

Google称他们为Canonical layouts,被描述为

即可使用的成分,可帮助布局适应常见用例和屏幕尺寸。

这个想法是将它们用作组织应用程序中常见元素的起点。材料设计网站说明:

每个布局都考虑常见的用例和组件,以满足应用程序如何在窗口类规模和断点之间适应应​​用的期望和用户需求。

因此,至少目前,规范布局更像是概念,而不是现成的组件。但是,Google提供了很多使用 /实施的样本。有关详细信息,请参考 Resources 部分。我可能会在今年晚些时候提供 foldabledemo 的增强版本。

好吧,就是这样。希望您喜欢这个系列。请在评论中分享您的想法。


除非另有说明,否则所有图片均为(c)ThomasKã¼nneth

资源