使用 PdfRenderer 在 Jetpack Compose 中创建 PDF 查看器

Create a PDF Viewer in Jetpack Compose using PdfRenderer

我正在尝试创建一个可组合的 PDF 查看器,使用 PdfRenderer and Coil 将位图加载到 LazyColumn。 这是我到目前为止得到的:

@Composable
fun PdfViewer(
    modifier: Modifier = Modifier,
    uri: Uri,
    verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp)
) {
    val loaderScope = rememberCoroutineScope()
    val renderer = remember(uri) {
        val input = ParcelFileDescriptor.open(uri.toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
        PdfRenderer(input)
    }
    val context = LocalContext.current
    val mutex = remember { Mutex() }
    val imageLoader = LocalContext.current.imageLoader
    BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
        val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
        val height = (width * sqrt(2f)).toInt()
        LazyColumn(
            verticalArrangement = verticalArrangement
        ) {
            items(
                count = renderer.pageCount,
                key = { it }
            ) { index ->
                val cacheKey = MemoryCache.Key("$uri-$index")
                val bitmap = remember(uri, index) {
                    val cachedBitmap = imageLoader.memoryCache[cacheKey]
                    if (cachedBitmap != null) cachedBitmap else {
                        val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
                        loaderScope.launch(Dispatchers.IO) {
                            mutex.withLock {
                                Timber.d("Loading $uri - page $index")
                                renderer.openPage(index).use {
                                    it.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                                }
                            }
                        }
                        bitmap
                    }
                }
                val request = ImageRequest.Builder(context)
                    .size(width, height)
                    .memoryCacheKey(cacheKey)
                    .data(bitmap)
                    .build()

                Image(
                    modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth(),
                    contentScale = ContentScale.Fit,
                    painter = rememberImagePainter(request),
                    contentDescription = "Page ${index + 1} of ${renderer.pageCount}"
                )
            }
        }
    }
}

这种方法可行,但是当位图首次加载时,它不会显示在列表中,直到我滚动(即重绘之后)。我想利用 LazyColumn 的功能,只在 PDF 页面可见时加载它们。

有没有更好的方法来实现这个?

我是这样解决的:

@Composable
fun PdfViewer(
    modifier: Modifier = Modifier,
    uri: Uri,
    verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp)
) {
    val rendererScope = rememberCoroutineScope()
    val mutex = remember { Mutex() }
    val renderer by produceState<PdfRenderer?>(null, uri) {
        rendererScope.launch(Dispatchers.IO) {
            val input = ParcelFileDescriptor.open(uri.toFile(), ParcelFileDescriptor.MODE_READ_ONLY)
            value = PdfRenderer(input)
        }
        awaitDispose {
            val currentRenderer = value
            rendererScope.launch(Dispatchers.IO) {
                mutex.withLock {
                    currentRenderer?.close()
                }
            }
        }
    }
    val context = LocalContext.current
    val imageLoader = LocalContext.current.imageLoader
    val imageLoadingScope = rememberCoroutineScope()
    BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
        val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt()
        val height = (width * sqrt(2f)).toInt()
        val pageCount by remember(renderer) { derivedStateOf { renderer?.pageCount ?: 0 } }
        LazyColumn(
            verticalArrangement = verticalArrangement
        ) {
            items(
                count = pageCount,
                key = { index -> "$uri-$index" }
            ) { index ->
                val cacheKey = MemoryCache.Key("$uri-$index")
                var bitmap by remember { mutableStateOf(imageLoader.memoryCache[cacheKey]) }
                if (bitmap == null) {
                    DisposableEffect(uri, index) {
                        val job = imageLoadingScope.launch(Dispatchers.IO) {
                            val destinationBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
                            mutex.withLock {
                                Timber.d("Loading PDF $uri - page $index/$pageCount")
                                if (!coroutineContext.isActive) return@launch
                                try {
                                    renderer?.let {
                                        it.openPage(index).use { page ->
                                            page.render(
                                                destinationBitmap,
                                                null,
                                                null,
                                                PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY
                                            )
                                        }
                                    }
                                } catch (e: Exception) {
                                    //Just catch and return in case the renderer is being closed
                                    return@launch
                                }
                            }
                            bitmap = destinationBitmap
                        }
                        onDispose {
                            job.cancel()
                        }
                    }
                    Box(modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth())
                } else {
                    val request = ImageRequest.Builder(context)
                        .size(width, height)
                        .memoryCacheKey(cacheKey)
                        .data(bitmap)
                        .build()

                    Image(
                        modifier = Modifier.background(Color.White).aspectRatio(1f / sqrt(2f)).fillMaxWidth(),
                        contentScale = ContentScale.Fit,
                        painter = rememberImagePainter(request),
                        contentDescription = "Page ${index + 1} of $pageCount"
                    )
                }
            }
        }
    }
}

这也应该处理 pdf 渲染器的处理。

使用 Jetpack Compose 显示 PDF 的绝佳方式。

但是,要正确使用,您必须更改此行:

var bitmap by remember { mutableStateOf(imageLoader.memoryCache[cacheKey]) }

通过这个:

var bitmap by remember { mutableStateOf(imageLoader.memoryCache?.get(cacheKey) as? Bitmap?) }