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

书接上文,咱们继续

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相比,发出可组合项的成本相对比较低。

4.gif

10、保留状态

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

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

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

var shouldShowOnboarding by rememberSaveable { mutableStateOf(true) }

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

11、为列表添加动画效果

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

在本部分中,您将使用一个低阶API,但不用担心,它们也可以非常简单。

5.gif
为此,您将使用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规范不接受任何与实践有关的参数。它仅依赖于物理属性(阻尼和刚度),使动画更自然。立即运行该应用,查看新动画的效果:

6.gif

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

7.gif

如果您想探索不同类型的动画,请尝试为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样式。

下面我们构建应用来查看采用新样式的文本:

image.png

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

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

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

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



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

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

image.png

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

系统随即会添加一个深色模式的预览。

image.png

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

image.png

Theme.kt中,定义针对深色的调色板:

private val DarkColorScheme =  darkColorScheme(
    surface = Blue,

    onSurface = Navy,
    primary = Navy,
    onPrimary = Chartreuse
)



现在,当我们运行应用时,会看到深色的实际效果:

image.pngTheme.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、收尾!

通过几条提示来学习几个新的概念。您将创建以下内容:
7.gif

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 基础知识

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY27OER4' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片