第04章 Jetpack Compose UI框架详解¶
学习目标:全面掌握Jetpack Compose声明式UI框架,能够独立开发复杂的Android界面。
预计学习时间:7-10天 实践时间:3-4天
目录¶
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:图片浏览器¶
任务:实现一个支持手势的图片浏览器
要求: - 支持双指缩放 - 支持拖拽移动 - 支持双击放大/缩小 - 切换图片时添加过渡动画
本章小结¶
核心要点¶
- 声明式UI通过描述UI应该是什么样子而非如何构建来简化开发
- Composable函数是Compose的基本构建块,使用@Composable注解
- 状态管理遵循单向数据流原则,状态提升使组件更易于复用和测试
- Material Design 3提供完整的组件库和设计系统
- 动画系统强大且易于使用,支持多种动画类型
下一步¶
完成本章学习后,请进入第05章:MVVM架构与组件交互,学习如何构建可维护的应用架构。
本章完成时间:预计7-10天