书接上文,咱们继续
9、创建高效延迟列表
现在,我们来让名称列表更真实。到目前为止,你已经在Column
中显示了两条问候语。但是,它可以处理成成千上万条问候语吗?
更改Greetings
形参中的默认列表值以使用其他列表构造函数,这使您可以设置列表的大小并使用其lambda中包含的值来填充列表(这里的$it
代表列表索引):
names: List<String> = List(1000) { "$it" }
这会创建1000条问候语,即使屏幕上放不下这些问候语。显然,这样做效果并不好。你可以尝试在模拟器上运行此代码(警告:此代码可能会使模拟器卡住)。
为显示可滚动列表,我们需要使用LazyColumn
。LazyColumn
只会渲染屏幕上可见的内容,从而在渲染大型列表时提升效率。
注意:LazyColumn和LazyRow相当于Android View中的RecyclerView
在其基本用法中,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)
}
}
}
注意:请确保导入androidx.compose.foundation.lazy.items,因为 Android Studio默认会选择另一个items函数
注意:LazyColumn不会像RecyclerView一样回收其子级。他会在你滚动它时发出新的可组合项,并保持高效运行,因为与实例化Android Views相比,发出可组合项的成本相对比较低。
10、保留状态
我们的应用存在一个问题:如果您在设备上运行该应用,点击按钮,然后旋转屏幕,系统会再次显示初始配置屏幕。remember
函数仅在可组合项包含在组合中时起作用。旋转屏幕后,整个activity都会重启,所有状态都将丢失。当发生任何配置更改或者进程终止时,也会出现这种情况。
您可以使用rememberSaveable
,而不使用remeber
。这会保存每个在配置更改(如旋转)和进程终止后保留下来的状态。
现在,将shouldShowOnboarding
中的remember
替换为rememberSaveable
:
var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }
运行应用,旋转屏幕,更改为深色模式,或者终止进程。除非您之前推出了应用,否则系统不会显示初始配置屏幕。
11、为列表添加动画效果
在Compose中,有多种方式可以为界面添加动画效果:从用于添加简单动画的高阶API到用于实现完全控制和复杂过度的低阶方法,不一而足。
在本部分中,您将使用一个低阶API,但不用担心,它们也可以非常简单。
为此,您将使用animateDpAsState
可组合项。该可组合项会返回一个State对象,该对象的value
会被动画持续更新,直到动画播放完毕。该可组合项需要一个类型为Dp
的“目标值”。
创建一个依赖于展开状态的动画extraPadding
。此外,我们还需要使用属性委托(by
关键字):
@Composable
private fun Greeting(name: String) {
var expanded by remember { 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")
}
}
}
}
运行应用并查看该动画的效果。
注意:如果您站看第1项内容,然后滚动到第20项内容,在返回到第1项内容,您会发现第1项内容已回复为原始尺寸。如果需要,您可以使用 rememberSaveable保存此数据,但为了使示例保持简单,我们不这样做。
animateDpAsState
接受可选的animationSpec
参数供您自定义动画。让我们来做一些有趣的尝试,比如添加基于弹簧的动画:
@Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPaddign 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))
)
...
)
}
请注意,我们还要确保内边距不会为负数,否则可能会导致应用崩溃。这会引入一个细微的动画bug,我们稍后会在收尾部分进行修复。
spring
规范不接受任何与实践有关的参数。它仅依赖于物理属性(阻尼和刚度),使动画更自然。立即运行该应用,查看新动画的效果:
使用animate*AsState
创建的任何动画都是可中断的。这意味着,如果目标值在动画播放过程中发生变化,animate*AsState
会重启动画并指向新值。中断是基于弹簧的动画中看起来尤其自然:
如果您想探索不同类型的动画,请尝试为spring
提供不同的参数,尝试使用不同的规范(tween
、repeatable
)和不同的函数(animateColorAsState
或不同类型的动画 API)
11.1、此部分的完整代码
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.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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composepractise.ui.theme.ComposePractiseTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePractiseTheme {
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
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 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 Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expaded) 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" lese "Show more")
}
}
}
}
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
ComposePractiseTheme {
Greetings()
}
}
@Preview
@Composable
fun MyAppPreview() {
ComposePractiseTheme {
MyApp(Modifier.fillMaxSize())
}
}
12、设置应用的样式和主题
到目前为止,我还没有为任何可组合设置过样式,但已经获得了一个不错的默认效果,包括支持深色模式!下面我们来了解一下BasicsCodelabTheme
和MaterialTheme
.
如果你打开ui/theme/Theme.kt
文件,您会看到BasicsCodeTheme
在其实现中使用了MaterialTheme
:
@Composable
fun BasicsCodelabTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unir
) {
// ...
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
MaterialTheme
是一个可组合函数,体现了Material Design规范中的样式设置原则。样式设置信息会逐级向下传递到位于其content
内的组件,这些组件会读取该信息来设置自身的样式。您在界面中已经使用BasicCodelabTheme
,如下所示:
BasicsCodelabTheme {
MyApp(modifier = Modifier.fillMaxSize())
}
由于BasicsCodelabTheme
将MaterialTheme
包围在其内部,因此MyApp
会使用该主题中定义的属性来设置样式。从任何后台可组合项中都可以检索MaterialTheme
的三个属性:colorScheme
、typography
、和shapes
。使用它们设置其中一个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
样式。
下面我们构建应用来查看采用新样式的文本:
通常来说,最好是将颜色、形状和字体样式放在MaterialTheme
中。例如,如果对颜色进行硬编码,将会很难实现深色模式,并且需要进行大量修正工作,而这很容易造成错误。
不过,有时除了选择颜色和字体样式,您还可以基于现有的颜色或样式进行设置。
为此,您可以使用copy
函数修改预定义的样式。将数组加粗:
Text(
text = name,
style = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.ExtraBold
)
)
这样一来,如果您需要更改headlineMedium
的字体系列或其他任何属性,就不必担心出现细微偏差了。
现在,预览窗口中的结果应如下所示:
12.1、设置深色模式预览
目前,我们的预览仅会显示应用在浅色模式下的显示效果。使用UI_MODE_NIGHT_YES
向DefaultPreview
添加额外的@Preview
注解:
@Preview(
showBackground = true,
widthDp = 320,
unMode = UI_MODE_NIGHT_YES,
name = "Dark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
BasicsCodelabTheme {
Greeting()
}
}
系统随即会添加一个深色模式的预览。
12.2、微调应用的主题
您可以在ui/theme
文件夹内的文件中找到与当前主题相关的所有内容。例如,我们到目前为止所使用的默认颜色均在Color.kt
中定义。
首先,我们来定义新的颜色。将以下代码添加到Color.kt
中:
val Navy = Color(0xFF073042)
val Blue = Color(0xFF4285F4)
val LightBlue = Color(0xFFD7EFFE)
val Chartreuse = Color(OxFFEFF7CF)
现在,将这些颜色分配给Theme.kt
中的MaterialTheme
的调色板:
private val LightColorScheme = lightColorScheme(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)
如果您返回MainActivity.kt
并刷新预览,预览颜色实际上并不会改变!这是因为,您的预览将默认使用动态配色。您可以在Theme.kt
中查看使用dynamicColor
布尔值参数添加动态配色的逻辑。
如需查看非自适应版本的配色方案,请在API级别低于31(对应引入了自适应配色的Android S)的设备上运行您的应用。您会看到新颜色。
在Theme.kt
中,定义针对深色的调色板:
private val DarkColorScheme = darkColorScheme(
surface = Blue,
onSurface = Navy,
primary = Navy,
onPrimary = Chartreuse
)
现在,当我们运行应用时,会看到深色的实际效果:
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 LoghtColorScheme = lightColorScheme(
surface = Blue,
onSurface = Color.White,
primary = LightBlue,
onPrimary = Navy
)
@Composable
fun BasicsCodelabTheme(
dartTheme: 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.getWindowInsetController(view)?.isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
13、收尾!
通过几条提示来学习几个新的概念。您将创建以下内容:
13.1、用图标替换按钮
- 将
IconButton
可组合项与子级Icon
结合使用。 - 使用
material-icons-extended
工作中提供的Icons.Filled.ExpandLess
和Icons.Filled.ExpandMore
。将以下代码行添加到app/build.gradle
文件中的依赖项中。
implement "androidx.compose.material:material-icons-extended:$compose_version"
- 修改内边距以修正对其问题。
- 对无障碍功能添加内容说明
13.2、使用字符串资源
应该为“Show more”和“Show less”提供内容说明,您可以通过简单的if
语句进行添加:
contentDescription = if (expanded) "Show less" else "Show more"
不过,硬编码字符串的方式并不可取,应该从string.xml
文件中获取字符串。
您可以通过对每个字符串使用“Extract string resource”(在Android Studio中的“Context Actions”中提供)来自动执行此操作。
或者,打开app/src/res/values/strings.xml
并添加以下资源:
<string name="show_less">Show less</string>
<string name="show_more">Show more</string>
13.3、展开
“Compose ipsum”文字会在显示后显示,触发每张卡片的大小变化。
- 将新的
Text
添加到Greeting
中档内容展开式显示的Column中。 - 移除
extraPadding
并改为将animateContentSize
修饰符应用于Row
。这会自动执行创建动画的过程,而手动执行该过程会很困难。此外,也不需要再使用coerceAtLeast
。
13.4、添加高度和形状
- 您可以结合使用
shadow
修饰符和clip
修饰符来实现卡片外观。不过,有一种Material可组合项也可以做到这一点:Card
。您可以通过调用CardDefaults.cardColors
并覆盖想要更改的颜色,以此来更改Card
的颜色。
13.5、最终代码
package com.example.composepractise
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.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.dp
import com.example.composepractise.ui.theme.ComposePractiseTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePractiseTheme {
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) {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primary
),
modifier = Modifier.padding(vertical = 4.dp, horizontal = 8.dp)
) {
CardContent(name = name)
}
}
@Composable
private fun CardContent(name: String) {
var expanded by remember { 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 = "DefaultPreviewDark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
ComposePractiseTheme {
Greetings()
}
}
@Preview(showBackground = true, widthDp = 320, heightDp = 320)
@Composable
fun OnboardingPreview() {
ComposePractiseTheme {
OnboardingScreen(onContinueClicked = {})
}
}
@Preview
@Composable
fun MyAppPreview() {
ComposePractiseTheme {
MyApp(Modifier.fillMaxSize())
}
}
14、恭喜
恭喜,你已经学完了Compose的基础知识。
翻译原文:Compose 基础知识