跳转至

第04章 Jetpack Compose UI框架详解

Jetpack Compose UI框架图

学习目标:全面掌握Jetpack Compose声明式UI框架,能够独立开发复杂的Android界面。

预计学习时间:7-10天 实践时间:3-4天


目录

  1. 声明式UI编程范式
  2. Composable函数基础
  3. 状态管理与重组
  4. 布局系统
  5. Material Design 3
  6. 动画与手势
  7. 导航组件
  8. 动态颜色与自适应布局
  9. 实践练习

1. 声明式UI编程范式

1.1 命令式vs声明式

Kotlin
// 命令式UI(传统XML方式)
// 需要手动操作View
val textView = findViewById<TextView>(R.id.textView)
textView.text = "Hello"
textView.setTextColor(Color.RED)
textView.visibility = View.VISIBLE

// 声明式UI(Compose方式)
// 描述UI应该是什么样子
@Composable
fun Greeting(name: String) {
    Text(
        text = "Hello, $name!",
        color = Color.Red,
        modifier = Modifier.padding(16.dp)
    )
}

1.2 Compose核心概念

Kotlin
// Composable函数 - UI的基本构建块
@Composable
fun MyScreen() {
    // 描述UI结构
    Column {
        Header()
        Content()
        Footer()
    }
}

// 预览函数 - 实时预览
@Preview(showBackground = true)
@Composable
fun MyScreenPreview() {
    MyAppTheme {
        MyScreen()
    }
}

2. Composable函数基础

2.1 基本组件

Kotlin
// Text - 文本显示
@Composable
fun TextExamples() {
    Column {
        // 简单文本,使用默认样式
        Text("Simple Text")
        // 自定义样式文本:使用Material主题排版和颜色
        Text(
            text = "Styled Text",
            style = MaterialTheme.typography.headlineLarge, // 使用大标题样式
            color = MaterialTheme.colorScheme.primary // 使用主题主色调
        )
        // 多行文本:限制最大行数并处理溢出
        Text(
            text = "Multiline text that will wrap automatically when it exceeds the available width",
            maxLines = 2, // 最多显示2行
            overflow = TextOverflow.Ellipsis // 超出部分显示省略号
        )
    }
}

// Button - 按钮组件
@Composable
fun ButtonExamples() {
    Column {
        // 填充按钮:最高强调级别的操作按钮
        Button(onClick = { /* handle click */ }) {
            Text("Click Me")
        }

        // 轮廓按钮:中等强调级别
        OutlinedButton(onClick = { }) {
            Text("Outlined")
        }

        // 文本按钮:最低强调级别
        TextButton(onClick = { }) {
            Text("Text Button")
        }

        // 图标按钮:仅包含图标,常用于工具栏
        IconButton(onClick = { }) {
            Icon(Icons.Default.Add, contentDescription = "Add") // contentDescription用于无障碍
        }
    }
}

// Image - 图片
@Composable
fun ImageExamples() {
    Column {
        // 本地资源
        Image(
            painter = painterResource(R.drawable.sample_image),
            contentDescription = "Sample Image",
            modifier = Modifier.size(100.dp)
        )

        // 网络图片(使用Coil)
        AsyncImage(
            model = "https://example.com/image.jpg",
            contentDescription = "Network Image",
            modifier = Modifier.fillMaxWidth(),
            contentScale = ContentScale.Crop
        )

        // 矢量图标
        Icon(
            imageVector = Icons.Default.Home,
            contentDescription = "Home",
            tint = MaterialTheme.colorScheme.primary
        )
    }
}

// TextField - 输入框组件
@Composable
fun TextFieldExamples() {
    // remember保存状态,mutableStateOf创建可观察的状态值
    var text by remember { mutableStateOf("") }

    Column {
        // 带轮廓的输入框:用户名输入
        OutlinedTextField(
            value = text, // 受控组件:值由状态驱动
            onValueChange = { text = it }, // 值变化时更新状态,触发重组
            label = { Text("Username") }, // 浮动标签
            placeholder = { Text("Enter username") }, // 占位提示文字
            leadingIcon = { Icon(Icons.Default.Person, null) }, // 前置图标
            singleLine = true // 限制为单行输入
        )

        // 密码输入框的状态
        var password by remember { mutableStateOf("") }
        var passwordVisible by remember { mutableStateOf(false) } // 控制密码是否可见

        // 密码输入框:带可见性切换
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            // 根据passwordVisible状态切换密码显示/隐藏
            visualTransformation = if (passwordVisible)
                VisualTransformation.None // 明文显示
            else
                PasswordVisualTransformation(), // 密码遮罩显示
            trailingIcon = {
                // 尾部图标:切换密码可见性
                IconButton(onClick = { passwordVisible = !passwordVisible }) {
                    Icon(
                        if (passwordVisible) Icons.Default.Visibility
                        else Icons.Default.VisibilityOff,
                        null
                    )
                }
            }
        )
    }
}

2.2 Modifier系统

