Jetpack Compose 基础

1. 开始之前

Jetpack Compose 是一款旨在简化 UI 开发的现代工具包。它将响应式编程模型与 Kotlin 编程语言的简洁性和易用性相结合。它是完全声明式的,这意味着您可以通过调用一系列将数据转换为 UI 层次结构的函数来描述您的 UI。当底层数据发生变化时,框架会自动重新执行这些函数,为您更新 UI 层次结构。

一个 Compose 应用由可组合函数组成 - 只是用 @Composable 标记的常规函数,它可以调用其他可组合函数。一个函数就是创建新 UI 组件所需的一切。该注释告诉 Compose 为该函数添加特殊支持,以便随着时间的推移更新和维护您的 UI。Compose 允许您将代码结构化为小块。可组合函数通常简称为“可组合项”。

通过创建小型可重用的可组合项,可以轻松构建应用中使用的 UI 元素库。每个元素负责屏幕的一部分,并且可以独立编辑。

为了在您完成本 Codelab 时获得更多支持,请查看以下代码演练

注意:代码演练使用 Material 2,而 Codelab 已更新为使用 Material 3。请注意,在某些步骤中它们会有所不同。

先决条件

  • 熟悉 Kotlin 语法,包括 Lambda 表达式

您将做什么

在本 Codelab 中,您将学习

  • 什么是 Compose
  • 如何使用 Compose 构建 UI
  • 如何在可组合函数中管理状态
  • 如何创建高性能列表
  • 如何添加动画
  • 如何设置应用的样式和主题

您将构建一个包含入门屏幕和动画展开项列表的应用

8d24a786bfe1a8f2.gif

您需要什么

2. 创建新的 Compose 项目

要创建新的 Compose 项目,请打开 Android Studio。

如果您在“欢迎使用 Android Studio”窗口中,请点击“启动新的 Android Studio 项目”。如果您已打开 Android Studio 项目,请从菜单栏中选择“文件 > 新建 > 新建项目”。

对于新项目,请从可用模板中选择“空活动”。

d12472c6323de500.png

点击“下一步”并照常配置您的项目,将其命名为“Basics Codelab”。请确保您选择至少为 API 级别 21 的minimumSdkVersion,这是 Compose 支持的最低 API 级别。

选择“空活动”模板时,将在您的项目中为您生成以下代码

  • 该项目已配置为使用 Compose。
  • 已创建 AndroidManifest.xml 文件。
  • build.gradle.ktsapp/build.gradle.kts 文件包含 Compose 所需的选项和依赖项。

项目同步后,打开 MainActivity.kt 并查看代码。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting("Android")
    }
}

在下一节中,您将了解每个方法的作用,以及如何改进它们以创建灵活且可重用的布局。

Codelab 的解决方案

您可以从 GitHub 获取本 Codelab 解决方案的代码

$ git clone https://github.com/android/codelab-android-compose

或者,您可以将存储库下载为 Zip 文件

您将在 BasicsCodelab 项目中找到解决方案代码。我们建议您按照自己的节奏逐步完成 Codelab,并在必要时查看解决方案。在 Codelab 期间,将向您展示需要添加到项目的代码片段。

3. Compose 入门

浏览 Android Studio 为您生成的与 Compose 相关的不同类和方法。

可组合函数

可组合函数是用 @Composable 注释的常规函数。这使您的函数能够在其内部调用其他 @Composable 函数。您可以看到 Greeting 函数是如何标记为 @Composable 的。此函数将生成一段显示给定输入 String 的 UI 层次结构。 Text 是库提供的可组合函数。

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}

Android 应用中的 Compose

在 Compose 中,Activity 仍然是 Android 应用的入口点。在我们的项目中,当用户打开应用时会启动 MainActivity(如 AndroidManifest.xml 文件中所指定)。您可以使用 setContent 定义您的布局,但与在传统的 View 系统中使用 XML 文件不同,您可以在其中调用可组合函数。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                  modifier = Modifier.fillMaxSize(),
                  color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Android")
                }
            }
        }
    }
}

