Compose 中的基本布局

Compose 中的基本布局

1. 简介

学习内容

在此 Codelab 中,您将学习:

  • 如何借助修饰符扩充可组合项?
  • 如何通过 Column 和 LazyRow 等标准布局组件定位可组合子项?
  • 如何通过对齐方式和排列方式更改可组合子项在父项中的位置?
  • 如何借助 Scaffold 和 Bottom Navigation 等 Material 可组合项创建详细布局?
  • 如何使用槽位 API 构建灵活的可组合项?
  • 如何为不同的屏幕配置构建布局?

构建内容

2. 从制定计划着手

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

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

3. 搜索栏 - 修饰符

第一个要转换为可组合项的元素是搜索栏。

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

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

需要注意以下几点:

  • 您已对文本字段的值进行了硬编码,并且 onValueChange 回调不会执行任何操作。

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

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

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

  • 添加搜索图标。TextField 包含一个可接受其他可组合项的 leadingIcon 形参。您可以在其中设置 Icon,在此示例中应为 Search 图标。请务必使用正确的 Compose Icon 导入。
  • 您可以使用 TextFieldDefaults.colors 替换特定颜色。将文本字段的 focusedContainerColorunfocusedContainerColor 设置为 MaterialTheme 的 surface 颜色。
  • 添加占位符文本"Search"(以字符串资源 R.string.placeholder_search 的形式显示)。
kotlin 复制代码
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 数据类的副本,从而通过该类仅更新不同的颜色。在此示例中,即仅更新 unfocusedContainerColorfocusedContainerColor 颜色。

4. Align your body - 对齐

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

size 修饰符会调整可组合项以适应特定尺寸。

clip 修饰符的工作原理有所不同,用于调整可组合项的外观 。您可以将该修饰符设置为任何 Shape,然后它会按照相应形状对可组合项的内容进行裁剪。

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

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

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

  • Start
  • CenterHorizontally
  • End

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

  • Top
  • CenterVertically
  • Bottom

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

  • TopStart
  • TopCenter
  • TopEnd
  • CenterStart
  • Center
  • CenterEnd
  • BottomStart
  • BottomCenter
  • BottomEnd

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

kotlin 复制代码
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)
       )
   }
}

5. "Favorite collection"卡片 - Material Surface

您可以看到,文本的排版样式应为 titleMedium。

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

Surface 是 Compose Material 库中的一个组件。该组件遵循常规的 Material Design 模式,您可以通过更改应用主题来调整它。如需详细了解主题设置,请查看"Compose 中的主题设置"Codelab 或参阅主题设置文档

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

kotlin 复制代码
@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)
       )
   }
}

6. "Align your body"行 - 排列

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

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

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

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

对于 Column

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

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

此外,您需要在 LazyRow 两侧添加一定尺寸的内边距。在此示例中,添加一个简单的内边距修饰符并不能达到目的。滚动时,第一个和最后一个可见项在屏幕两侧被截断。

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

kotlin 复制代码
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)
       }
   }
}

7. "Favorite collections"网格 - 延迟网格

您需要使用 LazyHorizontalGrid,以便更好地将各项映射到网格元素。

kotlin 复制代码
@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))
       }
   }
}

8. 首页部分 - 槽位 API

在 MySoothe 主屏幕中,有多个版块 都遵循同一模式。每个版块都有一个标题,其中包含的内容因版块而异。我们想要实现的设计如下所示: 每个版块都有一个标题 和一个槽位。标题包含一些与其相关的间距和样式信息。可以使用不同的内容动态填充槽位,具体取决于版块。

如需实现这个灵活的版块容器,您可以使用所谓的槽位 API。

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

调整 HomeSection 可组合项以接收标题和槽位内容。

kotlin 复制代码
@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()
   }
}

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

9. 主屏幕 - 滚动

我们之前没有使用过 Spacer 可组合项,它可帮助我们在 Column 中添加额外的间距。如果您改为设置 Column 的内边距,便会看到之前在"Favorite Collections"网格中出现的相同截断行为。

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

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

kotlin 复制代码
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))
   }
}

10. 底部导航栏 - Material

您可以使用 Compose Material 库中的 NavigationBar 可组合项。在 NavigationBar 可组合项内,您可以添加一个或多个 NavigationBarItem 元素,然后 Material 库会自动为其设置样式。

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

kotlin 复制代码
@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 = {}
       )
   }
}