Kotlin
@Composable
fun ModifierExamples() {
    // 尺寸
    Box(
        modifier = Modifier
            .width(100.dp)
            .height(50.dp)
            .size(100.dp) // 宽高相同
            .fillMaxWidth()
            .fillMaxHeight(0.5f) // 填充50%高度
            .wrapContentSize()
    )

    // 边距
    Box(
        modifier = Modifier
            .padding(16.dp) // 四边相同
            .padding(horizontal = 16.dp, vertical = 8.dp)
            .padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp)
    )

    // 背景与边框
    Box(
        modifier = Modifier
            .background(Color.Red)
            .background(MaterialTheme.colorScheme.primary)
            .background(Color.Red, shape = RoundedCornerShape(8.dp))
            .border(1.dp, Color.Gray)
            .border(2.dp, Color.Blue, CircleShape)
    )

    // 点击与手势
    Box(
        modifier = Modifier
            .clickable { /* 单击处理 */ }
    )

    // 多手势支持(需使用 combinedClickable)
    Box(
        modifier = Modifier
            .combinedClickable(
                onClick = { },
                onDoubleClick = { },
                onLongClick = { }
            )
    )

    // 组合Modifier(注意:Modifier链式调用的顺序影响最终效果)
    Box(
        modifier = Modifier
            .fillMaxWidth() // 1. 填满宽度
            .padding(16.dp) // 2. 外边距(在背景之外)
            .background(MaterialTheme.colorScheme.surface) // 3. 背景色
            .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp)) // 4. 边框
            .clickable { } // 5. 点击涟漪效果覆盖到边框区域
            .padding(16.dp) // 6. 内边距(在背景之内)
    )
}

3. 状态管理与重组

3.1 状态基础

Kotlin
// remember - 在重组(Recomposition)中保持状态
// 当状态变化时Compose会重新执行函数,remember确保值不会被重置
@Composable
fun Counter() {
    // by委托语法简化了状态的读写操作
    var count by remember { mutableStateOf(0) }

    // count变化时,Button内部会自动重组更新显示
    Button(onClick = { count++ }) {
        Text("Count: $count")
    }
}

// rememberSaveable - 在配置变更(如屏幕旋转)时保持状态
// 与remember不同,它会将状态保存到Bundle中
@Composable
fun StatefulTextField() {
    var text by rememberSaveable { mutableStateOf("") }

    OutlinedTextField(
        value = text,
        onValueChange = { text = it }, // 单向数据流:用户输入 -> 更新状态 -> 触发重组
        label = { Text("Input") }
    )
}

// derivedStateOf - 派生状态,仅在依赖变化时重新计算
// 避免不必要的重组,提升性能
@Composable
fun DerivedStateExample(items: List<Item>) {
    val selectedCount by remember {
        derivedStateOf { items.count { it.isSelected } } // 仅当选中数量变化时才触发重组
    }

    Text("$selectedCount items selected")
}

// rememberUpdatedState - 捕获最新的值引用
// 适用于长期运行的副作用中需要引用最新回调的场景
@Composable
fun TimerExample(
    onTimeout: () -> Unit,
    timeout: Long = 5000L
) {
    // 确保LaunchedEffect中始终调用最新的onTimeout回调
    val currentOnTimeout by rememberUpdatedState(onTimeout)

    // LaunchedEffect(true)表示仅在首次组合时启动一次
    LaunchedEffect(true) {
        delay(timeout) // 延迟指定时间
        currentOnTimeout() // 调用最新的回调
    }
}

3.2 状态提升

Kotlin
// 状态提升模式(State Hoisting)
// 将状态上移到调用方,使组件变为无状态,更易复用和测试
@Composable
fun StatefulCounter() {
    // 状态持有者:管理count状态
    var count by remember { mutableStateOf(0) }

    // 将状态和事件回调向下传递给无状态组件
    StatelessCounter(
        count = count, // 状态下传
        onIncrement = { count++ }, // 事件上传
        onDecrement = { count-- }
    )
}

// 无状态组件:只负责展示,不持有状态
@Composable
fun StatelessCounter(
    count: Int, // 接收状态
    onIncrement: () -> Unit, // 接收事件回调
    onDecrement: () -> Unit
) {
    Row(
        verticalAlignment = Alignment.CenterVertically, // 垂直居中对齐
        horizontalArrangement = Arrangement.spacedBy(16.dp) // 子元素间距16dp
    ) {
        Button(onClick = onDecrement) {
            Text("-")
        }
        Text("$count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = onIncrement) {
            Text("+")
        }
    }
}

// 在ViewModel中管理状态(推荐的生产级状态管理方式)
class CounterViewModel : ViewModel() {
    // 私有可变状态流,仅ViewModel内部可修改
    private val _count = MutableStateFlow(0)
    // 对外暴露不可变StateFlow,遵循封装原则
    val count: StateFlow<Int> = _count.asStateFlow()

    fun increment() {
        _count.value++
    }

    fun decrement() {
        _count.value--
    }
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    // collectAsState()将Flow转换为Compose的State,自动订阅更新
    val count by viewModel.count.collectAsState()

    // 方法引用简化回调传递
    StatelessCounter(
        count = count,
        onIncrement = viewModel::increment,
        onDecrement = viewModel::decrement
    )
}

3.3 副作用处理

Kotlin
// LaunchedEffect - 在Compose协程作用域中执行副作用
// key参数变化时会取消旧协程并启动新协程
@Composable
fun AutoRefreshList() {
    var items by remember { mutableStateOf(listOf<Item>()) }

    // Unit作为key表示仅在首次组合时启动,组件销毁时自动取消
    LaunchedEffect(Unit) {
        while (isActive) { // isActive确保协程未被取消
            items = fetchItems() // 从网络获取数据
            delay(5000) // 每5秒刷新一次
        }
    }

    ItemList(items)
}

// SideEffect - 每次成功重组后执行(非挂起函数)
// 适用于将Compose状态同步到非Compose系统
@Composable
fun AnalyticsExample(user: User) {
    SideEffect {
        // 每次重组后上报埋点数据
        analytics.trackScreenView("UserProfile", user.id)
    }
}

