编写你的第一个 Compose 应用

Jetpack Compose 基础知识

1. 准备工作

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

Compose 应用由可组合函数构成。可组合函数即带有 @Composable 标记的常规函数,这些函数可以调用其他可组合函数。使用一个函数就可以创建一个新的界面组件。该注解会告知 Compose 为函数添加特殊支持,以便后续更新和维护界面。借助 Compose,您可以将代码设计成多个小代码块。可组合函数通常简称为"可组合项"。

通过创建可重用的小型可组合项,您可以轻松构建应用中所用界面元素的库。每个可组合项对应于屏幕的一个部分,可以单独修改。

2. 启动新的 Compose 项目

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

setContent 中使用的应用主题取决于项目名称。

3. Compose 使用入门

可组合函数

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

可组合函数 是带有 @Composable 注解的常规函数。这类函数自身可以调用其他 @Composable 函数。

注意 :可组合函数是带有 @Composable 注解的 Kotlin 函数,如上述代码段所示。

Android 应用中的 Compose

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

kotlin 复制代码
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 是为可组合函数设置样式的一种方式。

若要使用 Android Studio 预览,您只需使用 @Preview 注解标记所有无参数可组合函数或采用默认形参的函数,然后构建您的项目即可。现在 MainActivity.kt 文件中已经包含了一个 Preview Composable 函数。您可以在同一个文件中包含多个预览,并为它们指定名称。

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

注意:在此项目中导入与 Jetpack Compose 相关的类时,请从以下位置导入:

  • androidx.compose.*,适用于编译器和运行时类
  • androidx.compose.ui.*,适用于界面工具包和库

4. 微调界面

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

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

Material 组件(例如 androidx.compose.material3.Surface)旨在提供应用中可能需要的常见功能(例如为文本选择适当的颜色),让您获得更好的体验。我们之所以说 Material 很实用,是因为它提供在大多数应用中都会用到的实用默认值和模式。Compose 中的 Material 组件是在其他基础组件(位于 androidx.compose.foundation 中)的基础上构建的。如果您需要更高的灵活性,也可以从您的应用组件中访问这些组件。

在这种情况下,Surface 会了解,当该背景设置为 primary 颜色后,其上的任何文本都应使用 onPrimary 颜色,此颜色也在主题中进行了定义。

注意 :如需查看 Compose 中 Material 组件的交互式列表,请查看 Compose Material Catalog 应用。

修饰符

大多数 Compose 界面元素(例如 SurfaceText)都接受可选的 modifier 参数。修饰符会指示界面元素如何在其父布局中放置、显示或表现。

例如,padding 修饰符会在其修饰的元素周围应用一定的空间。您可以使用 Modifier.padding() 创建内边距修饰符。您还可串联多个修饰符以添加它们。

现在,为界面上的 Text 添加内边距:

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

有数十种修饰符可用于实现对齐、添加动画、设置布局、使可点击或可滚动以及转换等效果。有关完整列表,请查看 Compose 修饰符列表

5. 重复使用可组合项

您添加到界面的组件越多,创建的嵌套层级就越多。如果函数变得非常大,可能会影响可读性。通过创建可重用的小型组件,可以轻松构建应用中所用界面元素的库。每个组件对应于屏幕的一个部分,可以单独修改。

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

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

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

这样一来,由于现在可以重复使用 MyApp 可组合项,您就可以省去 onCreate 回调和预览,从而避免重复编写代码。

在预览中,调用 MyApp 并移除预览的名称。

kotlin 复制代码
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

6. 创建列和行

Compose 中的三个基本标准布局元素是 ColumnRowBox 可组合项。

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

kotlin 复制代码
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 中的其他函数一样使用。这会使界面构建变得非常有效,因为您可以添加语句来影响界面的显示方式。

例如,您可以使用 for 循环向 Column 中添加元素:

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

您尚未设置可组合项的尺寸,也未对可组合项的大小添加任何限制,因此每一行仅占用可能的最小空间,预览时的效果也是如此。让我们更改预览效果,以模拟小屏幕手机的常见宽度 320dp。按如下所示向 @Preview 注解添加 widthDp 参数:

kotlin 复制代码
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingPreview() {
    BasicsCodelabTheme {
        MyApp()
    }
}

请注意:

  • 修饰符可以包含重载,因而具有相应的优势,例如您可以指定不同的方式来创建内边距。
  • 若要向一个元素添加多个修饰符,您只需要将它们链接起来即可。

添加按钮

Button 是 material3 软件包提供的一种可组合项,它采用可组合项作为最后一个参数。由于尾随 lambda 可以移到括号之外,因此您可以向按钮添加任何内容作为子级,例如 Text

