Message Card

项目描述:simple UI component with declarative functions
地址:https://developer.android.com/develop/ui/compose/tutorial

Section1:Template

SubSection1:Single text

Step1: 添加文本元素

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Text("Hello world!")
        }
    }
}

Step2: 将文本元素抽象成 MessageCard 函数,并使用 @Preview 注解预览效果

// ...
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MessageCard("Android")
        }
    }
}

@Composable
fun MessageCard(name: String) {
    Text(text = "Hello $name!")
}

@Preview
@Composable
fun PreviewMessageCard() {
    MessageCard("Android")
}

这一小节我们注意到三点:

首先,是 Text() 函数的使用,它通过接收一个String参数,可以创建一个 Text 元素.接着,是将元素抽取成Composable函数 MessageCard(name: String).最后,通过Preview来进行预览上面我们抽取出来的函数

SubSection2:Multiple texts

Step1: 自定义 Message 数据类,包含两个String变量

Step2: 修改MessageCard 函数,接受 Message 变量并展示

data class Message(val author: String, val body: String)

@Composable
fun MessageCard(msg: Message) {
    Text(text = msg.author)
    Text(text = msg.body)
}

Summary

这一小节中,我们定义了一个数据类(data class) ,命名为Message,参数为两个 String 变量,authorbody,修改MessageCard使其接收并创建两个 Text 元素。

此时,这两个文本会重叠在一起,这个问题我们会在下一节中解决。

这两个小节中,我们把项目的骨架搭好了.后面会在这个骨架上继续进行开发,进行修改样式、添加图片元素等操作.做项目就像搭积木,要先把底层的搭好,才能搭上层的.


Section2:Layouts

本节中,我们将修改文本布局,添加图片元素,并引入MD3改善组件外观。

SubSection1:Modifier

Step1: 使用 Column 包裹多列文本[^Column, Row, Box]

Step2: 使用 Image 添加图片元素

Step3: 使用 modifier 自定义元素的样式,包括:

  • 图片:使用到的 Modifier 包括尺寸(size),裁剪形状(clip)
  • Spacer:修改元素间空间宽高,使用到的 Modifier 包括水平宽度(width)和垂直高度(height)
  • Row:使用到 Modifier 为 padding,作用为增加填充空间,项目P01的Iteration2中也有用到
@Composable
fun MessageCard(msg: Message) {
    // Add padding around our message
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.momoer),
            contentDescription = "Contact profile picture",
            modifier = Modifier
                // Set image size
                .size(40.dp)
                // Clip image to be shaped as a circle
                .clip(CircleShape)
        )
        // Add a horizonal space between the image and the column
        Spacer(modifier = Modifier.width(8.dp))

        Column {
            Text(text = msg.author)
            // Add a vertical space between the author and message texts
            Spacer(modifier = Modifier.height(4.dp))
            Text(text = msg.body)
        }


    }
}

Issue1: 图片导入/加载出错

这里出现了两个问题,我们来一一解析。

问题1:IDE中 Resource Manager 无法导入图片

原因:图片路径或名称中含有中文、数字或特殊字符。Android Studio无法导入路径中含中文或特殊字符的文件。

问题2:Preview中图片无法显示

原因:图片被放在了drawable-hdr 文件夹中,其属于资源限定符目录。这与Android资源加载机制相关,参见讨论一。1

Troubleshoot1:图片导入出错排查流程

优先级 排查步骤 说明
1 检查文件名 必须全小写 + 下划线,无空格/特殊字符
2 检查文件路径 放在 drawable 基础目录,不要放限定符文件夹
3 Clean + Rebuild 解决IDE的缓存问题

Summary

在本节中,我们首次接触了图片元素(Image),包括如何导入图片,如何修改图片尺寸。温习了P01中曾接触过的Modifier,未来还会多次接触这个Compose中的核心概念,需要做好心理准备。


SubSection2:Material3

使用MD3(Material Design3)风格改进 MessageCard 的外观(appearance)。

MD3三大核心:ColorTypographyShape。这三个部分,这一节都会涉及到。

Step1:使用ComposeTutorialTheme{}包裹Surface{}再包裹MessageCard()

Insight

思考:为什么不能直接ComposeTutorialTheme{} 包裹MessageCard()呢?

原因:ComposeTutorialTheme{}Surface{},它们的职责完全不同。前者只负责主题配置,后置负责提供实际的视觉容器。如果没有Surface{},组件将会缺失背景颜色。

反面案例:

// ❌ 这样写会导致背景是透明的,看不到内容
ComposeTutorialTheme {
    Column {  // 这个Column没有背景色
        Text("Hello")  // 文字会显示,但没有容器背景
    }
}

MaterialTheme单例对象

import androidx.compose.material3.MaterialTheme

核心机制: Compose 的 CompositionLocal,实现隐式传参

功能:

  1. MaterialTheme.colorScheme 可获取当前主题的完整颜色系统,返回的对象类型为 ColorScheme ,内部包括主题的多个颜色角色,如primary(主色)、secondary(次要色)、tertiary(第三色)等

  2. MaterialTheme.typography 调整文本框样式

  3. MaterialTheme.shapes 可决定当前容器的形状。

    • 它定义了MD3中不同尺寸组件的形状系统,返回的是 Shapes 对象。
    • 默认为圆角,还可以自定义为其他形状,如切角(CutCornerShape)、圆形(CircleShape)等。