// DisposableEffect - 需要注册/注销配对操作的副作用
// key变化时先执行onDispose清理旧副作用,再执行新副作用
@Composable
fun BackHandlerExample(onBack: () -> Unit) {
    // 创建返回键回调
    val backCallback = remember {
        object : OnBackPressedCallback(true) {
            override fun handleOnBackPressed() {
                onBack()
            }
        }
    }

    // 获取返回键分发器(CompositionLocal提供)
    val backDispatcher = LocalOnBackPressedDispatcherOwner.current
        ?.onBackPressedDispatcher

    // 注册返回键回调,组件销毁时自动注销
    DisposableEffect(backDispatcher) {
        backDispatcher?.addCallback(backCallback) // 注册回调
        onDispose {
            backCallback.remove() // 清理:移除回调,防止内存泄漏
        }
    }
}

// produceState - 将非Compose状态(如Flow、回调)转换为Compose State
@Composable
fun loadImage(url: String): State<ImageBitmap?> {
    // url变化时重新加载图片
    return produceState<ImageBitmap?>(initialValue = null, url) {
        // 在IO线程加载图片,结果自动发射到State
        value = withContext(Dispatchers.IO) {
            loadImageFromNetwork(url)
        }
    }
}

4. 布局系统

4.1 基础布局

Kotlin
// Column - 垂直排列(类似LinearLayout vertical)
@Composable
fun ColumnExample() {
    Column(
        modifier = Modifier.fillMaxSize(),
        verticalArrangement = Arrangement.spacedBy(8.dp), // 子元素间距8dp
        horizontalAlignment = Alignment.CenterHorizontally // 子元素水平居中
    ) {
        Text("Item 1")
        Text("Item 2")
        Text("Item 3")
    }
}

// Row - 水平排列(类似LinearLayout horizontal)
@Composable
fun RowExample() {
    Row(
        modifier = Modifier.fillMaxWidth(),
        horizontalArrangement = Arrangement.SpaceBetween, // 子元素两端对齐,均匀分布
        verticalAlignment = Alignment.CenterVertically // 子元素垂直居中
    ) {
        Text("Left")
        Text("Center")
        Text("Right")
    }
}

// Box - 层叠布局(类似FrameLayout,子元素可重叠)
@Composable
fun BoxExample() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center // 默认子元素居中对齐
    ) {
        CircularProgressIndicator() // 底层:进度指示器
        Text("Loading...", modifier = Modifier.align(Alignment.BottomCenter)) // 上层:单独指定对齐方式
    }
}

// ConstraintLayout - 约束布局(适合复杂的相对定位场景)
@Composable
fun ConstraintLayoutExample() {
    ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
        // 创建引用,用于在约束中引用组件
        val (button, text) = createRefs()

        Button(
            onClick = { },
            modifier = Modifier.constrainAs(button) { // 为button设置约束
                top.linkTo(parent.top, margin = 16.dp) // 顶部距父容器顶部16dp
                start.linkTo(parent.start, margin = 16.dp) // 左侧距父容器左侧16dp
            }
        ) {
            Text("Button")
        }

        Text(
            "Text",
            modifier = Modifier.constrainAs(text) { // 为text设置约束
                top.linkTo(button.bottom, margin = 16.dp) // 顶部链接到button底部
                centerHorizontallyTo(parent) // 水平居中于父容器
            }
        )
    }
}

4.2 列表与网格

Kotlin
// LazyColumn - 垂直懒加载列表(仅渲染可见项,类似RecyclerView)
@Composable
fun LazyColumnExample(items: List<Item>) {
    LazyColumn(
        modifier = Modifier.fillMaxSize(),
        contentPadding = PaddingValues(16.dp), // 列表内容的内边距
        verticalArrangement = Arrangement.spacedBy(8.dp) // 列表项间距
    ) {
        // items DSL:为每个数据项创建一个列表项
        items(items) { item ->
            ItemCard(item)
        }

        // item DSL:添加单个固定项(如底部加载指示器)
        item {
            LoadingIndicator()
        }
    }
}

// LazyRow - 水平懒加载列表
@Composable
fun LazyRowExample(categories: List<Category>) {
    LazyRow(
        contentPadding = PaddingValues(horizontal = 16.dp), // 左右内边距
        horizontalArrangement = Arrangement.spacedBy(8.dp) // 项间距
    ) {
        items(categories) { category ->
            CategoryChip(category)
        }
    }
}