BasicsCodelabTheme 是一种设置可组合函数样式的方法。您将在“设置应用主题”部分中了解有关此内容的更多信息。要查看文本如何在屏幕上显示,您可以 either 在模拟器或设备上运行应用,或者使用 Android Studio 预览。

要使用 Android Studio 预览,您只需使用 @Preview 注释标记任何不带参数的可组合函数或具有默认参数的函数,并构建您的项目。您已经在 MainActivity.kt 文件中看到了一个 Preview Composable 函数。您可以在同一个文件中拥有多个预览并为其命名。

@Preview(showBackground = true, name = "Text preview")
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greeting(name = "Android")
    }
}

fb011e374b98ccff.png

如果选择了“代码”eeacd000622ba9b.png,则预览可能不会出现。点击“拆分”7093def1e32785b2.png 以查看预览。

4. 微调 UI

让我们首先为 Greeting 设置不同的背景颜色。您可以通过使用 Surface 包装 Text 可组合项来实现此目的。 Surface 接受颜色,因此请使用MaterialTheme.colorScheme.primary

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier
        )
    }
}

嵌套在 Surface 内部的组件将在该背景颜色之上绘制。

您可以在预览中查看新更改

c88121ec49bde8c7.png

您可能错过了重要细节:文本现在是白色的。我们什么时候定义的?

您没有!Material 组件(例如 androidx.compose.material3.Surface)旨在通过处理应用中可能需要的常见功能来改善您的体验,例如为文本选择合适的颜色。我们说 Material 是有主见的,因为它提供了大多数应用通用的良好默认值和模式。Compose 中的 Material 组件构建在其他基础组件(在 androidx.compose.foundation 中)之上,如果您需要更多灵活性,也可以从您的应用组件访问这些组件。

在本例中,Surface 理解,当背景设置为 primary 颜色时,其上的任何文本都应使用 onPrimary 颜色,该颜色也在主题中定义。您可以在“设置应用主题”部分中详细了解此内容。

修饰符

大多数 Compose UI 元素(例如 SurfaceText)都接受可选的 modifier 参数。修饰符告诉 UI 元素如何在父布局中布局、显示或行为。您可能已经注意到 Greeting 可组合项已经有一个默认修饰符,然后将其传递给 Text

例如,padding 修饰符将在其修饰的元素周围应用一定量的空间。您可以使用 Modifier.padding() 创建填充修饰符。您还可以通过链接多个修饰符来添加它们,因此在我们的例子中,我们可以将填充修饰符添加到默认修饰符中:modifier.padding(24.dp)

现在,在屏幕上的 Text 上添加填充

import androidx.compose.foundation.layout.padding
import androidx.compose.ui.unit.dp
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

ef14f7c54ae7edf.png

有数十种修饰符可用于对齐、动画、布局、使其可点击或可滚动、转换等。有关完整列表,请查看Compose 修饰符列表。您将在后续步骤中使用其中一些修饰符。

5. 重用可组合项

在 UI 中添加的组件越多,创建的嵌套级别就越多。如果函数变得非常大,这可能会影响可读性。通过创建小的可重用组件,可以轻松构建应用程序中使用的 UI 元素库。每个组件负责屏幕的一小部分,并且可以独立编辑。

最佳实践是,您的函数应包含一个 Modifier 参数,该参数默认分配一个空 Modifier。将此修饰符转发到您在函数内部调用的第一个可组合项。这样,调用方就可以从可组合函数外部调整布局指令和行为。

创建一个名为 MyApp 的可组合项,其中包含问候语。

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

这使您可以清理 onCreate 回调和预览,因为您现在可以重用 MyApp 可组合项,避免代码重复。

在预览中,让我们调用 MyApp 并删除预览的名称。

