第二章:UI 开发——View 系统与 Jetpack Compose

本章深入 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 互操作 ⭐⭐⭐⭐ ⭐⭐⭐

👉 下一章:第三章------状态管理与架构组件

相关推荐
Kapaseker2 小时前
介绍一个新的 Compose 控件 — 浮动菜单
android·kotlin
空中海2 小时前
安卓 第五章:网络与数据持久化
android·网络
fengci.2 小时前
php反序列化(复习)(第五章)
android·开发语言·学习·php
美狐美颜sdk2 小时前
视频平台如何实现实时美颜?Android/iOS直播APP美颜SDK接入指南
android·前端·人工智能·ios·音视频·第三方美颜sdk·视频美颜sdk
程序猿追2 小时前
把手机变成调色盘:用 ArkUI 搓一个带放大镜效果的“UI 灵感色卡取色器”
ui·智能手机
XiaoLeisj2 小时前
Android 短视频项目实战:从登录态回流、设置页动作分发到缓存清理、协议页复用与密码重置的完整实现个人中心与设置模块
android·mvvm·webview·arouter
CYRUS_STUDIO11 小时前
Frida 源码编译全流程:自己动手编译 frida-server
android·逆向
冬奇Lab12 小时前
音视频同步与渲染:PTS、VSYNC 与 SurfaceFlinger 的协作之道
android·音视频开发