// LazyVerticalGrid - 垂直网格布局
@Composable
fun LazyGridExample(products: List<Product>) {
    LazyVerticalGrid(
        // Adaptive自适应列数:每列最小150dp,根据屏幕宽度自动计算列数
        columns = GridCells.Adaptive(minSize = 150.dp),
        contentPadding = PaddingValues(16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(products) { product ->
            ProductCard(product)
        }
    }
}

// 带状态的列表
@Composable
fun PaginatedList(
    items: List<Item>,
    isLoading: Boolean,
    onLoadMore: () -> Unit
) {
    // 保存列表滚动状态,用于检测滚动位置
    val listState = rememberLazyListState()

    LazyColumn(state = listState) {
        items(items) { item ->
            ItemCard(item)
        }

        // 加载中时在列表底部显示进度指示器
        if (isLoading) {
            item {
                Box(
                    modifier = Modifier.fillMaxWidth(),
                    contentAlignment = Alignment.Center
                ) {
                    CircularProgressIndicator()
                }
            }
        }
    }

    // 检测是否滚动到底部附近,触发加载更多
    val shouldLoadMore = remember {
        derivedStateOf {
            val layoutInfo = listState.layoutInfo // 获取列表布局信息
            val totalItems = layoutInfo.totalItemsCount // 总项数
            val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: 0 // 最后可见项索引
            lastVisibleItem >= totalItems - 5 // 距离底部还剩5项时触发
        }
    }

    // 当shouldLoadMore变为true时触发加载
    LaunchedEffect(shouldLoadMore.value) {
        if (shouldLoadMore.value) {
            onLoadMore()
        }
    }
}

4.3 自适应布局

Kotlin
// WindowSizeClass - 响应式布局(Material 3推荐方式)
// 根据窗口宽度分为三个断点:Compact、Medium、Expanded
@Composable
fun AdaptiveLayout(windowSizeClass: WindowSizeClass) {
    when (windowSizeClass.widthSizeClass) {
        WindowWidthSizeClass.Compact -> CompactLayout()   // 紧凑:手机竖屏(<600dp)
        WindowWidthSizeClass.Medium -> MediumLayout()     // 中等:手机横屏/折叠屏(600-840dp)
        WindowWidthSizeClass.Expanded -> ExpandedLayout() // 展开:平板/桌面(>840dp)
    }
}

// BoxWithConstraints - 根据父容器约束条件动态布局
// 提供maxWidth、maxHeight等约束信息
@Composable
fun ResponsiveCard() {
    BoxWithConstraints {
        val isCompact = maxWidth < 600.dp // 根据可用宽度判断布局模式

        if (isCompact) {
            CompactCard() // 窄屏使用紧凑卡片
        } else {
            ExpandedCard() // 宽屏使用展开卡片
        }
    }
}

// 多窗格布局:大屏显示列表+详情,小屏仅显示列表
@Composable
fun TwoPaneLayout(
    list: @Composable () -> Unit,
    detail: @Composable () -> Unit,
    windowSizeClass: WindowSizeClass
) {
    if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
        // 大屏:左右分栏,列表占1/3,详情占2/3
        Row {
            Box(modifier = Modifier.weight(1f)) { list() }
            Box(modifier = Modifier.weight(2f)) { detail() }
        }
    } else {
        // 小屏:仅显示列表,详情通过导航跳转
        list()
    }
}

5. Material Design 3

5.1 主题与颜色

Kotlin
// 定义主题
private val LightColorScheme = lightColorScheme(
    primary = md_theme_light_primary,
    onPrimary = md_theme_light_onPrimary,
    primaryContainer = md_theme_light_primaryContainer,
    onPrimaryContainer = md_theme_light_onPrimaryContainer,
    secondary = md_theme_light_secondary,
    // ... 其他颜色
)

private val DarkColorScheme = darkColorScheme(
    primary = md_theme_dark_primary,
    onPrimary = md_theme_dark_onPrimary,
    // ...
)

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    // 根据条件选择配色方案:优先动态颜色 > 暗黑模式 > 浅色模式
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
            val context = LocalContext.current
            // Android 12+支持从壁纸提取动态颜色
            if (darkTheme) dynamicDarkColorScheme(context)
            else dynamicLightColorScheme(context)
        }
        darkTheme -> DarkColorScheme // 使用预定义的暗色方案
        else -> LightColorScheme // 使用预定义的亮色方案
    }

    // MaterialTheme向下提供主题数据(通过CompositionLocal)
    MaterialTheme(
        colorScheme = colorScheme, // 颜色方案
        typography = Typography, // 排版方案
        content = content // 子内容
    )
}

// 使用主题:通过MaterialTheme.*访问当前主题属性
@Composable
fun ThemedComponents() {
    // Surface是Material容器组件,自动应用背景色和内容颜色
    Surface(
        color = MaterialTheme.colorScheme.background
    ) {
        Column {
            Text(
                "Primary Text",
                color = MaterialTheme.colorScheme.onBackground // 背景上的文字色(确保对比度)
            )
            Button(onClick = { }) {
                Text("Primary Button") // 按钮自动使用primary/onPrimary配色
            }
            Card(
                colors = CardDefaults.cardColors(
                    containerColor = MaterialTheme.colorScheme.surfaceVariant // 卡片使用表面变体色
                )
            ) {
                Text("Card Content")
            }
        }
    }
}

5.2 排版与形状

Kotlin
// 自定义Typography - Material 3定义了5个层级:Display、Headline、Title、Body、Label
// 每个层级有Large、Medium、Small三种尺寸
val Typography = Typography(
    displayLarge = TextStyle( // 最大的展示文字,用于醒目的短文本
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 57.sp, // sp单位随系统字体大小缩放
        lineHeight = 64.sp,
        letterSpacing = (-0.25).sp // 负值使字距更紧凑
    ),
    headlineMedium = TextStyle( // 中等标题,用于页面标题
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.SemiBold,
        fontSize = 28.sp,
        lineHeight = 36.sp
    ),
    bodyLarge = TextStyle( // 大号正文,用于主要阅读内容
        fontFamily = FontFamily.Default,
        fontWeight = FontWeight.Normal,
        fontSize = 16.sp,
        lineHeight = 24.sp,
        letterSpacing = 0.5.sp
    )
)

// 使用排版
@Composable
fun TypographyExample() {
    Column {
        Text(
            "Display Large",
            style = MaterialTheme.typography.displayLarge
        )
        Text(
            "Headline Medium",
            style = MaterialTheme.typography.headlineMedium
        )
        Text(
            "Body Large",
            style = MaterialTheme.typography.bodyLarge
        )
    }
}

// 自定义形状
val Shapes = Shapes(
    small = RoundedCornerShape(4.dp),
    medium = RoundedCornerShape(8.dp),
    large = RoundedCornerShape(16.dp)
)

5.3 Material组件