您的 MainActivity.kt 文件应如下所示

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
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.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(
        modifier = modifier,
        color = MaterialTheme.colorScheme.background
    ) {
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Text(
            text = "Hello $name!",
            modifier = modifier.padding(24.dp)
        )
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. 创建列和行

Compose 中的三个基本标准布局元素是 ColumnRowBox

518dbfad23ee1b05.png

它们是接受可组合内容的可组合函数,因此您可以在其中放置项目。例如,Column 中的每个子项都将垂直放置。

// Don't copy over
Column {
    Text("First row")
    Text("Second row")
}

现在尝试更改 Greeting,使其显示一个包含两个文本元素的列,如本例所示

bf27ee688c3231df.png

请注意,您可能需要调整填充。

将您的结果与以下解决方案进行比较

import androidx.compose.foundation.layout.Column
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(color = MaterialTheme.colorScheme.primary) {
        Column(modifier = modifier.padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

Compose 和 Kotlin

可组合函数可以像 Kotlin 中的任何其他函数一样使用。这使得构建 UI 非常强大,因为您可以添加语句来影响 UI 的显示方式。

例如,您可以使用 for 循环将元素添加到 Column

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

a7ba2a8cb7a7d79d.png

您尚未设置尺寸或添加任何约束到可组合项的大小,因此每行都占用其可以占用的最小空间,预览也执行相同的操作。让我们更改我们的预览以模拟小型手机的常用宽度 320dp。向 @Preview 注释添加 widthDp 参数,如下所示

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

a5d5f6cdbdd918a2.png

修饰符在 Compose 中被广泛使用,所以让我们通过一个更高级的练习来练习:尝试使用 fillMaxWidthpadding 修饰符复制以下布局。

a9599061cf49a214.png

现在将您的代码与解决方案进行比较

import androidx.compose.foundation.layout.fillMaxWidth

@Composable
fun MyApp(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Column(modifier = Modifier.fillMaxWidth().padding(24.dp)) {
            Text(text = "Hello ")
            Text(text = name)
        }
    }
}

请注意

  • 修饰符可以有多个重载,例如,您可以指定创建填充的不同方法。
  • 要将多个修饰符添加到一个元素,只需将它们链接起来。

有多种方法可以实现此结果,因此,如果您的代码与本代码片段不匹配,并不意味着您的代码错误。但是,复制并粘贴此代码以继续学习此代码实验室。

添加按钮

在下一步中,您将添加一个可点击的元素来扩展 Greeting,因此我们需要先添加该按钮。目标是创建以下布局

ff2d8c3c1349a891.png

Button 是 material3 包提供的可组合项,它将可组合项作为最后一个参数。由于 尾随 lambda 表达式 可以移动到括号之外,因此您可以将任何内容作为子项添加到按钮中。例如,一个 Text

// Don't copy yet
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

为了实现这一点,您需要学习如何在行的末尾放置一个可组合项。没有 alignEnd 修饰符,因此,您可以为开头的可组合项赋予一些 weightweight 修饰符使元素填充所有可用空间,使其变得灵活,有效地推开没有权重的其他元素,这些元素称为不灵活元素。它还使 fillMaxWidth 修饰符变得多余。

现在尝试添加按钮并将其放置在上一张图片中所示的位置。

在此处查看解决方案

import androidx.compose.foundation.layout.Row
import androidx.compose.material3.ElevatedButton
// ...

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { /* TODO */ }
            ) {
                Text("Show more")
            }
        }
    }
}

7. Compose 中的状态

在本节中,您将向屏幕添加一些交互。到目前为止,您已经创建了静态布局,但现在您将使它们对用户更改做出反应以实现这一点

6675d41779cac69.gif

在深入了解如何使按钮可点击以及如何调整项目大小之前,您需要将某些值存储在某个地方,以指示每个项目是否已展开——项目的状态。由于我们需要为每个问候语提供一个这样的值,因此逻辑位置是在 Greeting 可组合项中。请查看此 expanded 布尔值以及它在代码中的使用方式