示例代码:

Surface(shape = MaterialTheme.shapes.medium, shadowElevation = 1.dp) {
	Text(
        text = msg.body,
        modifier = Modifier.padding(all = 4.dp),
        style = MaterialTheme.typography.bodyMedium
	)
}
  1. 利用 Configuration.UI_MODE_NIGHT_YES@Preview 中设置黑夜模式:
    • 其中主题的亮暗色都由文件 Theme.kt 决定。
// ...
import android.content.res.Configuration

@Preview(name = "Light Mode")
@Preview(
    uiMode = Configuration.UI_MODE_NIGHT_YES,
    showBackground = true,
    name = "Dark Mode"
)
@Composable
fun PreviewMessageCard() {
   ComposeTutorialTheme {
    Surface {
      MessageCard(
        msg = Message("Lexi", "Hey, take a look at Jetpack Compose, it's great!")
      )
    }
   }
}

Section3:Lists and animations

本节中,我们将会接触如下知识:

  1. List的创建方法
  2. 简单动画的引入方法
  3. 如何让点击事件触发组件背景颜色改变的方法。

Step1:使用 LazyColumnLazyRow + Lambda表达式,实现多条消息逐条展示。

注意:Compose DSL2中,Lambda表达式是必须且最简洁的写法。

示例:

@Composable
fun Conversation(messages: List<Message>) {
    LazyColumn {
        items(messages) { message ->
            MessageCard(message)
        }
    }
}

Step2:添加折叠和展开动画效果

使用函数 remembermutableStateOf 记录状态(state)

重点:recomposition机制

示例代码:

// ...
import androidx.compose.foundation.clickable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue

class MainActivity : ComponentActivity() {
   override fun onCreate(savedInstanceState: Bundle?) {
       super.onCreate(savedInstanceState)
       setContent {
           ComposeTutorialTheme {
               Conversation(SampleData.conversationSample)
           }
       }
   }
}

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = null,
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.primary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))

        // We keep track if the message is expanded or not in this
        // variable
        var isExpanded by remember { mutableStateOf(false) }

        // We toggle the isExpanded variable when we click on this Column
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                shadowElevation = 1.dp,
            ) {
                Text(
                    text = msg.body,
                    modifier = Modifier.padding(all = 4.dp),
                    // If the message is expanded, we display all its content
                    // otherwise we only display the first line
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

Step3:为点击消息添加背景颜色

利用 isExpandedclickable Modifier 处理点击事件

// ...
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.animateContentSize

@Composable
fun MessageCard(msg: Message) {
    Row(modifier = Modifier.padding(all = 8.dp)) {
        Image(
            painter = painterResource(R.drawable.profile_picture),
            contentDescription = null,
            modifier = Modifier
                .size(40.dp)
                .clip(CircleShape)
                .border(1.5.dp, MaterialTheme.colorScheme.secondary, CircleShape)
        )
        Spacer(modifier = Modifier.width(8.dp))

        // We keep track if the message is expanded or not in this
        // variable
        var isExpanded by remember { mutableStateOf(false) }
        // surfaceColor will be updated gradually from one color to the other
        val surfaceColor by animateColorAsState(
            if (isExpanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surface,
        )

        // We toggle the isExpanded variable when we click on this Column
        Column(modifier = Modifier.clickable { isExpanded = !isExpanded }) {
            Text(
                text = msg.author,
                color = MaterialTheme.colorScheme.secondary,
                style = MaterialTheme.typography.titleSmall
            )

            Spacer(modifier = Modifier.height(4.dp))

            Surface(
                shape = MaterialTheme.shapes.medium,
                shadowElevation = 1.dp,
                // surfaceColor color will be changing gradually from primary to surface
                color = surfaceColor,
                // animateContentSize will change the Surface size gradually
                modifier = Modifier.animateContentSize().padding(1.dp)
            ) {
                Text(
                    text = msg.body,
                    modifier = Modifier.padding(all = 4.dp),
                    // If the message is expanded, we display all its content
                    // otherwise we only display the first line
                    maxLines = if (isExpanded) Int.MAX_VALUE else 1,
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }
    }
}

Summary

老实说这一节的内容是最难懂,但课程里讲的最少的。后面需要结合其他项目进一步熟悉。

General Summary

这个项目的代码量不大,但难懂的概念有一箩筐。由于它的Tutorial性质,导致注定不会讲太深。不过嘛,知识积累讲求一个循序渐进,计算机代码也是,先会模仿,后面再去加深理解。


Annotation

[^Column, Row, Box]: Jetpack Compose 里的三大核心布局组件。本身不渲染具体的视觉内容,而是专门决定他们内部的子组件如何排版和堆叠。Android Studio中被称为Widget。

The Column function lets you arrange elements vertically. You can use Row to arrange items horizontally and Box to stack elements.


  1. 系统会根据当前设备的屏幕密度、系统版本、语言等条件,选择最匹配的资源文件夹。如果你的设备或模拟器不满足 hdr 这个限定条件,系统就会完全忽略 drawable-hdr 文件夹里的所有资源。

  2. DSL(Domain-Specific Language,领域特定语言)是一种专门为解决特定领域问题设计的编程语言。Compose DSL 就是使用 Kotlin 语言特性构建的、专门用于声明式 UI 构建的领域特定语言。

评论