Kotlin
// Card - 卡片容器组件
@Composable
fun ItemCard(item: Item) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp) // 阴影高度
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(item.title, style = MaterialTheme.typography.titleMedium) // 卡片标题
            Text(item.description, style = MaterialTheme.typography.bodyMedium) // 卡片描述
        }
    }
}

// FilterChip - 过滤芯片(可选中/取消)
@Composable
fun FilterChipExample() {
    var selected by remember { mutableStateOf(false) } // 选中状态

    FilterChip(
        selected = selected,
        onClick = { selected = !selected }, // 切换选中状态
        label = { Text("Filter") },
        // 选中时显示勾选图标,未选中时不显示
        leadingIcon = if (selected) {
            { Icon(Icons.Default.Check, null) }
        } else null
    )
}

// NavigationBar - Material 3底部导航栏
@Composable
fun BottomNavExample() {
    var selectedItem by remember { mutableStateOf(0) } // 当前选中的导航项索引
    val items = listOf("Home", "Search", "Profile")

    NavigationBar {
        items.forEachIndexed { index, item ->
            NavigationBarItem(
                icon = { Icon(Icons.Default.Home, contentDescription = item) },
                label = { Text(item) },
                selected = selectedItem == index, // 高亮当前选中项
                onClick = { selectedItem = index } // 切换选中项
            )
        }
    }
}

// FloatingActionButton - 悬浮操作按钮(页面主要操作)
@Composable
fun FABExample() {
    FloatingActionButton(
        onClick = { },
        containerColor = MaterialTheme.colorScheme.primaryContainer, // 容器背景色
        contentColor = MaterialTheme.colorScheme.onPrimaryContainer // 内容(图标)颜色
    ) {
        Icon(Icons.Default.Add, "Add")
    }
}

// TopAppBar - 顶部应用栏
@Composable
fun TopBarExample(title: String, onBack: () -> Unit) {
    TopAppBar(
        title = { Text(title) }, // 标题区域
        navigationIcon = { // 导航图标(通常是返回按钮)
            IconButton(onClick = onBack) {
                Icon(Icons.Default.ArrowBack, "Back")
            }
        },
        actions = { // 操作按钮区域(右侧)
            IconButton(onClick = { }) {
                Icon(Icons.Default.Search, "Search")
            }
            IconButton(onClick = { }) {
                Icon(Icons.Default.MoreVert, "More")
            }
        }
    )
}

6. 动画与手势

6.1 基础动画

Kotlin
// animate*AsState - 最简单的动画API,值变化时自动插值过渡
@Composable
fun AnimatedBox() {
    var expanded by remember { mutableStateOf(false) } // 展开/收缩状态

    // animateDpAsState:当targetValue变化时自动执行动画
    val size by animateDpAsState(
        targetValue = if (expanded) 200.dp else 100.dp, // 目标值
        animationSpec = tween(durationMillis = 300) // 动画规格:300ms线性插值
    )

    Box(
        modifier = Modifier
            .size(size) // size会从当前值平滑过渡到目标值
            .background(MaterialTheme.colorScheme.primary)
            .clickable { expanded = !expanded } // 点击切换状态触发动画
    )
}

// 多属性动画:同时对多个属性执行动画
@Composable
fun MultiPropertyAnimation() {
    var selected by remember { mutableStateOf(false) }

    // 颜色渐变动画
    val color by animateColorAsState(
        targetValue = if (selected) MaterialTheme.colorScheme.primary
                     else MaterialTheme.colorScheme.surface
    )

    // 阴影高度动画
    val elevation by animateDpAsState(
        targetValue = if (selected) 8.dp else 2.dp
    )

    // 两个动画同步执行,实现颜色和阴影的联动过渡效果
    Card(
        modifier = Modifier.clickable { selected = !selected },
        colors = CardDefaults.cardColors(containerColor = color),
        elevation = CardDefaults.cardElevation(defaultElevation = elevation)
    ) {
        Text("Click me", modifier = Modifier.padding(16.dp))
    }
}

// AnimatedVisibility - 元素出现/消失时的过渡动画
@Composable
fun VisibilityAnimation() {
    var visible by remember { mutableStateOf(true) }

    Column {
        Button(onClick = { visible = !visible }) {
            Text(if (visible) "Hide" else "Show")
        }

        // 当visible变化时自动执行进入/退出动画
        AnimatedVisibility(
            visible = visible,
            enter = fadeIn() + slideInVertically(), // 进入动画:淡入+从上方滑入(可组合)
            exit = fadeOut() + slideOutVertically() // 退出动画:淡出+向上方滑出
        ) {
            Box(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(100.dp)
                    .background(MaterialTheme.colorScheme.primary)
            )
        }
    }
}

// AnimatedContent - 不同内容之间的切换动画
// 比AnimatedVisibility更强大,可自定义新旧内容的过渡方式
@Composable
fun ContentAnimation() {
    var count by remember { mutableStateOf(0) }

    Column {
        Button(onClick = { count++ }) {
            Text("Increment")
        }

        AnimatedContent(
            targetState = count, // 目标状态,变化时触发动画
            transitionSpec = {
                // 根据数值增减方向选择不同的动画方向
                if (targetState > initialState) {
                    // 数值增加:新内容从下方滑入,旧内容向上方滑出
                    slideInVertically { height -> height } + fadeIn() togetherWith
                    slideOutVertically { height -> -height } + fadeOut()
                } else {
                    // 数值减少:新内容从上方滑入,旧内容向下方滑出
                    slideInVertically { height -> -height } + fadeIn() togetherWith
                    slideOutVertically { height -> height } + fadeOut()
                }.using(SizeTransform(clip = false)) // 允许动画超出边界
            }
        ) { targetCount -> // lambda参数是目标状态值
            Text(
                "$targetCount",
                style = MaterialTheme.typography.headlineLarge
            )
        }
    }
}

