You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Compose开发PDF阅读器:缩放后VerticalPager滚动卡顿求助

解决Compose PDF阅读器缩放后滚动卡顿问题

核心问题分析

当前实现的性能瓶颈主要集中在缩放逻辑作用域过大、Bitmap绘制未优化、频繁状态更新触发全局重绘这几点上,以下是针对性的修复方案:


1. 将缩放逻辑从Pager容器移至单个页面

把graphicsLayer的缩放、平移逻辑从整个VerticalPager移到单个页面组件内,避免让Pager的布局计算、触摸事件处理都基于放大后的超大尺寸,大幅降低性能开销。

修改后的ViewPager代码

@OptIn(ExperimentalPagerApi::class, ExperimentalFoundationApi::class)
@Composable
fun ViewPager(
    list: List<Pair<ImageBitmap, List<Fields>>>,
    minScale: Float = 1f,
    maxScale: Float = 3f,
    scrollEnabled: MutableState<Boolean>,
    isRotation: Boolean = false
) {
    var targetScale by remember { mutableStateOf(1f) }
    val focusManager = LocalFocusManager.current

    VerticalPager(
        modifier= Modifier
            .fillMaxSize()
            .background(Color.Blue)
            .combinedClickable(
                interactionSource = remember { MutableInteractionSource() },
                indication = null,
                onClick = { focusManager.clearFocus() },
                onDoubleClick = {
                    targetScale = if (targetScale >= 2f) 1f else 3f
                    scrollEnabled.value = targetScale == 1f
                }
            ),
        count = list.size,
        userScrollEnabled = scrollEnabled.value,
        flingBehavior = ScrollableDefaults.flingBehavior()
    ) { page ->
        val (bitmap, fields) = list[page]
        ZoomablePage(
            imageBitmap = bitmap,
            fields = fields,
            targetScale = targetScale,
            minScale = minScale,
            maxScale = maxScale,
            scrollEnabled = scrollEnabled,
            isRotation = isRotation,
            color = if (page % 2 == 0) Color.Yellow else Color.Cyan
        )
    }
}

新增ZoomablePage封装缩放逻辑

@Composable
fun ZoomablePage(
    imageBitmap: ImageBitmap,
    fields: List<Fields>,
    targetScale: Float,
    minScale: Float,
    maxScale: Float,
    scrollEnabled: MutableState<Boolean>,
    isRotation: Boolean,
    color: Color
) {
    val scale = animateFloatAsState(targetValue = maxOf(minScale, minOf(maxScale, targetScale)))
    var rotationState by remember { mutableStateOf(0f) }
    var offsetX by remember { mutableStateOf(0f) }
    var offsetY by remember { mutableStateOf(0f) }
    val screenWidthPx = with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() }
    val screenHeightPx = with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .background(color)
            .graphicsLayer(
                optimizationFlags = GraphicsLayerOptimizeFlags.PREFER_OVERLAY,
                scaleX = scale.value,
                scaleY = scale.value,
                rotationZ = if (isRotation) rotationState else 0f,
                translationX = offsetX,
                translationY = offsetY
            )
            .pointerInput(Unit) {
                forEachGesture {
                    awaitPointerEventScope {
                        awaitFirstDown()
                        do {
                            val event = awaitPointerEvent()
                            val zoom = event.calculateZoom()
                            val newScale = targetScale * zoom
                            val offset = event.calculatePan()

                            if (newScale <= 1) {
                                offsetX = 0f
                                offsetY = 0f
                                scrollEnabled.value = true
                            } else {
                                scrollEnabled.value = false
                                offsetX += offset.x
                                offsetY += offset.y

                                // 边界限制优化,避免内容超出屏幕
                                val scaledImageWidth = screenWidthPx * scale.value
                                val scaledImageHeight = screenHeightPx * scale.value
                                offsetX = offsetX.coerceIn(-(scaledImageWidth - screenWidthPx)/2, (scaledImageWidth - screenWidthPx)/2)
                                offsetY = offsetY.coerceIn(-(scaledImageHeight - screenHeightPx)/2, (scaledImageHeight - screenHeightPx)/2)

                                if (zoom != 1f) {
                                    rotationState += event.calculateRotation()
                                }
                            }
                        } while (event.changes.any { it.pressed })
                    }
                }
            }
    ) {
        PageContent(
            modifier = Modifier.fillMaxSize(),
            imagePainter = BitmapPainter(imageBitmap),
            fields = fields
        )
    }
}

2. 优化Bitmap绘制与内存占用

直接使用原始Bitmap进行实时缩放会导致GPU处理大量冗余像素,建议:

  • 提前根据屏幕尺寸和最大缩放比例生成预缩放的Bitmap,避免运行时动态缩放
  • 加载Bitmap时指定采样率,减少内存占用:
    // 示例:根据屏幕尺寸计算采样率
    fun decodeSampledBitmapFromFile(path: String, reqWidth: Int, reqHeight: Int): Bitmap {
        val options = BitmapFactory.Options().apply {
            inJustDecodeBounds = true
            BitmapFactory.decodeFile(path, this)
            inSampleSize = calculateInSampleSize(this, reqWidth, reqHeight)
            inJustDecodeBounds = false
        }
        return BitmapFactory.decodeFile(path, options)
    }
    
  • 启用graphicsLayer的PREFER_OVERLAY优化标记,提升硬件加速效率

3. 减少不必要的重组与重绘

  • 移除代码中所有调试用的Log.e输出,避免运行时性能损耗
  • 简化自定义Layout的测量逻辑,避免重复计算:
    @Composable
    fun Page(
        modifier: Modifier = Modifier,
        fields: List<Fields>,
        pageContent: @Composable () -> Unit,
        color: Color
    ) {
        Box(modifier = modifier.fillMaxSize().background(color)) {
            Layout(content = pageContent) { measurables, constraints ->
                val imagePlaceable = measurables.first().measure(constraints)
                val fieldPlaceables = measurables.drop(1).map {
                    it.measure(constraints.copy(minWidth = 0, minHeight = 0))
                }
    
                layout(imagePlaceable.width, imagePlaceable.height) {
                    imagePlaceable.place(0, 0)
                    fieldPlaceables.forEachIndexed { index, placeable ->
                        val field = fields[index]
                        placeable.placeRelative(field.x, field.y - 70)
                    }
                }
            }
        }
    }
    

内容的提问来源于stack exchange,提问作者Ramesh Kumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.01 17:50:37