完成MySoothe应用的界面设计

一、实验名称

  完成MySoothe应用的界面设计。

二、 参考资料

《Android开发者官方网站:https://developer.android.google.cn/》、第二章课件。

三、实验目的

在本实验中,您将根据设计人员提供的设计方案实现真实的应用设计。MySoothe 是一款健康应用,其中列出了可改善身心健康的各种方法。这款应用包含两个版块,一个列出了您的收藏合集,另一个列出了各种体育锻炼。该应用如下所示:

应用的纵向版本

应用的横向版本

四、实验内容

1. 实验要求
本实验需要使用Android Studio的最新版本(最低也要Android Studio 2023.1.1)。
2. 初始项目
本实验在BasicLayoutsCodelab项目的基础上完成,该项目已经提供实验需要的图片资源。

3. 从制定计划着手
首先实现应用的纵向设计。下面我们来详细了解一下:

纵向设计

在需要实现设计时,最好先清楚了解其结构。不要立即开始编码,而应分析设计本身。如何将此界面拆分为多个可重复利用的部分?

接下来,我们试着分析一下设计。在最高抽象级别,我们可以将此设计细分为两部分:

屏幕上的内容。

底部导航栏。

应用设计细分

进一步细分后,您可以看到屏幕内容包含三个子部分:

搜索栏。

“Align your body”版块。

“Favorite Collections”版块。

应用设计细分

在每个版块中,您还可以看到一些可重复利用的较低级别组件:

“Align your body”元素,显示在可水平滚动的行中。

“Align your body”元素

“Favorite Collections”卡片,显示在可水平滚动的网格中。

“Favorite Collections”卡片

        现在,您已经分析了设计,接下来可以开始为每个已确定的界面部分实现可组合项。先从最低级别的可组合项着手,然后继续将它们组合成更复杂的可组合项。完成此 Codelab 后,您的新应用将与所提供的设计相似。

4. 搜索栏 - 修饰符
第一个要转换为可组合项的元素是搜索栏。我们再来看一下设计:

搜索栏

        只通过上面的屏幕截图,很难在实现该设计时让像素完美呈现。通常情况下,设计人员应传达更多关于设计的信息。他们可以授权您访问他们的设计工具,或分享所谓的用红线标注的设计。在此示例中,我们的设计人员提交了用红线标注的设计,方便您读出任何尺寸值。该设计采用 8dp 网格叠加层的方式显示,以便您轻松查看各个元素之间和周围的空间大小。此外,还明确添加了一些间距,以阐明特定尺寸。

搜索栏红线

        您可以看到,搜索栏的高度应为 56 密度无关像素。它还应填充其父项的全宽。

        若要实现搜索栏,请使用名为文本字段的 Material 组件。Compose Material 库包含一个名为 TextField 的可组合项,后者是此 Material 组件的实现。

        从基本的 TextField 实现着手。在您的代码库中,打开 MainActivity.kt 并搜索 SearchBar 可组合项。

        在名为 SearchBar 的可组合项内,编写基本的 TextField 实现:

import androidx.compose.material3.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
   )
}

需要注意以下几点:

        您已对文本字段的值进行了硬编码,并且 onValueChange 回调不会执行任何操作。由于此 Codelab 主要介绍布局,因此您可以忽略与状态相关的一切内容。

        SearchBar 可组合函数接受 modifier 形参,并将其传递给 TextField。这是符合 Compose 准则的最佳实践。这样一来,方法调用方就可以修改可组合项的外观和风格,使其更加灵活且可重复利用。对于此 Codelab 中的所有可组合项,您应继续遵循这一最佳实践。

        我们来看一看此可组合项的预览。请注意,您可以使用 Android Studio 中的预览功能快速迭代各个可组合项。MainActivity.kt 包含您要在此 Codelab 中构建的所有可组合项的预览。在此示例中,SearchBarPreview 方法会渲染我们的 SearchBar 可组合项,并提供一些背景和内边距,以便提供更多上下文。完成您刚才添加的实现后,预览应如下所示:

搜索栏预览

        不过,还缺少一些内容。首先,我们使用修饰符修正可组合项的尺寸。

编写可组合项时,您可以使用修饰符执行以下操作:

        更改可组合项的尺寸、布局、行为和外观。

        添加信息,例如无障碍标签。

        处理用户输入。

        添加高级互动,例如使元素可点击、可滚动、可拖动或可缩放。

您调用的每个可组合项都有一个 modifier 形参,您可以设置该形参以适应相应可组合项的外观、风格和行为。设置修饰符时,您可以将多个修饰符方法串联起来,以创建更复杂的调适。

如果您想要详细了解修饰符的行为,请参阅修饰符文档。您还可以查看可用修饰符的完整列表

        在此示例中,搜索栏的高度应至少为 56dp,并填充其父项的宽度。为了找到适合搜索栏的修饰符,您可以浏览修饰符列表,并查看尺寸部分。对于高度,您可以使用 heightIn 修饰符。这可确保该可组合项具有特定的最小高度。但是,如果用户放大系统字体大小,该高度会随之变高。对于宽度,您可以使用 fillMaxWidth 修饰符。此修饰符可确保搜索栏占用其父项的所有水平空间。

        更新修饰符以匹配下面的代码:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.material3.TextField

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

        在此示例中,由于一个修饰符会影响宽度,另一个修饰符会影响高度,因此这些修饰符的顺序无关紧要。

        您还必须设置 TextField 的一些形参。您可以试着设置形参值,以使可组合项类似于设计。同样,以下设计供您参考:

搜索栏

更新实现时应采取以下步骤:

  • 添加搜索图标。TextField 包含一个可接受其他可组合项的 leadingIcon 形参。您可以在其中设置 Icon,在此示例中应为 Search 图标。请务必使用正确的 Compose Icon 导入。

  • 您可以使用 TextFieldDefaults.textFieldColors 替换特定颜色。将文本字段的 focusedContainerColor 和 unfocusedContainerColor 设置为 MaterialTheme 的 surface 颜色。

  • 添加占位符文本“Search”(以字符串资源 R.string.placeholder_search 的形式显示)。

完成后,您的可组合项应如下所示:

import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search

@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   TextField(
       value = "",
       onValueChange = {},
       leadingIcon = {
           Icon(
               imageVector = Icons.Default.Search,
               contentDescription = null
           )
       },
       colors = TextFieldDefaults.colors(
           unfocusedContainerColor = MaterialTheme.colorScheme.surface,
           focusedContainerColor = MaterialTheme.colorScheme.surface
       ),
       placeholder = {
           Text(stringResource(R.string.placeholder_search))
       },
       modifier = modifier
           .fillMaxWidth()
           .heightIn(min = 56.dp)
   )
}