6.2 手势处理

Kotlin
// combinedClickable(支持单击、双击、长按)
@Composable
fun ClickableExample() {
    Box(
        modifier = Modifier
            .size(100.dp)
            .background(MaterialTheme.colorScheme.primary)
            .combinedClickable(
                onClick = { /* 单击处理 */ },
                onDoubleClick = { /* 双击处理 */ },
                onLongClick = { /* 长按处理 */ }
            )
    )
}

// draggable - 单轴拖拽手势
@Composable
fun DraggableBox() {
    var offsetX by remember { mutableStateOf(0f) } // 记录水平偏移量

    Box(
        modifier = Modifier
            .offset { IntOffset(offsetX.roundToInt(), 0) } // 根据偏移量移动位置
            .size(100.dp)
            .background(MaterialTheme.colorScheme.primary)
            .draggable(
                orientation = Orientation.Horizontal, // 仅允许水平方向拖拽
                state = rememberDraggableState { delta ->
                    offsetX += delta // delta为每帧的位移增量(像素)
                }
            )
    )
}

// swipeable - 带锚点的滑动手势(常用于滑动删除、抽屉等)
@Composable
fun SwipeableCard() {
    val swipeableState = rememberSwipeableState(0) // 初始锚点状态为0
    val anchors = mapOf(0f to 0, 300f to 1) // 定义锚点:0px对应状态0,300px对应状态1

    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(100.dp)
            .swipeable(
                state = swipeableState,
                anchors = anchors, // 锚点映射
                thresholds = { _, _ -> FractionalThreshold(0.3f) }, // 滑动超过30%即切换到下一个锚点
                orientation = Orientation.Horizontal
            )
    ) {
        Card(
            modifier = Modifier.offset {
                IntOffset(swipeableState.offset.value.roundToInt(), 0) // 跟随滑动偏移
            }
        ) {
            Text("Swipe me")
        }
    }
}

// pointerInput - 自定义多点触控手势(最灵活的手势API)
@Composable
fun CustomGesture() {
    var scale by remember { mutableStateOf(1f) }    // 缩放比例
    var rotation by remember { mutableStateOf(0f) }  // 旋转角度
    var offset by remember { mutableStateOf(Offset.Zero) } // 平移偏移

    Box(
        modifier = Modifier
            .fillMaxSize()
            .pointerInput(Unit) {
                // 检测多指变换手势:同时处理平移、缩放、旋转
                detectTransformGestures { centroid, pan, zoom, rotate ->
                    offset += pan      // 累加平移量
                    scale *= zoom      // 累乘缩放因子
                    rotation += rotate  // 累加旋转角度
                }
            }
    ) {
        Image(
            painter = painterResource(R.drawable.image),
            contentDescription = null,
            modifier = Modifier
                // graphicsLayer在绘制层应用变换,不触发重新布局,性能更好
                .graphicsLayer {
                    translationX = offset.x  // X轴平移
                    translationY = offset.y  // Y轴平移
                    scaleX = scale           // X轴缩放
                    scaleY = scale           // Y轴缩放
                    rotationZ = rotation     // Z轴旋转(平面内旋转)
                }
                .fillMaxSize()
        )
    }
}

7. 导航组件

7.1 基础导航

Kotlin
// 定义路由:使用密封类确保路由类型安全
sealed class Screen(val route: String) {
    object Home : Screen("home")
    object Profile : Screen("profile/{userId}") { // {userId}为路径参数占位符
        fun createRoute(userId: String) = "profile/$userId" // 构建实际路由字符串
    }
    object Settings : Screen("settings")
}

// 设置导航图(NavGraph)
@Composable
fun AppNavigation() {
    // 创建并记住导航控制器
    val navController = rememberNavController()

    // NavHost是导航的容器,管理页面的切换和回退栈
    NavHost(
        navController = navController,
        startDestination = Screen.Home.route // 起始页面
    ) {
        // 注册Home页面路由
        composable(Screen.Home.route) {
            HomeScreen(
                onNavigateToProfile = { userId ->
                    // 导航到Profile页面,传递userId参数
                    navController.navigate(Screen.Profile.createRoute(userId))
                }
            )
        }

        // 注册Profile页面路由(带参数)
        composable(
            route = Screen.Profile.route,
            arguments = listOf(navArgument("userId") { type = NavType.StringType }) // 声明参数类型
        ) { backStackEntry ->
            // 从回退栈条目中提取参数
            val userId = backStackEntry.arguments?.getString("userId") ?: ""
            ProfileScreen(
                userId = userId,
                onBack = { navController.popBackStack() } // 返回上一页
            )
        }

        composable(Screen.Settings.route) {
            SettingsScreen()
        }
    }
}

// 带深链接的导航:支持从URL直接打开对应页面
composable(
    route = "item/{itemId}",
    deepLinks = listOf(navDeepLink { uriPattern = "https://example.com/item/{itemId}" }) // 匹配URL模式
) { backStackEntry ->
    val itemId = backStackEntry.arguments?.getString("itemId")
    ItemDetailScreen(itemId)
}

7.2 底部导航集成