// Don't copy over
@Composable
fun Greeting(name: String) {
    var expanded = false // Don't do this!

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

请注意,我们还添加了一个 onClick 操作和一个动态按钮文本。稍后将详细介绍。

但是,这不会按预期工作。为 expanded 变量设置不同的值不会使 Compose 将其检测为状态更改,因此不会发生任何事情。

修改此变量不会触发重新组合的原因是Compose 并未跟踪它。此外,每次调用 Greeting 时,变量都将重置为 false。

要向可组合项添加内部状态,您可以使用 mutableStateOf 函数,这使得 Compose 重新组合读取该 State 的函数。

import androidx.compose.runtime.mutableStateOf
// ...

// Don't copy over
@Composable
fun Greeting() {
    val expanded = mutableStateOf(false) // Don't do this!
}

但是,您不能只是将 mutableStateOf 分配给可组合项内的变量。如前所述,重新组合可以在任何时间发生,这将再次调用可组合项,并将状态重置为一个新的可变状态,其值为 false

为了在重新组合中保留状态,请使用 remember 记住可变状态。

import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

@Composable
fun Greeting(...) {
    val expanded = remember { mutableStateOf(false) }
    // ...
}

remember 用于保护免受重新组合的影响,因此状态不会重置。

请注意,如果您从屏幕的不同部分调用相同可组合项,则会创建不同的 UI 元素,每个元素都有其自己的状态版本。您可以将内部状态视为类中的私有变量。

可组合函数将自动“订阅”状态。如果状态发生更改,读取这些字段的可组合项将被重新组合以显示更新。

修改状态并对状态更改做出反应

为了更改状态,您可能已经注意到 Button 有一个名为 onClick 的参数,但它不接受值,它接受函数

您可以通过为其分配 lambda 表达式 来定义点击时要执行的操作。例如,让我们切换 expanded 状态的值,并根据该值显示不同的文本。

ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

在交互模式下运行应用程序以查看行为。

374998ad358bf8d6.png

单击按钮时,expanded 将被切换,从而触发按钮内部文本的重新组合。每个 Greeting 保留其自己的 expanded 状态,因为它们属于不同的 UI 元素。

93d839b53b7d9bea.gif

到目前为止的代码

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier.weight(1f)) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

展开项目

现在让我们在请求时实际展开一个项目。添加一个取决于我们状态的其他变量

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    val expanded = remember { mutableStateOf(false) }

    val extraPadding = if (expanded.value) 48.dp else 0.dp
// ...

您无需针对重新组合记住 extraPadding,因为它正在执行简单的计算。

现在我们可以将新的填充修饰符应用于 Column

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val expanded = remember { mutableStateOf(false) }
    val extraPadding = if (expanded.value) 48.dp else 0.dp
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                    .weight(1f)
                    .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded.value = !expanded.value }
            ) {
                Text(if (expanded.value) "Show less" else "Show more")
            }
        }
    }
}

如果您在模拟器或交互模式下运行,您应该会看到每个项目都可以独立展开

6675d41779cac69.gif

8. 状态提升

在可组合函数中,由多个函数读取或修改的状态应位于一个公共祖先中——此过程称为状态提升提升是指提升提高

使状态可提升可以避免重复状态和引入错误,有助于重用可组合项,并使可组合项更容易测试。相反,不需要由可组合项的父级控制的状态不应被提升。真相来源属于创建和控制该状态的人。

例如,让我们为我们的应用程序创建一个入门屏幕。

5d5f44508fcfa779.png

将以下代码添加到 MainActivity.kt

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.material3.Button
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
// ...

@Composable
fun OnboardingScreen(modifier: Modifier = Modifier) {
    // TODO: This state should be hoisted
    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = { shouldShowOnboarding = false } 
        ) {
            Text("Continue")
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen()
    }
}

此代码包含许多新功能

  • 您添加了一个名为 OnboardingScreen 的新可组合项,以及一个新的预览。如果您构建项目,您会注意到您可以同时拥有多个预览。我们还添加了一个固定高度以验证内容是否正确对齐。
  • Column 可以配置为在其屏幕中心显示其内容。

  • shouldShowOnboarding 使用了 by 关键字而不是 =。这是一个属性委托,可以避免每次都键入 .value
  • 当点击按钮时,shouldShowOnboarding 被设置为 false,但是你还没有从任何地方读取状态。

现在我们可以将这个新的 onboarding 屏幕添加到我们的应用中。我们希望在启动时显示它,然后当用户按下“继续”时隐藏它。

在 Compose 中,**你不会隐藏 UI 元素**。相反,你只需不将它们添加到组合中,这样它们就不会被添加到 Compose 生成的 UI 树中。你可以使用简单的 Kotlin 条件逻辑来实现。例如,要显示 onboarding 屏幕或问候语列表,你可以执行以下操作