搜索栏

请注意:

  • 您添加了一个用于显示搜索图标的 leadingIcon。此图标不需要内容说明,因为文本字段的占位符已经说明了文本字段的含义。请注意,内容说明通常用于实现无障碍功能,以文本形式向应用用户呈现图片或图标。

  • 如需调整文本字段的背景颜色,请设置 colors 属性。该可组合项包含一个组合形参,而不是每种颜色的单独形参。在这种情况下,您可以传入 TextFieldDefaults 数据类的副本,从而通过该类仅更新不同的颜色。在此示例中,即仅更新 unfocusedContainerColor 和 focusedContainerColor 颜色。

        在此步骤中,您了解了如何使用可组合项的形参和修饰符来更改可组合项的外观和风格。这适用于 Compose 库和 Material 库提供的可组合项,以及您自己编写的可组合项。您应始终考虑通过提供形参来自定义所编写的可组合项。您还应添加 modifier 属性,以便从外部调整可组合项的外观和风格。

5. Align your body - 对齐

接下来,您要实现的可组合项是“Align your body”元素。我们来看看该元素的设计,包括它旁边的红线设计:

“Align your body”组件

“Align your body”红线

红线设计现在还包含面向基线的间距。以下是我们从中获得的信息:

  • 图片高度应为 88dp。

  • 文本基线与图片之间的间距应为 24dp。

  • 基线与元素底部的间距应为 8dp。

  • 文本的排版样式应为 bodyMedium。

文本基线是指字母所在的那一行。设计人员认为根据基线(而不是顶部或底部)对齐文本元素是一种最佳实践。

        如需实现此可组合项,您需要一个 Image 和一个 Text 可组合项。您还需要将它们添加到 Column 中,以便一个位于另一个下方。

        在您的代码中找到 AlignYourBodyElement 可组合项,使用以下基本实现更新其内容:

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.res.painterResource

@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null
       )
       Text(text = stringResource(R.string.ab1_inversions))
   }
}

请注意:

  • 您将图片的 contentDescription 设为 null,因为这张图片是纯装饰性的。图片下方的文本充分描述了图片的含义,因此无需为图片添加额外的说明。

  • 您使用的是经过硬编码的图片和文本。在下一步中,您要为这些内容改用 AlignYourBodyElement 可组合项中提供的形参,使其变为动态形式。

查看此可组合项的预览:

“Align your body”预览

        您需要进行一些改进。最值得注意的是,图片过大,且形状不是圆形。您可以使用 size 和 clip 修饰符以及 contentScale 形参调整 Image 可组合项。

   size 修饰符会调整可组合项以适应特定尺寸,这类似于您在上一步中看到的 fillMaxWidth 和 heightIn 修饰符。clip 修饰符的工作原理有所不同,用于调整可组合项的外观。您可以将该修饰符设置为任何 Shape,然后它会按照相应形状对可组合项的内容进行裁剪。

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(text = stringResource(R.string.ab1_inversions))
   }
}

现在,预览中的设计如下所示:

“Align your body”预览

图片也需要正确缩放。为此,我们可以使用 Image 的 contentScale 形参。具体选项有很多,最值得注意的是:

“Align your body”内容预览

在此示例中,剪裁类型就是要使用的正确类型。应用修饰符和形参后,您的代码应如下所示:

import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier
   ) {
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text( text = stringResource(R.string.ab1_inversions) )
   }
}

现在,您的设计应如下所示:

“Align your body”预览

接下来,通过设置 Column 的对齐方式来水平对齐文本。

一般来说,若要对齐父容器中的可组合项,您应设置该父容器的对齐方式。因此,您应告知父项如何对齐其子项,而不是告知子项将其自身放置在父项中。

对于 Column,您可以决定其子项的水平对齐方式。具体选项包括:

  • Start

  • CenterHorizontally

  • End

对于 Row,您可以设置垂直对齐。具体选项类似于 Column 的选项:

  • Top

  • CenterVertically

  • Bottom

对于 Box,您可以同时使用水平对齐和垂直对齐。具体选项包括:

  • TopStart

  • TopCenter

  • TopEnd

  • CenterStart

  • Center

  • CenterEnd

  • BottomStart

  • BottomCenter

  • BottomEnd

        容器的所有子项都将遵循这一相同的对齐模式。您可以通过向单个子项添加 align 修饰符来替换其行为。

        对于此设计,文本应水平居中。为此,请将 Column 的 horizontalAlignment 设置为水平居中:

import androidx.compose.ui.Alignment
@Composable
fun AlignYourBodyElement(
   modifier: Modifier = Modifier
) {
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,
       modifier = modifier
   ) {
       Image(
           //..
       )
       Text(
           //..
       )
   }
}

实现这些部分后,您只需进行一些细微更改,即可使可组合项与设计完全相同。如果遇到问题,不妨尝试自行实现这些部分,也可以引用最终代码。考虑采取以下步骤:

  • 将图片和文本变为动态形式。将它们作为实参传递给可组合函数。别忘了更新相应的预览,并传入一些经过硬编码的数据。

  • 更新文本以使用 bodyMedium 排版样式。

  • 根据图表更新文本元素的基线间距。

“Align your body”红线

执行完这些步骤后,您的代码应如下所示:

import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.ui.Alignment
import androidx.compose.ui.layout.ContentScale

@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Column(
       modifier = modifier,
       horizontalAlignment = Alignment.CenterHorizontally
   ) {
       Image(
           painter = painterResource(drawable),
           contentDescription = null,
           contentScale = ContentScale.Crop,
           modifier = Modifier
               .size(88.dp)
               .clip(CircleShape)
       )
       Text(
           text = stringResource(text),
           modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
           style = MaterialTheme.typography.bodyMedium
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}

通常情况下,您可以通过多种方式取得相同的效果,因此您的实现可能与此建议的解决方案略有不同。例如,您通常可以使用 Spacer 或设置内边距,这两种方法可取得相同的视觉效果。最重要的一点是,您的实现应遵循设计准则和 Compose 准则。

查看“Design”标签页中的 AlignYourBodyElement。

“Align your body”预览

6. “Favorite collection”卡片 - Material Surface

下一个可组合项的实现方式与“Align your body”元素类似。设计(包括红线)如下所示:

“Favorite Collections”卡片

“Favorite Collections”卡片红线

此示例提供了可组合项的完整尺寸。您可以看到,文本的排版样式应为 titleMedium。

此容器使用 surfaceVariant 作为其背景颜色,不同于整个屏幕的背景。它还带有圆角。我们使用 Material 的 Surface 可组合项为“Favorite Collections”卡片指定这些属性。

您可以根据自己的需求调整 Surface,只需设置其形参和修饰符即可。在此示例中,表面应有圆角。针对这种情况,您可以使用 shape 形参。对于上一步中的图片,您将形状设置为 Shape,但在这一步中,您将使用来自我们的 Material 主题的值。

我们来看一看效果如何:

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.Surface

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
   ) {
       Row {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null
           )
           Text(text = stringResource(R.string.fc2_nature_meditations))
       }
   }
}