kotlin 复制代码
Button(
    onClick = { } // You'll learn about this callback later
) {
    Text("Show less")
}

注意 :Compose 根据 Material Design 按钮规范提供了不同类型的 ButtonButtonElevatedButtonFilledTonalButtonOutlinedButtonTextButton。在本示例中,您将使用 ElevatedButton,它会封装 Text 作为 ElevatedButton 内容。

为了实现这一点,您需要学习如何在行尾放置可组合项。由于没有 alignEnd 修饰符,因此您需要在开始时为该可组合项赋予一定的 weightweight 修饰符会让元素填满所有可用空间,使其"具有弹性",也就是会推开其他没有权重的元素(即"无弹性"元素)。该修饰符还会使 fillMaxWidth 修饰符变得多余。

现在尝试添加该按钮,并按照上述图片中所示放置该按钮。

kotlin 复制代码
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 中的状态

注意 :Compose 应用通过调用可组合函数将数据转换为界面。如果您的数据发生变化,Compose 会使用新数据重新执行这些函数,从而创建更新后的界面,此过程称为重组。Compose 还会查看各个可组合项需要哪些数据,以便只需重组数据发生了变化的组件,而避免重组未受影响的组件。

正如 Compose 编程思想一文中所述:

可组合函数可以按任意顺序频繁执行,因此您不能以代码的执行顺序或该函数的重组次数为判断依据。

如需向可组合项添加内部状态,可以使用 mutableStateOf 函数,该函数可让 Compose 重组读取该 State 的函数。

注意: StateMutableState 是两个接口,它们具有特定的值,每当该值发生变化时,它们就会触发界面更新(重组)。

但是,不能只是mutableStateOf 分配给可组合项中的某个变量 。如前所述,重组可能会随时发生,这会再次调用可组合项,从而将状态重置为值为 false 的新可变状态。

如需在重组后保留状态,请使用 remember 记住可变状态。

kotlin 复制代码
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
// ...

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

remember 可以起到保护作用,防止状态在重组时被重置。

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

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

更改状态和响应状态更改

为了更改状态,Button 具有一个名为 onClick 的形参,但它不接受值,而接受函数

注意 :您可能不熟悉以这种方式使用的函数,这其实就是一种在 Compose 中广泛使用的非常强大的 Kotlin 功能。函数是 Kotlin 中的首要元素,您可以将它们分配给某个变量,传递给其他函数,甚至可以从它们自身返回函数。您可以在此处了解 Compose 如何使用 Kotlin 功能

如需详细了解如何定义和实例化函数,请参阅函数类型文档

您可以通过为"onClick"指定 lambda 表达式,定义点击时将执行的操作。例如,切换展开状态的值,并根据该值显示不同的文本。

kotlin 复制代码
ElevatedButton(
    onClick = { expanded.value = !expanded.value },
) {
   Text(if (expanded.value) "Show less" else "Show more")
}

点击该按钮后,expanded 会切换,从而触发对按钮内文本的重组。每个 Greeting 都具有自己的展开状态,因为它们属于不同的界面元素。

展开内容

现在,我们来根据请求实际展开内容。添加一个依赖于状态的额外变量:

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

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

8. 状态提升

在可组合函数中,被多个函数读取或修改的状态应位于共同祖先实体中,此过程称为状态提升。"提升"意为"提高"或"升级"。

使状态可提升,可以避免复制状态和引入 bug,有助于重复使用可组合项,并大大降低可组合项的测试难度。相反,不需要由可组合项的父级控制的状态则不应该被提升。可信来源属于该状态的创建者和控制者。

在 Compose 中,您不会隐藏界面元素,因为不会将它们添加到组合中,因此它们也不会添加到 Compose 生成的界面树中。