// Don't copy yet
@Composable
fun MyApp(modifier: Modifier = Modifier) {
    Surface(modifier) {
        if (shouldShowOnboarding) { // Where does this come from?
            OnboardingScreen()
        } else {
            Greetings()
        }
    }
}

但是我们无法访问 shouldShowOnboarding。很明显,我们需要将我们在 OnboardingScreen 中创建的状态与 MyApp 可组合项共享。

与其以某种方式将状态的值与它的父级共享,不如**提升**状态——我们只需将其移动到需要访问它的公共祖先。

首先,将 MyApp 的内容移动到一个名为 Greetings 的新可组合项中。同时调整预览以调用 Greetings 方法而不是当前的。

@Composable
fun MyApp(modifier: Modifier = Modifier) {
     Greetings()
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

为我们新的顶级 MyApp 可组合项添加一个预览,以便我们可以测试其行为。

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

现在添加在 MyApp 中显示不同屏幕的逻辑,并**提升**状态。

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(/* TODO */)
        } else {
            Greetings()
        }
    }
}

我们还需要与 onboarding 屏幕共享 shouldShowOnboarding,但我们不会直接传递它。与其让 OnboardingScreen 改变我们的状态,不如让它在用户点击“继续”按钮时通知我们。

我们如何向上传递事件?通过**向下传递回调**。回调是作为参数传递给其他函数的函数,并在事件发生时执行。

尝试向 onboarding 屏幕添加一个函数参数,定义为 onContinueClicked: () -> Unit,以便你可以从 MyApp 中修改状态。

解决方案

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {


    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier
                .padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

通过向 OnboardingScreen 传递一个函数而不是状态,我们使这个可组合项更具可重用性,并防止状态被其他可组合项修改。一般来说,这可以简化事情。一个很好的例子是 onboarding 预览需要如何修改才能现在调用 OnboardingScreen

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {}) // Do nothing on click.
    }
}

onContinueClicked 分配给一个空 lambda 表达式意味着“不做任何事”,这对于预览来说是完美的。

这越来越像一个真正的应用程序了,干得好!

25915eb273a7ef49.gif

MyApp 可组合项中,我们第一次使用了 by 属性委托来避免每次都使用 value。让我们在 Greeting 可组合项中也为 expanded 属性使用 by 而不是 =。确保将 expandedval 更改为 var

迄今为止的完整代码

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {

    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = listOf("World", "Compose")
) {
    Column(modifier = modifier.padding(vertical = 4.dp)) {
        for (name in names) {
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by remember { mutableStateOf(false) }

    val extraPadding = if (expanded) 48.dp else 0.dp

    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(
                modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

9. 创建高性能的懒加载列表

现在让我们使姓名列表更真实一些。到目前为止,你已在一个 Column 中显示了两个问候语。但是,它能否处理数千个问候语呢?

更改 Greetings 参数中的默认列表值,以使用另一个列表构造函数,该构造函数允许设置列表大小并使用其 lambda 中包含的值填充它(此处 $it 表示列表索引)

names: List<String> = List(1000) { "$it" }

这会创建 1000 个问候语,即使是屏幕上无法容纳的那些问候语。显然,这效率不高。你可以尝试在模拟器上运行它(警告:此代码可能会冻结你的模拟器)。

要显示一个可滚动的列,我们使用 LazyColumnLazyColumn 只渲染屏幕上可见的项目,在渲染大型列表时可以提高性能。

在基本用法中,LazyColumn API 在其范围内提供了一个 items 元素,其中编写了单个项目渲染逻辑。

import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
// ...

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" } 
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

284f925eb984fb56.gif

10. 持久化状态

我们的应用存在两个问题

持久化 onboarding 屏幕状态

如果你在设备上运行应用,点击按钮,然后旋转屏幕,onboarding 屏幕将再次显示。 remember 函数**仅在可组合项保留在 Composition 中时有效**。当你旋转屏幕时,整个活动将重新启动,因此所有状态都会丢失。这种情况也会发生在任何配置更改和进程死亡时。

与其使用 remember,不如使用 rememberSaveable。这将保存每个状态,使其能够在配置更改(例如旋转)和进程死亡后继续存在。

现在,将 shouldShowOnboardingremember 的使用替换为 rememberSaveable

    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

运行应用,旋转屏幕,切换到深色模式或杀死进程。除非你之前已退出应用,否则 onboarding 屏幕不会显示。

持久化列表项的展开状态

如果你展开一个列表项,然后滚动列表直到该项超出视野,或者旋转设备然后返回到已展开的项,你会发现该项现在已恢复到其初始状态。

解决方案是也为展开状态使用 rememberSaveable。

   var expanded by rememberSaveable { mutableStateOf(false) }

到目前为止,大约 120 行代码,你能够显示一个长而高效的可滚动项目列表,每个项目都保存其自己的状态。此外,如你所见,你的应用具有完美的深色模式,无需额外的代码行。你将在稍后学习主题。

11. 为你的列表添加动画

在 Compose 中,有多种方法可以为你的 UI 添加动画:从用于简单动画的高级 API 到用于完全控制和复杂过渡的低级方法。你可以在文档中阅读有关它们的信息。

在本节中,你将使用其中一个低级 API,但不用担心,它们也可以非常简单。让我们为我们已经实现的大小变化添加动画。

9efa14ce118d3835.gif

为此,你将使用 animateDpAsState 可组合项。它返回一个 State 对象,其 value 将通过动画持续更新,直到动画完成。它接受一个类型为 Dp 的“目标值”。

创建一个依赖于展开状态的动画 extraPadding

import androidx.compose.animation.core.animateDpAsState

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding)
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }

        }
    }
}