我们来看一下此实现的预览:

“Favorite Collections”预览

接下来,运用在上一步中学到的经验。

  • 设置 Row 的宽度,并与其子项垂直对齐。

  • 根据图表设置图片尺寸,然后在容器中对其进行剪裁。

“Favorite Collections”红线

先尝试自行实施这些更改,然后再查看解决方案代码!

现在,您的代码如下所示:

import androidx.compose.foundation.layout.width

@Composable
fun FavoriteCollectionCard(
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(R.string.fc2_nature_meditations)
           )
       }
   }
}

预览现在应如下所示:

“Favorite Collections”预览

如需完成此可组合项,请实施以下步骤:

  • 将图片和文本变为动态形式。将它们作为实参传入可组合函数。

  • 将颜色更新为 surfaceVariant。

  • 更新文本以使用 titleMedium 排版样式。

  • 更新图片与文本之间的间距。

最终结果应如下所示:

@Composable
fun FavoriteCollectionCard(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   Surface(
       shape = MaterialTheme.shapes.medium,
       color = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(drawable),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(
               text = stringResource(text),
               style = MaterialTheme.typography.titleMedium,
               modifier = Modifier.padding(horizontal = 16.dp)
           )
       }
   }
}

//..

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionCardPreview() {
   MySootheTheme {
       FavoriteCollectionCard(
           text = R.string.fc2_nature_meditations,
           drawable = R.drawable.fc2_nature_meditations,
           modifier = Modifier.padding(8.dp)
       )
   }
}

查看 FavoriteCollectionCardPreview 的预览。

“Favorite Collections”预览

7. “Align your body”行 - 排列

现在,您已经创建了屏幕上显示的基本可组合项,接下来可以开始创建屏幕的不同部分了。

先从“Align your body”可滚动行着手。

image.png

此组件的红线设计如下所示:

“Align your body”红线

请注意,一个网格块代表 8dp。因此,在此设计中,该行中的第一项内容前和最后一项内容后均留有 16dp 的间距。各项内容之间的间距为 8dp。

在 Compose 中,您可以使用 LazyRow 可组合项实现这种可滚动行。如需详细了解延迟列表(例如 LazyRow 和 LazyColumn),请参阅有关列表的文档。在此 Codelab 中,了解 LazyRow 只会渲染屏幕上显示的元素(而不是同时渲染所有元素)就足够了,这有助于让应用保持出色性能。

先从此 LazyRow 的基本实现着手:

import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

如您所见,LazyRow 的子项不是可组合项。您应改用延迟列表 DSL,它可提供 item 和 items 等方法,这些方法会以列表项的形式发出可组合项。对于所提供的 alignYourBodyData 中的各项,您可以发出之前实现的 AlignYourBodyElement 可组合项。

请注意显示方式:

“Align your body”预览

我们在红线设计中看到的间距仍未显示。若要实现这些部分,您必须了解排列方式。

在上一步中,您了解了对齐方式,它用于在交叉轴上对齐容器的子项。对于 Column,交叉轴是水平轴;对于 Row,交叉轴则是垂直轴。

不过,我们也可以决定如何在容器的主轴(对于 Row,是水平轴;对于 Column,是垂直轴)上放置可组合子项。

对于 Row,您可以选择以下排列方式:

行排列方式

对于 Column

列排列方式

除了这些排列方式之外,您还可以使用 Arrangement.spacedBy() 方法,在每个可组合子项之间添加固定间距。

在此示例中,您需要使用 spacedBy 方法,因为您需要在 LazyRow 中的各项之间留出 8dp 的间距。

import androidx.compose.foundation.layout.Arrangement

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

现在,设计如下所示:

“Align your body”预览

此外,您需要在 LazyRow 两侧添加一定尺寸的内边距。在此示例中,添加一个简单的内边距修饰符并不能达到目的。请尝试向 LazyRow 添加内边距,并使用互动式预览看看其行为方式:

“Align your body”红线

如您所见,滚动时,第一个和最后一个可见项在屏幕两侧被截断。

为了保持相同的内边距,同时确保在父级列表的边界内滚动内容时内容不会被截断,所有列表都需向 LazyRow 提供一个名为 contentPadding 的形参,并将其设置为 16.dp

确保可组合项在您滑动浏览列表时能够正常运行。您可以使用互动式预览与可组合项的预览互动。

import androidx.compose.foundation.layout.PaddingValues

@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   LazyRow(
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp),
       modifier = modifier
   ) {
       items(alignYourBodyData) { item ->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

请尝试使用互动式预览看看内边距带来的差异。

image.png

8. “Favorite collections”网格 - 延迟网格

接下来,您要实现的是屏幕的“Favorite collections”版块。这个可组合项需要的是网格,而不是单个行:

“Favorite Collections”滚动

您可以按照与上一部分类似的方式实现此版块,具体方法是创建一个 LazyRow,让各项都包含一个具有两个 FavoriteCollectionCard 实例的 Column。但在此步骤中,您需要使用 LazyHorizontalGrid,以便更好地将各项映射到网格元素。

首先实现包含两个固定行的简单网格:

import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       modifier = modifier
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text)
       }
   }
}

如您所见,只需将上一步中的 LazyRow 替换为 LazyHorizontalGrid 即可。不过,这样还无法得到正确的结果:

“Favorite Collections”预览

网格占用的空间与其父项相同,这意味着,“Favorite Collections”卡片会在垂直方向被过度拉伸。

调整可组合项,以便实现如下效果:

  • 网格的水平 contentPadding 为 16dp。

  • 水平和垂直排列的间距为 16dp。

  • 网格的高度为 168dp。

  • FavoriteCollectionCard 的修饰符将高度指定为 80dp。

最终代码应如下所示:

@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),
       horizontalArrangement = Arrangement.spacedBy(16.dp),
       verticalArrangement = Arrangement.spacedBy(16.dp),
       modifier = modifier.height(168.dp)
   ) {
       items(favoriteCollectionsData) { item ->
           FavoriteCollectionCard(item.drawable, item.text, Modifier.height(80.dp))
       }
   }
}

预览应如下所示:

“Favorite Collections”预览

9. 首页部分 - 槽位 API

在 MySoothe 主屏幕中,有多个版块都遵循同一模式。每个版块都有一个标题,其中包含的内容因版块而异。我们想要实现的红线设计如下所示:

