本章深入 Android UI 的两大体系:传统 View/ViewGroup 绘制原理与自定义 View,以及现代声明式 UI 框架 Jetpack Compose 的核心机制、布局、动画、手势与性能优化。
📋 章节目录
| 节 | 主题 |
|---|---|
| 2.1 | View 绘制原理(measure / layout / draw) |
| 2.2 | 自定义 View 与 ViewGroup |
| 2.3 | Jetpack Compose 核心概念 |
| 2.4 | Compose 布局系统 |
| 2.5 | Compose 动画系统 |
| 2.6 | 手势处理 |
| 2.7 | LazyList 与滚动性能优化 |
| 2.8 | Compose 与 View 互操作 |
2.1 View 绘制原理(measure / layout / draw)
三大流程
ViewRootImpl.performTraversals()
↓
measure() ── 测量阶段:确定每个 View 的大小
↓
layout() ── 布局阶段:确定每个 View 的位置
↓
draw() ── 绘制阶段:将 View 绘制到屏幕
MeasureSpec 详解
kotlin
// MeasureSpec 是父 View 对子 View 的测量约束,由 mode + size 组成
// 打包在一个 int 中:高 2 位是 mode,低 30 位是 size
// 三种 mode:
// EXACTLY - 精确值(match_parent 或具体 dp 值)
// AT_MOST - 最大值(wrap_content)
// UNSPECIFIED - 无限制(ScrollView 中的子 View)
class SquareView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 计算期望大小
val desiredSize = 200.dp
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize // 父指定精确宽度
MeasureSpec.AT_MOST -> minOf(desiredSize, widthSize) // wrap_content
else -> desiredSize // 无约束
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> minOf(desiredSize, heightSize)
else -> desiredSize
}
// 正方形:取宽高最小值
val size = minOf(width, height)
setMeasuredDimension(size, size) // 必须调用!
}
private val Int.dp: Int get() = (this * context.resources.displayMetrics.density).toInt()
}
View 的 draw 流程
kotlin
// View.draw() 源码流程(简化)
// 1. drawBackground() - 绘制背景
// 2. onDraw() - 绘制自身内容(重写此方法)
// 3. dispatchDraw() - 绘制子 View(ViewGroup 实现)
// 4. onDrawForeground() - 绘制前景(滚动条等)
// 硬件加速的绘制流程(Android 3.0+):
// CPU → DisplayList(记录绘制命令)→ GPU 渲染
// 不是每次 invalidate() 都重新执行 onDraw(),而是重放 DisplayList
class ChartView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : View(context, attrs) {
// ✅ Paint 对象在 onDraw 外创建(避免频繁分配内存)
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.parseColor("#4CAF50")
style = Paint.Style.FILL
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
color = Color.WHITE
textSize = 14.sp
textAlign = Paint.Align.CENTER
}
private val data = listOf(60f, 80f, 45f, 90f, 70f)
private val rectF = RectF() // ✅ 复用对象
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
if (data.isEmpty()) return
val maxValue = data.max()
val barWidth = width / (data.size * 2f)
data.forEachIndexed { index, value ->
val barHeight = (value / maxValue) * height * 0.8f
val left = index * (barWidth * 2) + barWidth / 2
val top = height - barHeight
val right = left + barWidth
val bottom = height.toFloat()
rectF.set(left, top, right, bottom)
canvas.drawRoundRect(rectF, 8.dp.toFloat(), 8.dp.toFloat(), barPaint)
canvas.drawText(value.toInt().toString(), left + barWidth / 2, top - 8.dp, textPaint)
}
}
private val Int.dp: Int get() = (this * context.resources.displayMetrics.density).toInt()
private val Int.sp: Float get() = (this * context.resources.displayMetrics.scaledDensity)
}
2.2 自定义 View 与 ViewGroup
自定义属性(attrs.xml)
xml
<!-- res/values/attrs.xml -->
<resources>
<declare-styleable name="CircleProgressView">
<attr name="cpv_progress" format="float" />
<attr name="cpv_maxProgress" format="float" />
<attr name="cpv_progressColor" format="color" />
<attr name="cpv_trackColor" format="color" />
<attr name="cpv_strokeWidth" format="dimension" />
<attr name="cpv_showText" format="boolean" />
</declare-styleable>
</resources>
kotlin
// 完整的自定义圆形进度条 View
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, maxProgress)
invalidate() // 请求重绘
}
var maxProgress: Float = 100f
private var progressColor: Int = Color.parseColor("#2196F3")
private var trackColor: Int = Color.parseColor("#E0E0E0")
private var strokeWidth: Float = 12.dp
private var showText: Boolean = true
// 读取自定义属性
init {
attrs?.let {
val ta = context.obtainStyledAttributes(it, R.styleable.CircleProgressView)
try {
progress = ta.getFloat(R.styleable.CircleProgressView_cpv_progress, 0f)
maxProgress = ta.getFloat(R.styleable.CircleProgressView_cpv_maxProgress, 100f)
progressColor = ta.getColor(R.styleable.CircleProgressView_cpv_progressColor, progressColor)
trackColor = ta.getColor(R.styleable.CircleProgressView_cpv_trackColor, trackColor)
strokeWidth = ta.getDimension(R.styleable.CircleProgressView_cpv_strokeWidth, 12.dp)
showText = ta.getBoolean(R.styleable.CircleProgressView_cpv_showText, true)
} finally {
ta.recycle() // 必须 recycle!
}
}
}
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
textAlign = Paint.Align.CENTER
color = Color.parseColor("#333333")
}
private val oval = RectF()
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val padding = strokeWidth / 2
oval.set(padding, padding, w - padding, h - padding)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val desiredSize = 120.dp
val width = resolveSize(desiredSize, widthMeasureSpec)
val height = resolveSize(desiredSize, heightMeasureSpec)
val size = minOf(width, height)
setMeasuredDimension(size, size)
}
override fun onDraw(canvas: Canvas) {
// 1. 绘制轨道
trackPaint.strokeWidth = strokeWidth
trackPaint.color = trackColor
canvas.drawArc(oval, 0f, 360f, false, trackPaint)
// 2. 绘制进度弧
val sweepAngle = 360f * progress / maxProgress
progressPaint.strokeWidth = strokeWidth
progressPaint.color = progressColor
canvas.drawArc(oval, -90f, sweepAngle, false, progressPaint) // 从顶部开始
// 3. 绘制文字
if (showText) {
textPaint.textSize = width * 0.2f
val percentage = "${(progress / maxProgress * 100).toInt()}%"
val y = height / 2f - (textPaint.descent() + textPaint.ascent()) / 2
canvas.drawText(percentage, width / 2f, y, textPaint)
}
}
// 动画方法
fun animateTo(targetProgress: Float, duration: Long = 1000L) {
ValueAnimator.ofFloat(progress, targetProgress).apply {
this.duration = duration
interpolator = DecelerateInterpolator()
addUpdateListener { progress = it.animatedValue as Float }
start()
}
}
private val Int.dp: Float get() = (this * context.resources.displayMetrics.density)
}
自定义 ViewGroup(流式布局)
kotlin
// FlowLayout:子 View 自动换行
class FlowLayout @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : ViewGroup(context, attrs) {
private var horizontalSpacing = 8.dp
private var verticalSpacing = 8.dp
// 存储每行的 View 列表
private val lines = mutableListOf<List<View>>()
private val lineHeights = mutableListOf<Int>()
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val maxWidth = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight
lines.clear()
lineHeights.clear()
var currentLineViews = mutableListOf<View>()
var currentLineWidth = 0
var currentLineHeight = 0
var totalHeight = paddingTop + paddingBottom
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == GONE) continue
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, totalHeight)
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
val spacing = if (currentLineViews.isEmpty()) 0 else horizontalSpacing
if (currentLineWidth + spacing + childWidth > maxWidth && currentLineViews.isNotEmpty()) {
// 换行
lines.add(currentLineViews)
lineHeights.add(currentLineHeight)
totalHeight += currentLineHeight + verticalSpacing
currentLineViews = mutableListOf()
currentLineWidth = 0
currentLineHeight = 0
}
currentLineViews.add(child)
currentLineWidth += (if (currentLineViews.size > 1) horizontalSpacing else 0) + childWidth
currentLineHeight = maxOf(currentLineHeight, childHeight)
}
if (currentLineViews.isNotEmpty()) {
lines.add(currentLineViews)
lineHeights.add(currentLineHeight)
totalHeight += currentLineHeight
}
setMeasuredDimension(
MeasureSpec.getSize(widthMeasureSpec),
resolveSize(totalHeight, heightMeasureSpec)
)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var top = paddingTop
lines.forEachIndexed { lineIndex, lineViews ->
var left = paddingLeft
val lineHeight = lineHeights[lineIndex]
lineViews.forEach { child ->
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
// 垂直居中对齐
val childTop = top + (lineHeight - childHeight) / 2
child.layout(left, childTop, left + childWidth, childTop + childHeight)
left += childWidth + horizontalSpacing
}
top += lineHeight + verticalSpacing
}
}
override fun generateLayoutParams(attrs: AttributeSet?): LayoutParams =
MarginLayoutParams(context, attrs)
override fun generateDefaultLayoutParams(): LayoutParams =
MarginLayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
private val Int.dp: Int get() = (this * context.resources.displayMetrics.density).toInt()
}
2.3 Jetpack Compose 核心概念
Composable 函数与重组(Recomposition)
kotlin
// Compose 的核心:可组合函数
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
// 状态驱动 UI:状态变化 → 重组
@Composable
fun Counter() {
// remember:在重组间保留状态
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Count: $count",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
// rememberSaveable:跨进程重建保留状态(如横竖屏切换)
@Composable
fun PersistentCounter() {
var count by rememberSaveable { mutableStateOf(0) }
// ...
}
状态提升(State Hoisting)
kotlin
// ❌ 状态内聚(难以测试和复用)
@Composable
fun BadSearchBar() {
var query by remember { mutableStateOf("") }
TextField(value = query, onValueChange = { query = it })
}
// ✅ 状态提升(无状态组件,更好的复用和可测试性)
@Composable
fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
TextField(
value = query,
onValueChange = onQueryChange,
placeholder = { Text("搜索...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
modifier = modifier.fillMaxWidth()
)
}
// 父组件持有状态
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
Column {
SearchBar(
query = uiState.query,
onQueryChange = viewModel::onQueryChange
)
SearchResults(results = uiState.results)
}
}
derivedStateOf 与性能优化
kotlin
@Composable
fun OptimizedList() {
val listState = rememberLazyListState()
// ❌ 每次滚动都重组(因为 firstVisibleItemIndex 频繁变化)
val showScrollToTop = listState.firstVisibleItemIndex > 0
// ✅ derivedStateOf:只有 showScrollToTop 的值真正改变时才重组
val showScrollToTopOptimized by remember {
derivedStateOf { listState.firstVisibleItemIndex > 0 }
}
Box {
LazyColumn(state = listState) {
items(100) { index ->
ListItem(index = index)
}
}
AnimatedVisibility(
visible = showScrollToTopOptimized,
modifier = Modifier.align(Alignment.BottomEnd).padding(16.dp)
) {
FloatingActionButton(onClick = { /* 滚动到顶部 */ }) {
Icon(Icons.Default.KeyboardArrowUp, contentDescription = "回到顶部")
}
}
}
}
@Composable
private fun ListItem(index: Int) {
Text(
text = "Item $index",
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
2.4 Compose 布局系统
基础布局
kotlin
@Composable
fun LayoutDemo() {
// Column:垂直排列
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Row:水平排列
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("左侧内容", style = MaterialTheme.typography.bodyLarge)
Icon(Icons.Default.ArrowForward, contentDescription = null)
}
// Box:叠加布局
Box(
modifier = Modifier
.size(120.dp)
.background(MaterialTheme.colorScheme.primaryContainer, RoundedCornerShape(12.dp)),
contentAlignment = Alignment.Center
) {
Text(
"居中文字",
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
ConstraintLayout in Compose
kotlin
@Composable
fun ProfileCard() {
ConstraintLayout(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
val (avatar, name, bio, followButton) = createRefs()
AsyncImage(
model = "https://example.com/avatar.jpg",
contentDescription = "头像",
modifier = Modifier
.size(64.dp)
.clip(CircleShape)
.constrainAs(avatar) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}
)
Text(
text = "张三",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.constrainAs(name) {
top.linkTo(avatar.top)
start.linkTo(avatar.end, margin = 12.dp)
}
)
Text(
text = "Android 开发工程师 | Kotlin 爱好者",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.constrainAs(bio) {
top.linkTo(name.bottom, margin = 4.dp)
start.linkTo(avatar.end, margin = 12.dp)
end.linkTo(parent.end)
width = Dimension.fillToConstraints
}
)
Button(
onClick = { },
modifier = Modifier.constrainAs(followButton) {
top.linkTo(avatar.bottom, margin = 12.dp)
end.linkTo(parent.end)
}
) {
Text("关注")
}
}
}
自定义 Compose Layout
kotlin
// 实现 Compose 版流式布局
@Composable
fun FlowRow(
modifier: Modifier = Modifier,
horizontalSpacing: Dp = 8.dp,
verticalSpacing: Dp = 8.dp,
content: @Composable () -> Unit
) {
Layout(
content = content,
modifier = modifier
) { measurables, constraints ->
val horizontalSpacingPx = horizontalSpacing.roundToPx()
val verticalSpacingPx = verticalSpacing.roundToPx()
val placeables = measurables.map {
it.measure(constraints.copy(minWidth = 0))
}
// 计算换行
val rows = mutableListOf<List<Placeable>>()
val rowHeights = mutableListOf<Int>()
var currentRow = mutableListOf<Placeable>()
var currentRowWidth = 0
var currentRowHeight = 0
placeables.forEach { placeable ->
val spacing = if (currentRow.isEmpty()) 0 else horizontalSpacingPx
if (currentRowWidth + spacing + placeable.width > constraints.maxWidth && currentRow.isNotEmpty()) {
rows.add(currentRow)
rowHeights.add(currentRowHeight)
currentRow = mutableListOf()
currentRowWidth = 0
currentRowHeight = 0
}
currentRow.add(placeable)
currentRowWidth += spacing + placeable.width
currentRowHeight = maxOf(currentRowHeight, placeable.height)
}
if (currentRow.isNotEmpty()) {
rows.add(currentRow)
rowHeights.add(currentRowHeight)
}
val totalHeight = rowHeights.sumOf { it } + (rows.size - 1).coerceAtLeast(0) * verticalSpacingPx
layout(constraints.maxWidth, totalHeight) {
var yOffset = 0
rows.forEachIndexed { rowIndex, row ->
var xOffset = 0
row.forEach { placeable ->
placeable.placeRelative(xOffset, yOffset)
xOffset += placeable.width + horizontalSpacingPx
}
yOffset += rowHeights[rowIndex] + verticalSpacingPx
}
}
}
}
// 使用示例
@Composable
fun TagsDemo() {
val tags = listOf("Android", "Kotlin", "Compose", "Coroutines", "Hilt", "Room", "Retrofit")
FlowRow(modifier = Modifier.padding(16.dp)) {
tags.forEach { tag ->
FilterChip(
selected = false,
onClick = {},
label = { Text(tag) },
modifier = Modifier.padding(end = 4.dp, bottom = 4.dp)
)
}
}
}
2.5 Compose 动画系统
animateAsState
kotlin
@Composable
fun AnimatedBox() {
var expanded by remember { mutableStateOf(false) }
// animateAsState:单值动画
val size by animateDpAsState(
targetValue = if (expanded) 200.dp else 80.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
),
label = "size"
)
val color by animateColorAsState(
targetValue = if (expanded) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.secondary,
animationSpec = tween(durationMillis = 500),
label = "color"
)
Box(
modifier = Modifier
.size(size)
.background(color, RoundedCornerShape(if (expanded) 24.dp else 50))
.clickable { expanded = !expanded },
contentAlignment = Alignment.Center
) {
Text(
text = if (expanded) "收起" else "展开",
color = Color.White
)
}
}
Transition(多属性协调动画)
kotlin
@Composable
fun CardWithTransition() {
var selected by remember { mutableStateOf(false) }
val transition = updateTransition(
targetState = selected,
label = "card_transition"
)
val cardElevation by transition.animateDp(
transitionSpec = { spring(stiffness = Spring.StiffnessLow) },
label = "elevation"
) { isSelected ->
if (isSelected) 8.dp else 2.dp
}
val cardColor by transition.animateColor(
transitionSpec = { tween(300) },
label = "color"
) { isSelected ->
if (isSelected) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surface
}
val iconRotation by transition.animateFloat(
transitionSpec = { tween(300) },
label = "rotation"
) { isSelected ->
if (isSelected) 180f else 0f
}
Card(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.clickable { selected = !selected },
elevation = CardDefaults.cardElevation(defaultElevation = cardElevation),
colors = CardDefaults.cardColors(containerColor = cardColor)
) {
Row(
modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("点击展开")
Icon(
imageVector = Icons.Default.ExpandMore,
contentDescription = null,
modifier = Modifier.rotate(iconRotation)
)
}
}
}
AnimatedContent & AnimatedVisibility
kotlin
@Composable
fun StateTransitionDemo() {
var state by remember { mutableStateOf<ContentState>(ContentState.Loading) }
// AnimatedContent:内容切换动画
AnimatedContent(
targetState = state,
transitionSpec = {
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
},
label = "content"
) { targetState ->
when (targetState) {
ContentState.Loading -> CircularProgressIndicator()
is ContentState.Success -> SuccessContent(targetState.data)
is ContentState.Error -> ErrorContent(targetState.message)
}
}
// AnimatedVisibility:显示/隐藏动画
var visible by remember { mutableStateOf(true) }
AnimatedVisibility(
visible = visible,
enter = slideInVertically { -it } + fadeIn(),
exit = slideOutVertically { -it } + fadeOut()
) {
Banner()
}
}
sealed class ContentState {
object Loading : ContentState()
data class Success(val data: String) : ContentState()
data class Error(val message: String) : ContentState()
}
@Composable fun SuccessContent(data: String) { Text(data) }
@Composable fun ErrorContent(message: String) { Text(message, color = MaterialTheme.colorScheme.error) }
@Composable fun Banner() { Box(Modifier.fillMaxWidth().height(100.dp).background(MaterialTheme.colorScheme.primaryContainer)) }
2.6 手势处理
kotlin
@Composable
fun GestureDemo() {
var offset by remember { mutableStateOf(Offset.Zero) }
var scale by remember { mutableStateOf(1f) }
var rotation by remember { mutableStateOf(0f) }
Box(
modifier = Modifier
.fillMaxSize()
.pointerInput(Unit) {
detectTransformGestures { centroid, pan, zoom, rotationChange ->
scale = (scale * zoom).coerceIn(0.5f, 3f)
rotation += rotationChange
offset += pan
}
}
) {
Box(
modifier = Modifier
.size(150.dp)
.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
.graphicsLayer {
scaleX = scale
scaleY = scale
rotationZ = rotation
}
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(12.dp))
.align(Alignment.Center),
contentAlignment = Alignment.Center
) {
Text("拖拽 / 缩放 / 旋转", color = Color.White, textAlign = TextAlign.Center)
}
}
}
// 可拖拽卡片(带边界约束和动画)
@Composable
fun DraggableCard() {
val density = LocalDensity.current
var cardOffset by remember { mutableStateOf(Offset.Zero) }
val animatedOffset by animateOffsetAsState(
targetValue = cardOffset,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
label = "card_offset"
)
Card(
modifier = Modifier
.size(200.dp, 120.dp)
.offset {
IntOffset(
animatedOffset.x.roundToInt(),
animatedOffset.y.roundToInt()
)
}
.pointerInput(Unit) {
detectDragGestures(
onDragEnd = { cardOffset = Offset.Zero } // 松手回弹
) { change, dragAmount ->
change.consume()
cardOffset += dragAmount
}
}
) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("拖拽我")
}
}
}
2.7 LazyList 与滚动性能优化
kotlin
// 高性能列表
@Composable
fun HighPerformanceList(
items: List<Product>,
onItemClick: (Product) -> Unit
) {
val listState = rememberLazyListState()
LazyColumn(
state = listState,
contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// ✅ 使用 key 避免不必要的重组
items(
items = items,
key = { it.id }
) { product ->
ProductCard(
product = product,
onClick = { onItemClick(product) },
// ✅ Modifier 不在 items 中重新创建
modifier = Modifier.animateItemPlacement()
)
}
}
}
// ✅ 将 ProductCard 声明为 Stable,减少重组
@Stable
@Composable
fun ProductCard(
product: Product,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Row(
modifier = Modifier.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
AsyncImage(
model = product.imageUrl,
contentDescription = product.name,
modifier = Modifier
.size(64.dp)
.clip(RoundedCornerShape(8.dp)),
contentScale = ContentScale.Crop
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = product.name,
style = MaterialTheme.typography.titleSmall,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Text(
text = "¥${product.price}",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error
)
}
}
}
}
// 多类型列表
@Composable
fun MultiTypeFeed(feedItems: List<FeedItem>) {
LazyColumn {
feedItems.forEach { item ->
when (item) {
is FeedItem.Banner -> item {
BannerItem(banner = item)
}
is FeedItem.Product -> item(key = item.product.id) {
ProductCard(product = item.product, onClick = {})
}
is FeedItem.Section -> stickyHeader(key = item.title) {
SectionHeader(title = item.title)
}
is FeedItem.Grid -> item {
LazyVerticalGrid(/* ... */) { /* ... */ }
}
}
}
}
}
sealed class FeedItem {
data class Banner(val imageUrl: String) : FeedItem()
data class Product(val product: com.example.Product) : FeedItem()
data class Section(val title: String) : FeedItem()
data class Grid(val products: List<com.example.Product>) : FeedItem()
}
2.8 Compose 与 View 互操作
kotlin
// View → Compose:AndroidView
@Composable
fun LegacyMapView() {
val mapView = rememberMapView()
AndroidView(
factory = { mapView },
update = { view ->
// 更新 View 属性
},
modifier = Modifier.fillMaxSize()
)
}
@Composable
fun rememberMapView(): MapView {
val context = LocalContext.current
val mapView = remember { MapView(context) }
// 与 Compose 生命周期同步
val lifecycle = LocalLifecycleOwner.current.lifecycle
DisposableEffect(lifecycle) {
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_RESUME -> mapView.onResume()
Lifecycle.Event.ON_PAUSE -> mapView.onPause()
Lifecycle.Event.ON_DESTROY -> mapView.onDestroy()
else -> {}
}
}
lifecycle.addObserver(observer)
onDispose { lifecycle.removeObserver(observer) }
}
return mapView
}
// Compose → View:ComposeView(在传统 View 系统中嵌入 Compose)
class ProductListFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(
// Fragment 的 View 销毁时,Compose 也销毁
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
MaterialTheme {
ProductListScreen()
}
}
}
}
}
@Composable
private fun ProductListScreen() {
// Compose UI
}
Demo 代码:chapter02
kotlin
// chapter02/ComposeUIDemo.kt
package com.example.androiddemos.chapter02
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp
import kotlin.math.roundToInt
import androidx.compose.ui.unit.IntOffset
@Composable
fun Chapter02DemoScreen() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp)
) {
Text("第二章 Demo", style = MaterialTheme.typography.headlineMedium)
CounterSection()
AnimatedBoxSection()
DraggableSection()
}
}
@Composable
private fun CounterSection() {
var count by remember { mutableIntStateOf(0) }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("计数器 Demo", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Row(verticalAlignment = Alignment.CenterVertically) {
Button(onClick = { if (count > 0) count-- }) { Text("-") }
Text(
text = count.toString(),
modifier = Modifier.padding(horizontal = 24.dp),
style = MaterialTheme.typography.headlineLarge
)
Button(onClick = { count++ }) { Text("+") }
}
}
}
}
@Composable
private fun AnimatedBoxSection() {
var expanded by remember { mutableStateOf(false) }
val color by animateColorAsState(
if (expanded) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
label = "color"
)
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("动画 Demo", style = MaterialTheme.typography.titleMedium)
Spacer(Modifier.height(8.dp))
Box(
modifier = Modifier
.size(80.dp)
.background(color, RoundedCornerShape(if (expanded) 8.dp else 40.dp))
.clickable { expanded = !expanded },
contentAlignment = Alignment.Center
) {
Text(if (expanded) "收起" else "展开", color = Color.White)
}
}
}
}
@Composable
private fun DraggableSection() {
var offset by remember { mutableStateOf(Offset.Zero) }
Card(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.padding(16.dp)) {
Text("拖拽 Demo", style = MaterialTheme.typography.titleMedium)
Box(
modifier = Modifier
.fillMaxWidth()
.height(200.dp)
.background(MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape(8.dp))
) {
Box(
modifier = Modifier
.size(60.dp)
.offset { IntOffset(offset.x.roundToInt(), offset.y.roundToInt()) }
.background(MaterialTheme.colorScheme.primary, RoundedCornerShape(8.dp))
.pointerInput(Unit) {
detectDragGestures { change, dragAmount ->
change.consume()
offset += dragAmount
}
}
.align(Alignment.TopStart)
)
}
}
}
}
章节总结
| 知识点 | 必掌握程度 | 面试频率 |
|---|---|---|
| measure / layout / draw 流程 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 自定义 View(onMeasure / onDraw) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Compose Recomposition 原理 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 状态提升(State Hoisting) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| remember / rememberSaveable | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| derivedStateOf 优化 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Compose 动画(animateAsState / Transition) | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| LazyColumn 性能优化(key) | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| View 与 Compose 互操作 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
👉 下一章:第三章------状态管理与架构组件