使用 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?) }
我正在尝试创建一个可组合的 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?) }