首页部分红线

如您所见,每个版块都有一个标题和一个槽位。标题包含一些与其相关的间距和样式信息。可以使用不同的内容动态填充槽位,具体取决于版块。

如需实现这个灵活的版块容器,您可以使用所谓的槽位 API。在进行这项实现之前,请先阅读文档页面上有关基于槽位的布局的部分。这有助于您了解什么是基于槽位的布局,以及如何使用槽位 API 构建此类布局。

基于槽位的布局会在界面中留出空白区域,让开发者按照自己的意愿来填充。您可以使用它们创建更灵活的布局。

调整 HomeSection 可组合项以接收标题和槽位内容。您还应调整关联的预览,以调用这个包含“Align your body”标题及相关内容的 HomeSection

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(stringResource(title))
       content()
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body) {
           AlignYourBodyRow()
       }
   }
}

您可为可组合项的槽位使用 content 形参。这样一来,当您使用 HomeSection 可组合项时,便可使用尾随 lambda 填充内容槽位。当可组合项提供多个要填充的槽位时,您可为这些槽位指定有意义的名称,用于在更大的可组合项容器中代表其功能。例如,Material 的 TopAppBar 为 titlenavigationIcon 和 actions 提供槽位。

我们来看一下这个版块采用该实现后的效果:

首页部分预览

Text 可组合项需要更多信息才能与设计保持一致。

首页部分红线

更新可组合项,以便实现如下效果:

  • 采用 titleMedium 排版样式。

  • 文本基线与顶部的间距为 40dp。

  • 基线与元素底部的间距为 16dp。

  • 水平内边距为 16dp。

您的最终解决方案应如下所示:

@Composable
fun HomeSection(
   @StringRes title: Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   Column(modifier) {
       Text(
           text = stringResource(title),
           style = MaterialTheme.typography.titleMedium,
           modifier = Modifier
               .paddingFromBaseline(top = 40.dp, bottom = 16.dp)
               .padding(horizontal = 16.dp)
       )
       content()
   }
}

10. 主屏幕 - 滚动

现在,您已经创建了所有单独的构建块,接下来可以将它们组合成一个全屏实现了。

您尝试实现的设计如下所示:

首页部分红线

只需依序放置搜索栏和这两个版块。您需要添加一定尺寸的间距,以确保一切都契合设计。我们之前没有使用过 Spacer 可组合项,它可帮助我们在 Column 中添加额外的间距。如果您改为设置 Column 的内边距,便会看到之前在“Favorite Collections”网格中出现的相同截断行为。

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(modifier) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

虽然设计与大多数设备尺寸都非常契合,但如果设备的高度不足(例如在横屏模式下),设计需要能够垂直滚动。这就需要您添加滚动行为。

如前所述,LazyRow 和 LazyHorizontalGrid 等延迟布局会自动添加滚动行为。但是,您不一定总是需要延迟布局。一般来说,在列表中有许多元素或需要加载大型数据集时,您需要使用延迟布局,因此一次发出所有项不仅会降低性能,还会拖慢应用的运行速度。如果列表中的元素数量有限,您也可以选择使用简单的 Column 或 Row,然后手动添加滚动行为。为此,您可以使用 verticalScroll 或 horizontalScroll 修饰符。这些修饰符需要 ScrollState,后者包含当前的滚动状态,可用于从外部修改滚动状态。在此示例中,您不需要修改滚动状态,只需使用 rememberScrollState 创建一个持久的 ScrollState 实例。

如需详细了解 remember 及其在 Compose 状态中的角色,请完成“在 Compose 中使用状态”Codelab

最终结果应如下所示:

import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
   Column(
       modifier
           .verticalScroll(rememberScrollState())
   ) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body) {
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections) {
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))
   }
}

如需验证可组合项的滚动行为,请限制预览的高度,并在互动式预览中运行它:

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

屏幕内容滚动

11. 底部导航栏 - Material

现在,您已经实现了屏幕内容,可以开始添加窗口装饰了。就 MySoothe 而言,用户可以通过导航栏在不同的屏幕之间切换。

首先实现导航栏可组合项,然后将其添加到应用中。

下面我们来看一下设计:

底部导航栏设计

幸运的是,您无需自己从头开始实现整个可组合项。您可以使用 Compose Material 库中的 NavigationBar 可组合项。在 NavigationBar 可组合项内,您可以添加一个或多个 NavigationBarItem 元素,然后 Material 库会自动为其设置样式。

先从此底部导航栏的基本实现着手:

import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   NavigationBar(
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_home)
               )
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_profile)
               )
           },
           selected = false,
           onClick = {}
       )
   }
}

其基本实现过程如下所示(内容颜色和导航栏颜色的反差不明显):

底部导航栏预览

您应该进行一些样式调整。首先,您可以通过设置底部导航栏的 containerColor 形参来更新其背景颜色。为此,您可以使用 Material 主题中的 surfaceVariant 颜色。您的最终解决方案应如下所示:

@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   NavigationBar(
       containerColor = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_home))
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(stringResource(R.string.bottom_navigation_profile))
           },
           selected = false,
           onClick = {}
       )
   }
}

现在,导航栏应如下所示(请注意此设置带来的颜色反差如何):

底部导航栏设计

12. MySoothe 应用 - Scaffold

在这一步中,创建全屏实现,包含底部导航栏。使用 Material 的 Scaffold 可组合项。对于实现 Material Design 的应用,Scaffold 提供了可配置的顶级可组合项。它包含可用于各种 Material 概念的槽位,其中一个就是底部栏。在此底部栏中,您可以放置在上一步中创建的底部导航栏可组合项。

实现 MySootheAppPortrait() 可组合项。这是应用的顶级可组合项,因此您应该:

  • 应用 MySootheTheme Material 主题。

  • 添加 Scaffold

  • 将底部栏设置为 SootheBottomNavigation 可组合项。

  • 将内容设置为 HomeScreen 可组合项。

最终结果应如下所示:

import androidx.compose.material3.Scaffold