11. MySoothe 应用 - Scaffold

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

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

  • 应用 MySootheTheme Material 主题。
  • 添加 Scaffold
  • 将底部栏设置为 SootheBottomNavigation 可组合项。
  • 将内容设置为 HomeScreen 可组合项。
kotlin 复制代码
import androidx.compose.material3.Scaffold

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

12. 导航栏 - Material

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

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

kotlin 复制代码
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,并将导航栏和屏幕内容并排放置。

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

kotlin 复制代码
@Composable
fun MySootheAppLandscape() {
   MySootheTheme {
       Surface(color = MaterialTheme.colorScheme.background) {
           Row {
               SootheNavigationRail()
               HomeScreen()
           }
       }
   }
}

13. MySoothe 应用 - 窗口大小

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

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

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

kotlin 复制代码
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
   when (windowSize.widthSizeClass) {
       WindowWidthSizeClass.Compact -> {
           MySootheAppPortrait()
       }
       WindowWidthSizeClass.Medium, WindowWidthSizeClass.Expanded -> {
           MySootheAppLandscape()
       }
   }
}

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

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

kotlin 复制代码
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass

class MySootheActivity : ComponentActivity() {
    @OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            val windowSizeClass = calculateWindowSizeClass(this)
            MySootheApp(windowSizeClass)
        }
    }
}
kotlin 复制代码
package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
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.AccountCircle
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Spa
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationRail
import androidx.compose.material3.NavigationRailItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.ui.unit.dp
import com.example.basicscodelab.ui.theme.MySootheTheme

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

@Composable
fun MySootheApp(windowSize: WindowSizeClass) {
    when (windowSize.widthSizeClass) {
        WindowWidthSizeClass.Compact -> {
            MySootheAppPortrait()
        }

        WindowWidthSizeClass.Medium, WindowWidthSizeClass.Expanded -> {
            MySootheAppLandscape()
        }
    }

}

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

@Composable
fun MySootheAppLandscape() {
    MySootheTheme {
        Scaffold { paddingValues ->
            Surface(
                color = MaterialTheme.colorScheme.background,
                modifier = Modifier.padding(paddingValues)
            ) {
                Row {
                    SootheNavigationRail()
                    HomeScreen()
                }
            }
        }

    }
}

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

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

@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
            .heightIn(56.dp)
            .fillMaxWidth()
    )
}

@Composable
fun AlignYourBodyElement(
    @DrawableRes drawable: Int,
    @StringRes text: Int,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Image(
            painter = painterResource(drawable),
            contentDescription = null,
            modifier = Modifier
                .size(88.dp)
                .clip(CircleShape),
            contentScale = ContentScale.Crop
        )

        Text(
            text = stringResource(text),
            modifier = Modifier.paddingFromBaseline(top = 24.dp, bottom = 8.dp),
            style = MaterialTheme.typography.bodyMedium
        )
    }
}

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

@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)
            )
        }
    }

}

@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(
                drawable = item.drawable,
                text = item.text,
                modifier = Modifier.height(80.dp)
            )
        }
    }
}

@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(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 = {}
        )
    }
}


@Composable
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(text = stringResource(R.string.bottom_navigation_home))
                },
                selected = true,
                onClick = {}
            )

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

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
)
相关推荐
掘金安东尼5 分钟前
解读 hidden=until-found 属性
前端·javascript·面试
1024小神13 分钟前
jsPDF 不同屏幕尺寸 生成的pdf不一致,怎么解决
前端·javascript
滕本尊13 分钟前
构建可扩展的 DSL 驱动前端框架:从 CRUD 到领域模型的跃迁
前端·全栈
借月14 分钟前
高德地图绘制工具全解析:线路、矩形、圆形、多边形绘制与编辑指南 🗺️✏️
前端·vue.js
li理14 分钟前
NavPathStack 是鸿蒙 Navigation 路由的核心控制器
前端
二闹17 分钟前
一招帮你记住上次读到哪儿了?
前端
li理21 分钟前
核心概念:Navigation路由生命周期是什么
前端
古夕24 分钟前
my-first-ai-web_问题记录02:Next.js 15 动态路由参数处理
前端·javascript·react.js
梦里寻码24 分钟前
自行食用 uniapp 多端 手写签名组件
前端·uni-app
前端小白199526 分钟前
面试取经:工程化篇-webpack性能优化之热替换
前端·面试·前端工程化