书接上文,咱们继续

9、创立高效延迟列表

现在,咱们来让称号列表更真实。到目前为止,你现已在Column中显现了两条问候语。可是,它能够处理成不计其数条问候语吗?

更改Greetings形参中的默许列表值以运用其他列表结构函数,这使您能够设置列表的大小并运用其lambda中包含的值来填充列表(这儿的$it代表列表索引):

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

这会创立1000条问候语,即便屏幕上放不下这些问候语。显然,这样做作用并不好。你能够测验在模拟器上运转此代码(警告:此代码或许会使模拟器卡住)。

为显现可翻滚列表,咱们需求运用LazyColumnLazyColumn只会烘托屏幕上可见的内容,从而在烘托大型列表时提高效率

留意: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,但不必忧虑,它们也能够非常简略。

Jetpack Compose(第二趴)——Compose 基础知识(下)
为此,您将运用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标准不承受任何与实践有关的参数。它仅依赖于物理特点(阻尼和刚度),使动画更天然。当即运转该运用,检查新动画的作用:

Jetpack Compose(第二趴)——Compose 基础知识(下)

运用animate*AsState创立的任何动画都是可中止的。这意味着,假如目标值在动画播放进程中产生变化,animate*AsState会重启动画并指向新值。中止是基于绷簧的动画中看起来特别天然:

Jetpack Compose(第二趴)——Compose 基础知识(下)

假如您想探究不同类型的动画,请测验为spring供给不同的参数,测验运用不同的标准(tweenrepeatable)和不同的函数(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、设置运用的款式和主题

到目前为止,我还没有为任何可组合设置过款式,但现已获得了一个不错的默许作用,包含支撑深色形式!下面咱们来了解一下BasicsCodelabThemeMaterialTheme.

假如你翻开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())
}

由于BasicsCodelabThemeMaterialTheme包围在其内部,因此MyApp会运用该主题中界说的特点来设置款式。从任何后台可组合项中都能够检索MaterialTheme的三个特点:colorSchemetypography、和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款式。

下面咱们构建运用来检查采用新款式的文本:

Jetpack Compose(第二趴)——Compose 基础知识(下)

一般来说,最好是将色彩、形状和字体款式放在MaterialTheme中。例如,假如对色彩进行硬编码,将会很难完成深色形式,并且需求进行很多批改工作,而这很容易造成错误。

不过,有时除了选择色彩和字体款式,您还能够基于现有的色彩或款式进行设置。

为此,您能够运用copy函数修正预界说的款式。将数组加粗:

Text(
    text = name,
    style = MaterialTheme.typography.headlineMedium.copy(
        fontWeight = FontWeight.ExtraBold
    )
)

这样一来,假如您需求更改headlineMedium的字体系列或其他任何特点,就不必忧虑呈现纤细偏差了。

现在,预览窗口中的成果应如下所示:

Jetpack Compose(第二趴)——Compose 基础知识(下)

12.1、设置深色形式预览

目前,咱们的预览仅会显现运用在浅色形式下的显现作用。运用UI_MODE_NIGHT_YESDefaultPreview增加额定的@Preview注解:

@Preview(
    showBackground = true,
    widthDp = 320,
    unMode = UI_MODE_NIGHT_YES,
    name = "Dark"
)
@Preview(showBackground = true, widthDp = 320)
@Composable
fun DefaultPreview() {
    BasicsCodelabTheme {
        Greeting()
    }
}

体系随即会增加一个深色形式的预览。

Jetpack Compose(第二趴)——Compose 基础知识(下)

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)的设备上运转您的运用。您会看到新色彩。

Jetpack Compose(第二趴)——Compose 基础知识(下)

Theme.kt中,界说针对深色的调色板:

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

现在,当咱们运转运用时,会看到深色的实际作用:

Jetpack Compose(第二趴)——Compose 基础知识(下)
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、收尾!

经过几条提示来学习几个新的概念。您将创立以下内容:

Jetpack Compose(第二趴)——Compose 基础知识(下)

13.1、用图标替换按钮

  • IconButton可组合项与子级Icon结合运用。
  • 运用material-icons-extended工作中供给的Icons.Filled.ExpandLessIcons.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 基础知识