@Composable
fun MySootheAppPortrait() {
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

您的实现现已完成!如果想检查您实现的设计能否让像素完美呈现,可以将此图片与您自己的预览实现结果进行比较。

我的 MySoothe 实现结果

13. 导航栏 - Material

为应用创建布局时,您还需要注意它在手机的多个配置(包括横屏模式)下的显示效果。下面是应用在横屏模式下的设计,请注意底部导航栏是如何变成屏幕内容左侧的侧边栏的。

横向设计

如需实现此设计,您不仅要使用 Compose Material 库中的 NavigationRail 可组合项,还要有与用于创建底部导航栏的 NavigationBar 类似的实现方案。在 NavigationRail 可组合项内,您需要为主屏幕和个人资料添加 NavigationRailItem 元素。

底部导航栏设计

我们先从导航栏的基本实现入手。

import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
   ) {
       Column(
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )

           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

导航栏预览

您应该进行一些样式调整。

  • 在侧边栏的开头和末尾添加 8dp 的内边距。

  • 为此,请使用 Material 主题中的背景颜色设置导航栏的 containerColor 参数,以更新其背景颜色。通过设置背景颜色,图标和文本的颜色会自动适应主题的 onBackground 颜色。

  • 此列应填满最大高度。

  • 将列的垂直排列方式设为居中。

  • 将此列的水平对齐方式设置为水平居中。

  • 在两个图标之间添加 8dp 的内边距。

您的最终解决方案应如下所示:

import androidx.compose.foundation.layout.fillMaxHeight

@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   NavigationRail(
       modifier = modifier.padding(start = 8.dp, end = 8.dp),
       containerColor = MaterialTheme.colorScheme.background,
   ) {
       Column(
           modifier = modifier.fillMaxHeight(),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )
           Spacer(modifier = Modifier.height(8.dp))
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

导航栏设计

现在,我们将导航栏添加到横向布局中。

横向设计

对于应用的纵向版本,您使用的是 Scaffold。不过,在横向模式下,您需要使用 Row,并将导航栏和屏幕内容并排放置。

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Row {
           SootheNavigationRail()
           HomeScreen()
       }
   }
}

在纵向版本中使用 Scaffold 时,它还会负责将内容颜色设置为背景。如需设置导航栏的颜色,请将 Row 封装在 Surface 中,并将其设置为背景颜色。

@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Surface(color = MaterialTheme.colorScheme.background) {
           Row {
               SootheNavigationRail()
               HomeScreen()
           }
       }
   }
}

横向预览

14. MySoothe 应用 - 窗口大小

横屏模式的预览效果很理想。不过,如果您在设备或模拟器上运行应用时将其切换到横向模式,系统并不会显示横向版本。这是因为我们需要通知应用何时显示应用的哪个配置。为此,请使用 calculateWindowSizeClass() 函数查看手机当前所用的配置。

窗口大小示意图

窗口大小类别分为三种宽度:较小、中等和较大。应用处于竖屏模式时,使用较小宽度;处于横屏模式时,使用较大宽度。在此 Codelab 中,不会用到中等宽度。

在 MySootheApp 可组合项中,对其进行更新以接受设备的 WindowSizeClass。如果宽度较小,则传入应用的纵向版本。如果处于横向模式,则传入应用的横向版本。

import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
   when (windowSize.widthSizeClass) {
       WindowWidthSizeClass.Compact -> {
           MySootheAppPortrait()
       }
       WindowWidthSizeClass.Expanded -> {
           MySootheAppLandscape()
       }
   }
}

在 setContent() 中,创建一个名为 windowSizeClass 的值,并将其设置为 calculateWindowSize(),然后将其传入 MySootheApp()。

由于 calculateWindowSize() 仍处于实验阶段,因此您需要选择启用 ExperimentalMaterial3WindowSizeClassApi 类。

import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

class MainActivity : ComponentActivity() {
   @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           val windowSizeClass = calculateWindowSizeClass(this)
           MySootheApp(windowSizeClass)
       }
   }
}

现在,请在模拟器或设备上运行应用,并观察显示内容在旋转时如何变化。

应用的纵向版本

应用的横向版本

按照实验任务书的要求完成以下实验报告:

15. 实验报告

一、程序代码