Kotlin
@Composable
fun BottomNavApp() {
    val navController = rememberNavController()
    val items = listOf("home", "search", "profile")

    // Scaffold提供Material Design的基本页面结构
    Scaffold(
        bottomBar = {
            NavigationBar {
                // 监听当前导航回退栈变化,自动更新选中状态
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentRoute = navBackStackEntry?.destination?.route

                items.forEach { screen ->
                    NavigationBarItem(
                        icon = { /* icon */ },
                        label = { Text(screen) },
                        selected = currentRoute == screen, // 高亮当前路由对应的导航项
                        onClick = {
                            navController.navigate(screen) {
                                // 弹出到起始目的地,避免回退栈无限增长
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true // 保存被弹出页面的状态
                                }
                                launchSingleTop = true // 避免重复创建同一页面
                                restoreState = true // 恢复之前保存的状态
                            }
                        }
                    )
                }
            }
        }
    ) { innerPadding -> // innerPadding包含Scaffold各栏位占用的空间
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding) // 应用内边距避免内容被底栏遮挡
        ) {
            composable("home") { HomeScreen() }
            composable("search") { SearchScreen() }
            composable("profile") { ProfileScreen() }
        }
    }
}

7.3 类型安全导航(Navigation 2.8.0+)

Kotlin
// 定义序列化路由(Navigation 2.8.0+新特性)
// 使用Kotlin Serialization替代字符串路由,编译时类型检查
@Serializable
object Home // 无参数的单例路由

@Serializable
data class Profile(val userId: String) // 带参数的路由,参数自动序列化

@Serializable
object Settings

// 使用类型安全导航:无需手动拼接路由字符串
@Composable
fun TypeSafeNavigation() {
    val navController = rememberNavController()

    // 直接使用对象作为startDestination,无需字符串
    NavHost(navController, startDestination = Home) {
        composable<Home> { // 泛型参数指定路由类型
            HomeScreen(
                onNavigateToProfile = { userId ->
                    // 直接传递数据类实例,参数自动序列化
                    navController.navigate(Profile(userId))
                }
            )
        }

        composable<Profile> { backStackEntry ->
            // toRoute()自动反序列化参数,类型安全
            val profile: Profile = backStackEntry.toRoute()
            ProfileScreen(userId = profile.userId)
        }

        composable<Settings> {
            SettingsScreen()
        }
    }
}

8. 动态颜色与自适应布局

8.1 Material Design 3简介

Material Design 3(也称为Material You)是Google推出的最新设计系统,强调个性化和自适应。Jetpack Compose的Material 3组件库提供了完整的支持。

核心特性

特性 说明
动态颜色 根据壁纸自动提取配色方案
自适应布局 根据屏幕尺寸自动调整布局
改进的组件 更新的视觉风格和交互反馈
无障碍优化 更好的屏幕阅读器支持

8.2 动态颜色系统

动态颜色(Dynamic Color)是Material 3的核心特性,可以根据用户壁纸自动生成配色方案:

Kotlin
// 启用动态颜色(Material You核心特性)
@Composable
fun MyApp() {
    // 检查设备是否支持动态颜色(Android 12/API 31+)
    val dynamicColor = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S

    // 动态颜色优先级最高,其次根据系统暗黑模式选择配色
    val colorScheme = when {
        dynamicColor && isSystemInDarkTheme() -> {
            dynamicDarkColorScheme(LocalContext.current) // 从壁纸提取暗色方案
        }
        dynamicColor -> {
            dynamicLightColorScheme(LocalContext.current) // 从壁纸提取亮色方案
        }
        isSystemInDarkTheme() -> DarkColorScheme // 回退到静态暗色方案
        else -> LightColorScheme // 回退到静态亮色方案
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

颜色角色(Color Roles)

Material 3定义了一套完整的颜色角色系统:

Kotlin
// 主要颜色
MaterialTheme.colorScheme.primary          // 主要品牌色
MaterialTheme.colorScheme.onPrimary        // 在primary上的文字/图标色
MaterialTheme.colorScheme.primaryContainer // 主要容器色
MaterialTheme.colorScheme.onPrimaryContainer // 在primaryContainer上的色

// 次要颜色
MaterialTheme.colorScheme.secondary        // 次要品牌色
MaterialTheme.colorScheme.onSecondary
MaterialTheme.colorScheme.secondaryContainer
MaterialTheme.colorScheme.onSecondaryContainer

// 第三颜色
MaterialTheme.colorScheme.tertiary         // 第三品牌色(对比强调)
MaterialTheme.colorScheme.onTertiary
MaterialTheme.colorScheme.tertiaryContainer
MaterialTheme.colorScheme.onTertiaryContainer

// 表面颜色
MaterialTheme.colorScheme.surface          // 表面背景色
MaterialTheme.colorScheme.onSurface        // 在surface上的文字色
MaterialTheme.colorScheme.surfaceVariant   // 表面变体色
MaterialTheme.colorScheme.onSurfaceVariant

// 错误颜色
MaterialTheme.colorScheme.error            // 错误状态色
MaterialTheme.colorScheme.onError
MaterialTheme.colorScheme.errorContainer
MaterialTheme.colorScheme.onErrorContainer

// 轮廓
MaterialTheme.colorScheme.outline          // 边框色
MaterialTheme.colorScheme.outlineVariant   // 边框变体色

8.3 自定义配色方案

如果设备不支持动态颜色,可以定义静态配色方案:

Kotlin
// 定义颜色
val md_theme_light_primary = Color(0xFF6750A4)
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
val md_theme_light_primaryContainer = Color(0xFFEADDFF)
val md_theme_light_onPrimaryContainer = Color(0xFF21005D)

// 构建配色方案
private val LightColorScheme = lightColorScheme(
    primary = md_theme_light_primary,
    onPrimary = md_theme_light_onPrimary,
    primaryContainer = md_theme_light_primaryContainer,
    onPrimaryContainer = md_theme_light_onPrimaryContainer,
    // ... 其他颜色
)

private val DarkColorScheme = darkColorScheme(
    primary = md_theme_dark_primary,
    onPrimary = md_theme_dark_onPrimary,
    // ... 其他颜色
)

8.4 使用Material 3组件

Kotlin
@Composable
fun Material3Components() {
    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp) // 各组件间距8dp
    ) {
        // 填充按钮(Filled Button)- 最高强调,用于主要操作
        Button(onClick = { }) {
            Text("Filled Button")
        }

        // 填充色调按钮(Filled Tonal Button)- 中等强调,使用secondaryContainer色
        FilledTonalButton(onClick = { }) {
            Text("Tonal Button")
        }

        // 轮廓按钮(Outlined Button)
        OutlinedButton(onClick = { }) {
            Text("Outlined Button")
        }

        // 文本按钮(Text Button)
        TextButton(onClick = { }) {
            Text("Text Button")
        }

        // 悬浮操作按钮(FAB)
        FloatingActionButton(onClick = { }) {
            Icon(Icons.Default.Add, contentDescription = "Add")
        }

        // 扩展FAB - 带图标和文本的大号悬浮按钮
        ExtendedFloatingActionButton(
            onClick = { },
            icon = { Icon(Icons.Default.Edit, contentDescription = "Edit") },
            text = { Text("Edit") }
        )

        // 卡片 - 使用surfaceVariant作为容器背景色
        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(
                containerColor = MaterialTheme.colorScheme.surfaceVariant
            )
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = "Card Title",
                    style = MaterialTheme.typography.titleLarge
                )
                Text(
                    text = "Card content with Material 3 styling",
                    style = MaterialTheme.typography.bodyMedium
                )
            }
        }

        // 芯片(Chip)- Material 3提供四种芯片类型
        // AssistChip:辅助芯片,用于智能建议/快捷操作
        AssistChip(
            onClick = { },
            label = { Text("Assist Chip") },
            leadingIcon = {
                Icon(Icons.Default.Settings, contentDescription = null)
            }
        )

        // FilterChip:过滤芯片,可选中/取消,用于筛选条件
        FilterChip(
            selected = true, // 选中状态会改变芯片外观
            onClick = { },
            label = { Text("Filter Chip") }
        )

        // InputChip:输入芯片,用于展示用户输入的标签
        InputChip(
            selected = false,
            onClick = { },
            label = { Text("Input Chip") },
            avatar = { // 头像区域
                Icon(Icons.Default.Person, contentDescription = null)
            }
        )
    }
}