kotlin 复制代码
@Composable
fun MyApp(modifier: Modifier = Modifier) {

    var shouldShowOnboarding by remember { mutableStateOf(true) }

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

我们还需要与初始配置屏幕共享 shouldShowOnboarding,但我们不会直接传递它。与其让 OnboardingScreen 更改状态,不如让它在用户点击"Continue"按钮时通知我们。

如何向上传递事件?通过向下传递回调来传递。回调是这样一类函数,它们以实参的形式传递给其他函数,并在事件发生时执行。

尝试向初始配置屏幕添加定义为 onContinueClicked: () -> Unit 的函数参数,以便您可以从 MyApp 更改状态。

解决方案:

kotlin 复制代码
@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 传递函数而不是状态,可以提高该可组合项的可重用性,并防止状态被其他可组合项更改。一般而言,这可以让事情变得简单。一个很好的例子就是,现在需要如何修改初始配置屏幕预览来调用 OnboardingScreen

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

onContinueClicked 分配给空 lambda 表达式就等于"什么也不做",这非常适合于预览。

shouldShowOnboarding 使用的是 by 关键字,而不是 =。这是一个属性委托,可让您无需每次都输入 .value

我们首次使用了 by 属性委托,以避免每次都使用值。

9. 创建高效延迟列表

您可以设置列表的大小并使用其 lambda 中包含的值来填充列表(这里的 $it 代表列表索引):

kotlin 复制代码
names: List<String> = List(1000) { "$it" }

为显示可滚动列,我们需要使用 LazyColumnLazyColumn 只会渲染屏幕上可见的内容,从而在渲染大型列表时提升效率。

注意LazyColumnLazyRow 相当于 Android View 中的 RecyclerView

在其基本用法中,LazyColumn API 会在其作用域内提供一个 items 元素,并在该元素中编写各项内容的渲染逻辑:

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

注意 :请确保导入 androidx.compose.foundation.lazy.items,因为 Android Studio 默认会选择另一个 items 函数。
注意LazyColumn 不会像 RecyclerView 一样回收其子级。它会在您滚动它时发出新的可组合项,并保持高效运行,因为与实例化 Android Views 相比,发出可组合项的成本相对较低。

10. 保留状态

保留初始配置界面状态

如果您在设备上运行应用,点击按钮,然后旋转,系统会再次显示初始配置界面。remember 函数仅在可组合项包含在组合中时起作用。旋转屏幕后,整个 activity 都会重启,所有状态都将丢失。当发生任何配置更改或者进程终止时,也会出现这种情况。

您可以使用 rememberSaveable,而不使用 remember。这会保存每个在配置更改(如旋转)和进程终止后保留下来的状态。

现在,将 shouldShowOnboarding 中的 remember 替换为 rememberSaveable

kotlin 复制代码
    import androidx.compose.runtime.saveable.rememberSaveable
    // ...

    var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

运行应用,旋转屏幕,更改为深色模式,或者终止进程。除非您之前退出了应用,否则系统不会显示初始配置界面。

保持列表项的展开状态

如果您展开某个列表项并滚动列表直至该项不在视野范围内,或者旋转设备并返回到展开的项,您会看到该项现已恢复为初始状态。

解决方法是也为展开状态使用 rememberSaveable:

kotlin 复制代码
   var expanded by rememberSaveable { mutableStateOf(false) }

11. 为列表添加动画效果

在 Compose 中,有多种方式可以为界面添加动画效果:从用于添加简单动画的高阶 API 到用于实现完全控制和复杂过渡的低阶方法,不一而足。您可以在该文档中了解相关信息。

为此,您将使用 animateDpAsState 可组合项。该可组合项会返回一个 State 对象,该对象的 value 会被动画持续更新,直到动画播放完毕。该可组合项需要一个类型为 Dp 的"目标值"。

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

kotlin 复制代码
    val extraPadding by animateDpAsState(
        if (expanded) 48.dp else 0.dp
    )

animateDpAsState 接受可选的 animationSpec 参数供您自定义动画。

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

spring 规范不接受任何与时间有关的参数。它仅依赖于物理属性(阻尼和刚度),使动画更自然。

使用 animate*AsState 创建的任何动画都是可中断的。这意味着,如果目标值在动画播放过程中发生变化,animate*AsState 会重启动画并指向新值。中断在基于弹簧的动画中看起来尤其自然:

如果您想探索不同类型的动画,请尝试为 spring 提供不同的参数,尝试使用不同的规范(tweenrepeatable)和不同的函数(animateColorAsState不同类型的动画 API)。

12. 设置应用的样式和主题

MaterialTheme 是一个可组合函数,体现了 Material Design 规范中的样式设置原则。样式设置信息会逐级向下传递到位于其 content 内的组件,这些组件会读取该信息来设置自身的样式。您在界面中已经使用了 BasicsCodelabTheme,如下所示:

kotlin 复制代码
    BasicsCodelabTheme {
        MyApp(modifier = Modifier.fillMaxSize())
    }

由于 BasicsCodelabThemeMaterialTheme 包围在其内部,因此 MyApp 会使用该主题中定义的属性来设置样式。从任何后代可组合项中都可以检索 MaterialTheme 的三个属性:colorSchemetypographyshapes。使用它们设置其中一个 Text 的标题样式:

kotlin 复制代码
            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 样式。

通常来说,最好是将颜色、形状和字体样式放在 MaterialTheme 中。例如,如果对颜色进行硬编码,将会很难实现深色模式,并且需要进行大量修正工作,而这很容易造成错误。

不过,有时除了选择颜色和字体样式,您还可以基于现有的颜色或样式进行设置。

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

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

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

设置深色模式预览

目前,我们的预览仅会显示应用在浅色模式下的显示效果。使用 UI_MODE_NIGHT_YESGreetingPreview 添加额外的 @Preview 注解:

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

微调应用的主题

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

您的预览将默认使用动态配色。您可以在 Theme.kt 中查看使用 dynamicColor 布尔值参数添加动态配色的逻辑。

如需查看非自适应版本的配色方案,请在 API 级别低于 31(对应引入了自适应配色的 Android S)的设备上运行您的应用。

13. 收尾!

用图标替换按钮

  • IconButton 可组合项与子级 Icon 结合使用。
  • 使用 material-icons-extended 制品中提供的 Icons.Filled.ExpandLessIcons.Filled.ExpandMore。将以下代码行添加到 app/build.gradle.kts 文件中的依赖项中。
gradle 复制代码
implementation("androidx.compose.material:material-icons-extended")
  • 修改内边距以修正对齐问题。
  • 为无障碍功能添加内容说明。

使用字符串资源

应该为"Show more"和"show less"提供内容说明,您可以通过简单的 if 语句进行添加:

kotlin 复制代码
contentDescription = if (expanded) "Show less" else "Show more"

不过,硬编码字符串的方式并不可取,应该从 strings.xml 文件中获取字符串。

您可以通过对每个字符串使用"Extract string resource"(在 Android Studio 中的"Context Actions"中提供)来自动执行此操作。

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

xml 复制代码
<string name="show_less">Show less</string>
<string name="show_more">Show more</string>

展开

"Composem ipsum"文字会在显示后消失,触发每张卡片的大小变化。

  • 将新的 Text 添加到 Greeting 中当内容展开时显示的 Column 中。
  • 移除 extraPadding 并改为将 animateContentSize 修饰符应用于 Row。这会自动执行创建动画的过程,而手动执行该过程会很困难。此外,也不需要再使用 coerceAtLeast

添加高度和形状

  • 您可以结合使用 shadow 修饰符和 clip 修饰符来实现卡片外观。不过,有一种 Material 可组合项也可以做到这一点:Card。您可以通过调用 CardDefaults.cardColors 并覆盖想要更改的颜色,以此来更改 Card 的颜色。

14. 最终源码

kotlin 复制代码
package com.example.basicscodelab

import android.content.res.Configuration
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.animation.animateContentSize
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.material.icons.Icons
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.ElevatedButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
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.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.coerceAtLeast
import androidx.compose.ui.unit.dp
import com.example.basicscodelab.ui.theme.BasicsCodelabTheme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            BasicsCodelabTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    MyApp(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(innerPadding)
                    )
                }
            }
        }
    }
}