/*
* Copyright 2022 The Android Open Source Project
*
* Licensed under the Apache License, Version .2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at

*
*     https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.codelab.basiclayouts

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
    import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basiclayouts.ui.theme.MySootheTheme
import androidx.compose.material3.TextField
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.ui.Alignment
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material.icons.filled.AccountCircle
import androidx.compose.material.icons.filled.Spa
import androidx.compose.material3.Scaffold
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass

class MainActivity : ComponentActivity() {
   @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           val windowSizeClass = calculateWindowSizeClass(this)
           MySootheApp(windowSizeClass)
       }
   }
}

// Step: Search bar - Modifiers
@Composable
fun SearchBar(
   modifier: Modifier = Modifier
) {
   // Implement composable here
   //SearchBar 可组合函数接受 modifier 形参,并将其传递给 TextField。
   TextField(
       value ="",
       onValueChange ={},
       leadingIcon = {//添加搜索图标。TextField 包含一个可接受其他可组合项的 leadingIcon形参
           Icon(
               imageVector =Icons.Default.Search,
               contentDescription = null
           )
       },
       colors = TextFieldDefaults.colors(//替换特定颜色
           unfocusedContainerColor = MaterialTheme.colorScheme.surface,//将文本字段的unfocusedContainerColor设置为 MaterialTheme 的surface颜色
           focusedContainerColor = MaterialTheme.colorScheme.surface//将文本字段的focusedContainerColor设置为 MaterialTheme 的surface颜色
       ),
       placeholder = {
            Text(stringResource(R.string.placeholder_search))//添加占位符文本“Search”(以字符串资源R.string.placeholder_search 的形式显示)
       },
       modifier = modifier
           .fillMaxWidth()//宽度使用 fillMaxWidth 修饰符.填充其父项的宽度
           .heightIn(min = 56.dp)//高度使用 heightIn 修饰符.搜索栏的高度应至少为 56dp
   )
}

// Step: Align your body - Alignment
@Composable
fun AlignYourBodyElement(
   @DrawableRes drawable: Int,
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   // Implement composable here
   Column(
       horizontalAlignment = Alignment.CenterHorizontally,//将Column的horizontalAlignment设置为水平居中
       modifier = modifier
   ){//一个Image和一个Text可组合项,添加到Column中
       Image(
           painter = painterResource(R.drawable.ab1_inversions),
           contentDescription = null,//因为这张图片是纯装饰性的,将图片的contentDescription设为 null
           contentScale = ContentScale.Crop,//使用Image的contentScale形参对图片进行正确缩放
           modifier = Modifier//使用size和clip修饰符以及contentScale形参调整Image可组合项。
               .size(88.dp)//图片大小
               .clip(CircleShape)//调整可组合项的外观(图片形状为圆形)
       )
       Text(text = stringResource(R.string.ab1_inversions),
           modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
           style = MaterialTheme.typography.bodyMedium
       )
   }
}

// Step: Favorite collection card - Material Surface
@Composable
fun FavoriteCollectionCard(
   @DrawableRes drawable: Int,//将图片和文本变为动态形式。将它们作为实参传入可组合函数
   @StringRes text: Int,
   modifier: Modifier = Modifier
) {
   // Implement composable here
   Surface(
       shape = MaterialTheme.shapes.medium,
       color = MaterialTheme.colorScheme.surfaceVariant,//将颜色更新为 surfaceVariant
       modifier = modifier
   ) {
       Row(
           verticalAlignment = Alignment.CenterVertically,//设置Row的宽度,并与其子项垂直对齐。根据图表设置图片尺寸,然后在容器中对其进行剪裁
           modifier = Modifier.width(255.dp)
       ) {
           Image(
               painter = painterResource(R.drawable.fc2_nature_meditations),
               contentDescription = null,
               contentScale = ContentScale.Crop,
               modifier = Modifier.size(80.dp)
           )
           Text(text = stringResource(R.string.fc2_nature_meditations),
               style = MaterialTheme.typography.titleMedium,//更新文本以使用 titleMedium 排版样式
               modifier = Modifier.padding(horizontal = 16.dp)//更新图片与文本之间的间距
           )
       }
   }
}

// Step: Align your body row - Arrangements
@Composable
fun AlignYourBodyRow(
   modifier: Modifier = Modifier
) {
   // Implement composable here
   LazyRow(
       modifier = modifier,
       horizontalArrangement = Arrangement.spacedBy(8.dp),
       contentPadding = PaddingValues(horizontal = 16.dp)
   ) {
       items(alignYourBodyData) { item->
           AlignYourBodyElement(item.drawable, item.text)
       }
   }
}

// Step: Favorite collections grid - LazyGrid
@Composable
fun FavoriteCollectionsGrid(
   modifier: Modifier = Modifier
) {
   // Implement composable here
   LazyHorizontalGrid(
       rows = GridCells.Fixed(2),
       contentPadding = PaddingValues(horizontal = 16.dp),//网格的水平 contentPadding 为 16dp
       horizontalArrangement = Arrangement.spacedBy(16.dp),//水平和垂直排列的间距为 16dp
       verticalArrangement = Arrangement.spacedBy(16.dp),
       modifier = modifier.height(168.dp)//网格的高度为 168dp。
   ) {
       items(favoriteCollectionsData) { item->
           FavoriteCollectionCard(item.drawable,item.text,Modifier.height(80.dp))//FavoriteCollectionCard 的修饰符将高度指定为 80dp。
       }
   }
}

// Step: Home section - Slot APIs
@Composable
fun HomeSection(
   @StringRes title:Int,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit//调整HomeSection可组合项以接收标题和槽位内容
) {
   // Implement composable here
   Column(modifier) {
       Text(
           text = stringResource(title),
           style = MaterialTheme.typography.titleMedium,//采用 titleMedium 排版样式。
           modifier = Modifier
               .paddingFromBaseline(
                   top = 40.dp,
                   bottom = 16.dp
               )//文本基线与顶部的间距为 40dp,基线与元素底部的间距为 16dp。
               .padding(horizontal = 16.dp)//水平内边距为 16dp。
       )
       content()
   }
}

// Step: Home screen - Scrolling
@Composable
fun HomeScreen(
   modifier: Modifier = Modifier
) {
   // Implement composable here
   Column(
       modifier
           .verticalScroll(rememberScrollState())//使用rememberScrollState创建一个持久的ScrollState实例
   ) {
       Spacer(Modifier.height(16.dp))
       SearchBar(Modifier.padding(horizontal = 16.dp))
       HomeSection(title = R.string.align_your_body){
           AlignYourBodyRow()
       }
       HomeSection(title = R.string.favorite_collections){
           FavoriteCollectionsGrid()
       }
       Spacer(Modifier.height(16.dp))//Spacer可组合项在Column中添加额外的间距
   }
}

// Step: Bottom navigation - Material
@Composable
private fun SootheBottomNavigation(modifier: Modifier = Modifier) {
   // Implement composable here
   NavigationBar(
       containerColor = MaterialTheme.colorScheme.surfaceVariant,
       modifier = modifier
   ) {
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.Spa,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_home)
               )
           },
           selected = true,
           onClick = {}
       )
       NavigationBarItem(
           icon = {
               Icon(
                   imageVector = Icons.Default.AccountCircle,
                   contentDescription = null
               )
           },
           label = {
               Text(
                   text = stringResource(R.string.bottom_navigation_profile)
               )
            },
           selected = false,
           onClick = {}
       )
   }
}

// Step: MySoothe App - Scaffold
@Composable
fun MySootheAppPortrait() {
   // Implement composable here
   MySootheTheme {
       Scaffold(
           bottomBar = { SootheBottomNavigation() }
       ) { padding ->
           HomeScreen(Modifier.padding(padding))
       }
   }
}

// Step: Bottom navigation - Material
@Composable
private fun SootheNavigationRail(modifier: Modifier = Modifier) {
   // Implement composable here
   NavigationRail(
       modifier = modifier.padding(start = 8.dp, end = 8.dp),
       containerColor = MaterialTheme.colorScheme.background,
   ) {
       Column(
           modifier = modifier.fillMaxHeight(),
           verticalArrangement = Arrangement.Center,
           horizontalAlignment = Alignment.CenterHorizontally
       ) {
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.Spa,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_home))
               },
               selected = true,
               onClick = {}
           )
           Spacer(modifier = Modifier.height(8.dp))
           NavigationRailItem(
               icon = {
                   Icon(
                       imageVector = Icons.Default.AccountCircle,
                       contentDescription = null
                   )
               },
               label = {
                   Text(stringResource(R.string.bottom_navigation_profile))
               },
               selected = false,
               onClick = {}
           )
       }
   }
}

// Step: Landscape Mode
@Composable
fun MySootheAppLandscape(){
   // Implement composable here
   MySootheTheme {
       Surface(color = MaterialTheme.colorScheme.background) {
           Row {
           SootheNavigationRail()
           HomeScreen()
           }
       }
   }
}

// Step: MySoothe App
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
   // Implement composable here
   when (windowSize.widthSizeClass) {
       WindowWidthSizeClass.Compact -> {
           MySootheAppPortrait()
       }
       WindowWidthSizeClass.Expanded -> {
           MySootheAppLandscape()
       }
   }
}

private val alignYourBodyData = listOf(
   R.drawable.ab1_inversions to R.string.ab1_inversions,
   R.drawable.ab2_quick_yoga to R.string.ab2_quick_yoga,
   R.drawable.ab3_stretching to R.string.ab3_stretching,
   R.drawable.ab4_tabata to R.string.ab4_tabata,
   R.drawable.ab5_hiit to R.string.ab5_hiit,
   R.drawable.ab6_pre_natal_yoga to R.string.ab6_pre_natal_yoga
).map { DrawableStringPair(it.first, it.second) }

private val favoriteCollectionsData = listOf(
   R.drawable.fc1_short_mantras to R.string.fc1_short_mantras,
   R.drawable.fc2_nature_meditations to R.string.fc2_nature_meditations,
   R.drawable.fc3_stress_and_anxiety to R.string.fc3_stress_and_anxiety,
   R.drawable.fc4_self_massage to R.string.fc4_self_massage,
   R.drawable.fc5_overwhelmed to R.string.fc5_overwhelmed,
   R.drawable.fc6_nightly_wind_down to R.string.fc6_nightly_wind_down
).map { DrawableStringPair(it.first, it.second) }

private data class DrawableStringPair(
   @DrawableRes val drawable: Int,
   @StringRes val text: Int
)

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun SearchBarPreview() {
   MySootheTheme { SearchBar(Modifier.padding(8.dp)) }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyElementPreview() {
   MySootheTheme {
       AlignYourBodyElement(
           text = R.string.ab1_inversions,
           drawable = R.drawable.ab1_inversions,
           modifier = Modifier.padding(8.dp)
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionCardPreview() {
   MySootheTheme {
       FavoriteCollectionCard(
           text = R.string.fc2_nature_meditations,
           drawable = R.drawable.fc2_nature_meditations,
           modifier = Modifier.padding(8.dp)
       )
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun FavoriteCollectionsGridPreview() {
   MySootheTheme { FavoriteCollectionsGrid() }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun AlignYourBodyRowPreview() {
   MySootheTheme { AlignYourBodyRow() }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun HomeSectionPreview() {
   MySootheTheme {
       HomeSection(R.string.align_your_body){//调用这个包含“Align your body”标题及相关内容
           AlignYourBodyRow()
       }
   }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE, heightDp = 180)
@Composable
fun ScreenContentPreview() {
   MySootheTheme { HomeScreen() }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun BottomNavigationPreview() {
   MySootheTheme { SootheBottomNavigation(Modifier.padding(top = 24.dp)) }
}

@Preview(showBackground = true, backgroundColor = 0xFFF5F0EE)
@Composable
fun NavigationRailPreview() {
   MySootheTheme { SootheNavigationRail() }
}

@Preview(widthDp = 360, heightDp = 640)
@Composable
fun MySoothePortraitPreview() {
   MySootheAppPortrait()
}

@Preview(widthDp = 640, heightDp = 360)
@Composable
fun MySootheLandscapePreview() {
   MySootheAppLandscape()
}

二、  实验结果(含程序运行截图)

1. 搜索栏 - 修饰符

(1)SearchBarPreview 方法会渲染我们的 SearchBar 可组合项,并提供一些背景和内边距

新的图片

(2)在 更改可组合项的尺寸、布局、行为和外观;添加信息;处理用户输入

新的图片

2. Align your body版块 - 对齐

(1)添加图片背景(以图片资源 R.drawable.ab1_inversions 的形式显示);添加占位符文本“Search”(以图片资源 R.string.placeholder_search 的形式显示)

新的图片

(2)使用 size 和 clip 修饰符以及 contentScale 形参调整 Image 可组合项

新的图片

(3)使用 Image 的 contentScale 形参进行正确缩放

新的图片

(4)设置 Column 的horizontalAlignment 设置水平对齐文本

新的图片

(5)“Align your body”元素,显示在可水平滚动的行中

新的图片

3. “Favorite Collections”版块 - Material Surface

(1)使用Surface 可组合项为“Favorite Collections”卡片指定这些属性;将形状设置为 Shape(圆角)

(2)设置 Row 的宽度,并与其子项垂直对齐;设置图片尺寸,然后在容器中对其进行剪裁

新的图片

(3)“Favorite Collections”卡片,显示在可水平滚动的网格中;将颜色更新为 surfaceVariant;更新文本以使用 titleMedium 排版样式;更新图片与文本之间的间距

新的图片

4. “Align your body”行 - 排列

(1)使用 LazyRow 可组合项实现这种可滚动行

新的图片

(2)在 LazyRow 两侧添加一定尺寸的内边距;滚动时,第一个和最后一个可见项在屏幕两侧被截断

新的图片

(3)使用互动式预览与可组合项的预览互动

新的图片

4. “Favorite collections”网格 - 延迟网格

(1)使用 LazyHorizontalGrid,以便更好地将各项映射到网格元素(网格占用的空间与其父项相同,这意味着,“Favorite Collections”卡片会在垂直方向被过度拉伸)

新的图片

(2)调整可组合项(网格的水平 contentPadding 为 16dp;水平和垂直排列的间距为 16dp;网格的高度为 168dp;FavoriteCollectionCard 的修饰符将高度指定为 80dp)

新的图片

5. 首页部分 - 槽位 API

    调整 HomeSection 可组合项以接收标题和槽位内容;更新可组合项(采用 titleMedium 排版样式;文本基线与顶部的间距为 40dp;基线与元素底部的间距为 16dp;水平内边距为 16dp)

新的图片

6. 主屏幕 - 滚动

    使用过 Spacer 可组合项在 Column 中添加额外的间距

新的图片

7. 底部导航栏 - Material

    使用 Compose Material 库中的 NavigationBar 可组合项(添加一个或多个 NavigationBarItem 元素,然后 Material 库会自动为其设置样式),通过设置底部导航栏的 containerColor 形参来更新其背景颜色为surfaceVariant

8. MySoothe 应用 - Scaffold

    应用 MySootheTheme Material 主题;添加 Scaffold;将底部栏设置为 SootheBottomNavigation 可组合项;将内容设置为 HomeScreen 可组合项

新的图片

9. 导航栏 - Material

(1)导航栏的基本实现用于创建底部导航栏的 NavigationBar(在侧边栏的开头和末尾添加 8dp 的内边距。使用 Material 主题中的背景颜色设置导航栏的 containerColor 参数,以更新其背景颜色。通过设置背景颜色,图标和文本的颜色会自动适应主题的 onBackground 颜色;此列应填满最大高度;将列的垂直排列方式设为居中;将此列的水平对齐方式设置为水平居中;在两个图标之间添加 8dp 的内边距)

(2)将导航栏添加到横向布局中。使用 Row,并将导航栏和屏幕内容并排放置;使用 Compose Material 库中的 NavigationRail 可组合项(需要为主屏幕和个人资料添加 NavigationRailItem 元素)

新的图片

10. MySoothe 应用 - 窗口大小

用 calculateWindowSizeClass() 函数查看手机当前所用的配置

在 MySootheApp 可组合项中,对其进行更新以接受设备的 WindowSizeClass。如果宽度较小,则传入应用的纵向版本。如果处于横向模式,则传入应用的横向版本。

新的图片

新的图片

三、 出现问题及解决方法

问题1:“Align your body”行 - 版块的图片为同一个:R.string.ab1_inversions;“Favorite Collections”网格 - 延迟网络板块的图片为同一个:fc2_nature_meditations。

解决方法:在“Align your body”版块的AlignYourBodyElementh函数中,没有进行正确调用 drawable ,在Image可组合项中的painter修饰符,用于获取一个与指定 drawable 资源相关的 painter 对象。

在“Favorite Collections”版块的FavoriteCollectionCardhh桉树中,没有正确调用 drawable,解决方法上同。

新的图片

新的图片

四、  实验心得

搜索栏 - 修饰符

    使用名为文本字段的 Material 组件,Compose Material 库包含一个名为 TextField 的可组合项,是此 Material 组件的实现

1. 名为 SearchBar 的可组合项内,编写基本的 TextField 实现

(1)SearchBar 可组合函数接受 modifier 形参,并将其传递给 TextField

(2)对文本字段的值进行了硬编码,并且 onValueChange 回调不会执行任何操作

(3)SearchBarPreview 方法会渲染我们的 SearchBar 可组合项,并提供一些背景和内边距

(4)使用修饰符修正可组合项的尺寸

2. 更改可组合项的尺寸、布局、行为和外观。(使用 heightIn 修饰符。这可确保该可组合项具有特定的最小高度;使用 fillMaxWidth 修饰符。此修饰符可确保搜索栏占用其父项的所有水平空间)

    添加信息。

    处理用户输入。

    添加高级互动,例如使元素可点击、可滚动、可拖动或可缩放。

 3. 添加搜索图标。TextField 包含一个可接受其他可组合项的 leadingIcon 形参。在其中设置 Icon,使用正确的 Compose Icon 导入 Search 图标。

4. 使用 TextFieldDefaults.textFieldColors 替换特定颜色。将文本字段的 focusedContainerColor 和 unfocusedContainerColor 设置为 MaterialTheme 的 surface 颜色。

5. 添加占位符文本“Search”(以字符串资源 R.string.placeholder_search 的形式显示)。

 Align your body - 对齐

    在AlignYourBodyElement 可组合项中需要一个 Image 和一个 Text 可组合项。您还需要将它们添加到 Column 中,以便一个位于另一个下方。

1. 无需为图片添加额外的说明,将图片的 contentDescription 设为 null

2. 使用 size 和 clip 修饰符以及 contentScale 形参调整 Image 可组合项

(1)size 修饰符会调整可组合项以适应特定尺寸;、

(2)clip 修饰符用于调整可组合项的外观(CircleShape、Fir、FillBounds、Crop……)。

3. 设置 Column 的对齐方式来水平居中对齐文本:CenterHorizontally

(    Column 可决定其子项的水平对齐方式Start、CenterHorizontally、End;

        Row,可设置垂直对齐(Top、CenterVertically、Bottom);

        Box,可同时使用水平对齐和垂直对齐TopStart、TopCenter、TopEnd、CenterStart、Center、CenterEnd、BottomStart、BottomCenter、BottomEnd    )

4. 将图片和文本变为动态形式。将它们作为实参传递给可组合函数。别忘了更新相应的预览,并传入一些经过硬编码的数据。

5. 更新文本以使用 bodyMedium 排版样式。

6. 根据图表更新文本元素的基线间距。

    图片高度应为 88dp;文本基线与图片之间的间距应为 24dp;基线与元素底部的间距应为 8dp;文本的排版样式应为 bodyMedium。

7. 使用 Spacer 或设置内边距

“Favorite collection”卡片 - Material Surface

1. 文本的排版样式应为 titleMedium。

2. 此容器使用 surfaceVariant 作为其背景颜色(带有圆角)。

3. 调整 Surface(使用 shape 形参,将形状设置为 Shape)

4. 设置 Row 的宽度,并与其子项垂直对齐。

5. 根据图表设置图片尺寸,然后在容器中对其进行剪裁

6. 将图片和文本变为动态形式。将它们作为实参传入可组合函数。

7. 将颜色更新为 surfaceVariant。

8. 更新文本以使用 titleMedium 排版样式。

9. 更新图片与文本之间的间距。

“Align your body”行 - 排列

1. 使用 LazyRow 可组合项实现这种可滚动行。(延迟列表: LazyRow -渲染屏幕上显示的元素 和  LazyColumn)

2. 使用 spacedBy 方法,在 LazyRow 中的各项之间留出 8dp 的间距

3. 在 LazyRow 两侧添加一定尺寸的内边距

4. 向 LazyRow 提供一个名为 contentPadding 的形参,并将其设置为 16.dp

“Favorite collections”网格 - 延迟网格

1. 使用 LazyHorizontalGrid,以便更好地将各项映射到网格元素(也可以创建一个 LazyRow,让各项都包含一个具有两个 FavoriteCollectionCard 实例的 Column)

(1)首先实现包含两个固定行的简单网格:LazyHorizontalGrid

(2)网格占用的空间与其父项相同,“Favorite Collections”卡片在垂直方向被过度拉伸

3. 调整可组合项

(1)网格的水平 contentPadding 为 16dp。

(2)水平和垂直排列的间距为 16dp。

(3)网格的高度为 168dp。

(4)FavoriteCollectionCard 的修饰符将高度指定为 80dp。

首页部分 - 槽位 API

    每个版块都有一个标题和一个槽位。标题包含一些与其相关的间距和样式信息。可以使用不同的内容动态填充槽位,具体取决于版块

1. 调整 HomeSection 可组合项以接收标题和槽位内容。(还应调整关联的预览,以调用这个包含“Align your body”标题及相关内容的 HomeSection)

2. 可为可组合项的槽位使用 content 形参。在使用 HomeSection 可组合项时,便可使用尾随 lambda 填充内容槽位。

3. 更新可组合项。(采用 titleMedium 排版样式、文本基线与顶部的间距为 40dp、基线与元素底部的间距为 16dp、水平内边距为 16dp)

主屏幕 - 滚动

1. 只需依序放置搜索栏和这两个版块。但需添加一定尺寸的间距(使用过 Spacer 可组合项,可在 Column 中添加额外的间距。如果改为设置 Column 的内边距,便会看到之前在“Favorite Collections”网格中出现的相同截断行为)

2. LazyRow 和 LazyHorizontalGrid 等延迟布局会自动添加滚动行为,如果列表中的元素数量有限,也可以选择使用简单的 Column 或 Row,然后手动添加滚动行为。

(1)使用 verticalScroll 或 horizontalScroll 修饰符。这些修饰符需要 ScrollState,后者包含当前的滚动状态,可用于从外部修改滚动状态。在此示例中,不需要修改滚动状态,只需使用 rememberScrollState 创建一个持久的 ScrollState 实例。

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值