运行应用并尝试动画。

animateDpAsState 接受一个可选的 animationSpec 参数,该参数允许你自定义动画。让我们做一些更有趣的事情,例如添加基于弹簧的动画。

import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring


@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )

    Surface(
    // ...
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))

    // ...

    )
}

请注意,我们还确保填充永远不会为负,否则可能会导致应用崩溃。这引入了一个细微的动画错误,我们将在后面的**收尾工作**中修复。

spring 规范不采用任何与时间相关的参数。相反,它依赖于物理属性(阻尼和刚度)使动画更自然。现在运行应用以尝试新的动画。

9efa14ce118d3835.gif

使用 animate*AsState 创建的任何动画都是可中断的。这意味着如果目标值在动画中间发生变化,animate*AsState 将重新启动动画并指向新值。对于基于弹簧的动画,中断看起来特别自然。

d5dbf92de69db775.gif

如果你想探索不同类型的动画,请尝试 spring 的不同参数、不同的规范(tweenrepeatable)和不同的函数:animateColorAsState不同类型的动画 API

本节的完整代码

package com.example.basicscodelab

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.ElevatedButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.codelab.basics.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {


    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }

}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {

    var expanded by rememberSaveable { mutableStateOf(false) }

    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioMediumBouncy,
            stiffness = Spring.StiffnessLow
        )
    )
    Surface(
        color = MaterialTheme.colorScheme.primary,
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        Row(modifier = Modifier.padding(24.dp)) {
            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name)
            }
            ElevatedButton(
                onClick = { expanded = !expanded }
            ) {
                Text(if (expanded) "Show less" else "Show more")
            }
        }
    }
}

@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

12. 样式和主题化你的应用

到目前为止,你还没有设置任何可组合项的样式,但你却获得了不错的默认样式,包括深色模式支持!让我们了解一下 BasicsCodelabThemeMaterialTheme 是什么。

如果你打开 ui/theme/Theme.kt 文件,你会看到 BasicsCodelabTheme 在其实现中使用了 MaterialTheme

// Do not copy
@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // ...

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

MaterialTheme 是一个可组合函数,它反映了来自Material Design 规范的样式原则。这些样式信息会级联到其 content 内部的组件,这些组件可能会读取这些信息以设置自己的样式。在你的 UI 中,你已经像这样使用 BasicsCodelabTheme

    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

因为 BasicsCodelabTheme 在内部包装了 MaterialTheme,所以 MyApp 使用主题中定义的属性进行样式设置。从任何后代可组合项,你可以检索 MaterialTheme 的三个属性:colorSchemetypographyshapes。使用它们为你的一个 Text 设置标题样式。

            Column(modifier = Modifier
                .weight(1f)
                .padding(bottom = extraPadding.coerceAtLeast(0.dp))
            ) {
                Text(text = "Hello, ")
                Text(text = name, style = MaterialTheme.typography.headlineMedium)
            }