@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(
    modifier: Modifier = Modifier,
    onContinueClicked: () -> Unit
) {
    Column(
        modifier = modifier.fillMaxSize(),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(text = "Welcome to the Basics Codelab!")
        Button(
            modifier = Modifier.padding(vertical = 24.dp),
            onClick = {
                onContinueClicked.invoke()
            }
        ) {
            Text(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
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) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore,
                contentDescription = if (expanded) {
                    stringResource(R.string.show_less)
                } else {
                    stringResource(R.string.show_more)
                }
            )
        }
    }

}

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

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

@Preview(
    showBackground = true,
    widthDp = 320,
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    name = "GreetingsPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun GreetingsPreview() {
    BasicsCodelabTheme {
        Greetings()
    }
}
相关推荐
hrrrrb11 分钟前
【Java Web 快速入门】十、AOP
java·前端·spring boot
chxii25 分钟前
4.3 computed watch watchEffect
前端·javascript·vue.js
Akshsjsjenjd33 分钟前
Linux 服务部署:自签 CA 证书构建 HTTPS 及动态 Web 集成
linux·前端·https
前端小巷子36 分钟前
Vue SSR原理
前端·vue.js·面试
excel44 分钟前
JavaScript 代理(Proxy)与反射(Reflect)详解
前端
活宝小娜1 小时前
新增和编辑共用弹窗模板
开发语言·前端·javascript·vue.js
小离a_a2 小时前
根据图片远程地址复制图片内容,可以在富文本、word等文本里粘贴
开发语言·前端·javascript
宇寒风暖5 小时前
@(AJAX)
前端·javascript·笔记·学习·ajax
Giser探索家10 小时前
低空智航平台技术架构深度解析:如何用AI +空域网格破解黑飞与安全管控难题
大数据·服务器·前端·数据库·人工智能·安全·架构
gnip11 小时前
前端实现自动检测项目部署更新
前端