8.5 自适应布局

Material 3提供了自适应布局组件,可以根据屏幕尺寸自动调整:

Kotlin
@Composable
fun AdaptiveLayoutExample() {
    // 使用WindowSizeClass判断屏幕尺寸(Material 3自适应布局核心API)
    val windowSizeClass = calculateWindowSizeClass(activity = LocalContext.current as Activity)

    // 根据窗口宽度类别选择不同的布局策略
    when (windowSizeClass.widthSizeClass) {
        WindowWidthSizeClass.Compact -> {
            // 紧凑布局(手机竖屏,宽度<600dp):单栏、底部导航
            CompactLayout()
        }
        WindowWidthSizeClass.Medium -> {
            // 中等布局(手机横屏/平板竖屏,600-840dp):可选侧边栏
            MediumLayout()
        }
        WindowWidthSizeClass.Expanded -> {
            // 展开布局(平板横屏/桌面,>840dp):多栏、永久导航抽屉
            ExpandedLayout()
        }
    }
}

// 导航组件自适应:根据屏幕大小自动切换导航形式
@Composable
fun AdaptiveNavigation() {
    val windowSizeClass = calculateWindowSizeClass(activity = LocalContext.current as Activity)

    // PermanentNavigationDrawer:大屏设备上显示永久侧边导航抽屉
    PermanentNavigationDrawer(
        drawerContent = {
            // 仅在大屏上显示导航抽屉内容
            if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Expanded) {
                NavigationDrawerContent()
            }
        }
    ) {
        Scaffold(
            bottomBar = {
                // 仅在小屏(手机)上显示底部导航栏
                if (windowSizeClass.widthSizeClass == WindowWidthSizeClass.Compact) {
                    BottomNavigationBar()
                }
            }
        ) { padding ->
            Content(modifier = Modifier.padding(padding))
        }
    }
}

9. 实践练习

练习1:登录界面

任务:使用Compose实现一个完整的登录界面

要求: - 邮箱和密码输入框(带验证) - 登录按钮(带加载状态) - 支持暗黑模式 - 响应式布局(适配不同屏幕尺寸)

练习2:新闻列表应用

任务:实现一个新闻阅读应用

要求: - 使用LazyColumn展示新闻列表 - 支持下拉刷新和加载更多 - 点击新闻进入详情页(使用导航) - 添加收藏功能(使用动画)

练习3:图片浏览器

任务:实现一个支持手势的图片浏览器

要求: - 支持双指缩放 - 支持拖拽移动 - 支持双击放大/缩小 - 切换图片时添加过渡动画


本章小结

核心要点

  1. 声明式UI通过描述UI应该是什么样子而非如何构建来简化开发
  2. Composable函数是Compose的基本构建块,使用@Composable注解
  3. 状态管理遵循单向数据流原则,状态提升使组件更易于复用和测试
  4. Material Design 3提供完整的组件库和设计系统
  5. 动画系统强大且易于使用,支持多种动画类型

下一步

完成本章学习后,请进入第05章:MVVM架构与组件交互,学习如何构建可维护的应用架构。


本章完成时间:预计7-10天