上面示例中的 Text 可组合项设置了一个新的 TextStyle。你可以创建自己的 TextStyle,或者你可以通过使用 MaterialTheme.typography 检索主题定义的样式,这是首选方法。此构造允许你访问 Material 定义的文本样式,例如 displayLarge, headlineMedium, titleSmall, bodyLarge, labelMedium 等。在你的示例中,你使用了主题中定义的 headlineMedium 样式。

现在构建以查看我们新设置样式的文本。

673955c38b076f1c.png

一般来说,最好将颜色、形状和字体样式保留在 MaterialTheme 中。例如,如果你硬编码颜色,深色模式将难以实现,并且修复它将需要大量容易出错的工作。

但是,有时你需要略微偏离颜色和字体样式的选择。在这些情况下,最好将你的颜色或样式基于现有的颜色或样式。

为此,你可以使用 copy 函数修改预定义样式。使数字加粗。

import androidx.compose.ui.text.font.FontWeight
// ...
Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

这样,如果你需要更改 headlineMedium 的字体系列或任何其他属性,就不必担心细微的偏差。

现在,预览窗口中应该显示以下结果。

b33493882bda9419.png

设置深色模式预览

目前,我们的预览仅显示应用程序在浅色模式下的外观。在 GreetingPreview 中添加另一个 @Preview 注解,并使用 UI_MODE_NIGHT_YES

import android.content.res.Configuration.UI_MODE_NIGHT_YES


@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

这会添加一个暗色模式下的预览。

2c94dc7775d80166.png

调整应用程序的主题

您可以在 ui/theme 文件夹内的文件中找到与当前主题相关的所有内容。例如,我们迄今为止一直在使用的默认颜色定义在 Color.kt 中。

让我们从定义新颜色开始。将这些添加到 Color.kt

val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(0xFFEFF7CF)

现在将它们分配到 Theme.ktMaterialTheme 的调色板中

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

如果您返回到 MainActivity.kt 并刷新预览,预览颜色实际上并没有改变!这是因为默认情况下,您的预览将使用 动态颜色。您可以在 Theme.kt 中看到添加动态着色的逻辑,使用 dynamicColor 布尔参数。

要查看颜色方案的非自适应版本,请在 API 级别低于 31(对应于引入自适应颜色的 Android S)的设备上运行您的应用程序。您将看到新颜色

493d754584574e91.png

Theme.kt 中,定义暗色调色板

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

现在运行应用程序时,我们将看到暗色在起作用

84d2a903ffa6d8df.png

Theme.kt 的最终代码

import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.ViewCompat

private val DarkColorScheme = darkColorScheme(
    surface = Blue,
    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)

private val LightColorScheme = lightColorScheme(
    surface = Blue,
    onSurface = Color.White,
    primary = LightBlue,
    onPrimary = Navy
)

@Composable
fun BasicsCodelabTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    // Dynamic color is available on Android 12+
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme
        else -> LightColorScheme
    }
    val view = LocalView.current
    if (!view.isInEditMode) {
        SideEffect {
            (view.context as Activity).window.statusBarColor = colorScheme.primary.toArgb()
            ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme
        }
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

13. 最后的润色!

在此步骤中,您将应用已知内容并学习一些新概念,只需少量提示。您将创建这个

8d24a786bfe1a8f2.gif

用图标替换按钮

  • IconButton 可组合项与子 Icon 结合使用。
  • 使用 Icons.Filled.ExpandLessIcons.Filled.ExpandMore,它们在 material-icons-extended 工件中可用。将以下行添加到 app/build.gradle.kts 文件中的依赖项中。
implementation("androidx.compose.material:material-icons-extended")
  • 修改填充以修复对齐方式。
  • 为辅助功能添加内容描述(请参阅下面的“使用字符串资源”)。

使用字符串资源

“显示更多”和“显示更少”的内容描述应该存在,您可以使用简单的 if 语句添加它们

contentDescription = if (expanded) "Show less" else "Show more"

但是,硬编码字符串是一种不好的做法,您应该从 strings.xml 文件中获取它们。

您可以对每个字符串使用“提取字符串资源”,它在 Android Studio 的“上下文操作”中可用,以自动执行此操作。

或者,打开 app/src/res/values/strings.xml 并添加以下资源

<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

显示更多

“Composem ipsum”文本出现和消失,触发每张卡片大小的变化。

  • Greeting 内部的 Column 中添加一个新的 Text,在项目展开时显示。
  • 移除 extraPadding,而是将 animateContentSize 修饰符应用于 Row。这将自动化创建动画的过程,手动执行此操作将很困难。此外,它消除了 coerceAtLeast 的必要性。

添加海拔和形状

  • 您可以将 shadow 修饰符与 clip 修饰符结合使用以实现卡片外观。但是,有一个 Material 可组合项可以准确地做到这一点:Card。您可以通过调用 CardDefaults.cardColors 并覆盖要更改的颜色来更改 Card 的颜色。

最终代码

package com.example.basicscodelab

import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons.Filled
import androidx.compose.material.icons.filled.ExpandLess
import androidx.compose.material.icons.filled.ExpandMore
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme


class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            BasicsCodelabTheme {
                MyApp(modifier = Modifier.fillMaxSize())
            }
        }
    }
}

@Composable
fun MyApp(modifier: Modifier = Modifier) {
    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

    Surface(modifier, color = MaterialTheme.colorScheme.background) {
        if (shouldShowOnboarding) {
            OnboardingScreen(onContinueClicked = { shouldShowOnboarding = false })
        } else {
            Greetings()
        }
    }
}

@Composable
fun OnboardingScreen(
    onContinueClicked: () -> Unit,
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = onContinueClicked
        ) {
            Text("Continue")
        }
    }
}

@Composable
private fun Greetings(
    modifier: Modifier = Modifier,
    names: List<String> = List(1000) { "$it" }
) {
    LazyColumn(modifier = modifier.padding(vertical = 4.dp)) {
        items(items = names) { name ->
            Greeting(name = name)
        }
    }
}

@Composable
private fun Greeting(name: String, modifier: Modifier = Modifier) {
    Card(
        colors = CardDefaults.cardColors(
            containerColor = MaterialTheme.colorScheme.primary
        ),
        modifier = modifier.padding(vertical = 4.dp, horizontal = 8.dp)
    ) {
        CardContent(name)
    }
}

@Composable
private fun CardContent(name: String) {
    var expanded by rememberSaveable { mutableStateOf(false) }

    Row(
        modifier = Modifier
            .padding(12.dp)
            .animateContentSize(
                animationSpec = spring(
                    dampingRatio = Spring.DampingRatioMediumBouncy,
                    stiffness = Spring.StiffnessLow
                )
            )
    ) {
        Column(
            modifier = Modifier
                .weight(1f)
                .padding(12.dp)
        ) {
            Text(text = "Hello, ")
            Text(
                text = name, style = MaterialTheme.typography.headlineMedium.copy(
                    fontWeight = FontWeight.ExtraBold
                )
            )
            if (expanded) {
                Text(
                    text = ("Composem ipsum color sit lazy, " +
                        "padding theme elit, sed do bouncy. ").repeat(4),
                )
            }
        }
        IconButton(onClick = { expanded = !expanded }) {
            Icon(
                imageVector = if (expanded) Filled.ExpandLess else Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }
}

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = UI_MODE_NIGHT_YES,
    name = "GreetingPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}

@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
    BasicsCodelabTheme {
        OnboardingScreen(onContinueClicked = {})
    }
}

@Preview
@Composable
fun MyAppPreview() {
    BasicsCodelabTheme {
        MyApp(Modifier.fillMaxSize())
    }
}

14. 恭喜

恭喜!您学习了 Compose 的基础知识!

Codelab 的解决方案

您可以从 GitHub 获取本 Codelab 解决方案的代码

$ git clone https://github.com/android/codelab-android-compose

或者,您可以将存储库下载为 Zip 文件

接下来是什么?

查看 Compose 学习路径 上的其他代码实验室

进一步阅读