Создание масштабируемой программы просмотра PDF-файлов с помощью Jetpack ComposeAndroid

Форум для тех, кто программирует под Android
Ответить
Anonymous
 Создание масштабируемой программы просмотра PDF-файлов с помощью Jetpack Compose

Сообщение Anonymous »

Функции:
  • Поддержка аннотаций PDF.
  • Разведите пальцы, чтобы увеличить масштаб, нажмите, чтобы увеличить или уменьшить масштаб.
    Обратная совместимость
Проблемы:
Система обнаружения жестов работает не так гладко, как в PDF-приложении Google. Прежде чем активируется зум, должна быть небольшая задержка касания пальцами. Иногда он не соответствует прокрутке. Есть идеи, как его улучшить?
Я искал что-то подобное, но с обратной совместимостью и легким ожиданием.
https://developer.android. com/jetpack/androidx/releases/pdf
Любые другие улучшения/предложения приветствуются.
PS: спасибо реализация("io.legere:pdfiumandroid:1.0.24")
package com.example

import android.graphics.Bitmap
import android.os.ParcelFileDescriptor
import androidx.compose.foundation.Image
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.detectTransformGestures
import androidx.compose.foundation.gestures.scrollBy
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalViewConfiguration
import androidx.compose.ui.platform.ViewConfiguration
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
import io.legere.pdfiumandroid.suspend.PdfiumCoreKt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream

@Composable
fun PDFViewer(
stream: InputStream,
modifier: Modifier = Modifier
.fillMaxSize()
.padding(4.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp),
loader: @Composable () -> Unit = { },
pageBox: @Composable (page: @Composable () -> Unit) -> Unit = { page ->
Box {
page()
}
}
){
val scope = rememberCoroutineScope()
var file by remember { mutableStateOf(null) }
val context = LocalContext.current

LaunchedEffect(Unit) {
scope.launch(Dispatchers.IO) {
try {
file = withContext(Dispatchers.IO) {
val tempFile = File.createTempFile("pdfTempFile", "pdf", context.cacheDir)
tempFile.mkdirs()
tempFile.deleteOnExit()

tempFile.outputStream().use {
it.write(stream.readBytes())
}

tempFile
}
} catch (e: Throwable) {

}
}
}

PDFViewer(file, modifier, verticalArrangement, loader, pageBox)
}

@Composable
fun PDFViewer(
file: File?,
modifier: Modifier = Modifier
.fillMaxSize()
.padding(4.dp),
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(4.dp),
loader: @Composable () -> Unit = { },
pageBox: @Composable (page: @Composable () -> Unit) -> Unit = { page ->
Box {
page()
}
}
) {
val density = LocalDensity.current

val listState = rememberLazyListState()

var scale by remember { mutableFloatStateOf(1F) }
var offsetX by remember { mutableFloatStateOf(0f) }
val minZoom = 1F
val maxZoom = 3F

val scope = rememberCoroutineScope()

val viewConfiguration = LocalViewConfiguration.current

CompositionLocalProvider(LocalViewConfiguration provides object : ViewConfiguration by viewConfiguration {
override val touchSlop: Float
get() = viewConfiguration.touchSlop * 5f // Help to detect zoom gesture
}) {
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = modifier
) {
if (file == null) {
loader()
} else {
val pdfRender = remember {
PdfRender(
screenWidth = constraints.maxWidth,
fileDescriptor = ParcelFileDescriptor.open(
file,
ParcelFileDescriptor.MODE_READ_ONLY
)
)
}

LaunchedEffect(Unit) {
pdfRender.loadPDF()
}

DisposableEffect(key1 = Unit) {
onDispose {
pdfRender.close()
}
}

val pages by pdfRender.pages.collectAsState(listOf())

LazyColumn(
state = listState,
verticalArrangement = verticalArrangement,
modifier = Modifier
.pointerInput(Unit) {
detectTransformGestures(true) { _, pan, zoom, _ ->
scale = (scale * zoom).coerceIn(minZoom, maxZoom)

val maxT = maxOf(constraints.maxWidth * scale - constraints.maxWidth, 0f)

offsetX = (offsetX + pan.x).coerceIn(
minimumValue = -maxT / 2,
maximumValue = maxT / 2
)

scope.launch {
listState.scrollBy(-(pan.y) * (1 / scale))
}
}
}
.pointerInput(Unit) {
detectTapGestures(
onDoubleTap = { tapCenter ->
if (scale > 1.0f) {
scale = minZoom
offsetX = 0f
} else {
scale = maxZoom
val center = Pair(
first = constraints.maxWidth / 2,
second = constraints.maxHeight / 2
)

offsetX = (tapCenter.x - center.first) * scale

val yDiff = ((tapCenter.y - center.second) * scale).coerceIn(
minimumValue = -(center.second * 2f),
maximumValue = (center.second * 2f)
)

scope.launch {
listState.scrollBy(-yDiff)
}
}
}
)
}
.graphicsLayer {
scaleX = scale
scaleY = scale
translationX = offsetX
},
) {
item {
ZoomSpacer(scale, constraints.maxHeight)
}

items(pages) { page ->

val height = with(density) {
page.size.height.toDp()
}

pageBox {
// Help Lazy Lazy Layout render only visible pages
Box(modifier = Modifier.height(height)) {
var bitmap by remember { mutableStateOf(null) }

LaunchedEffect(page) {
bitmap = page.load()
}

DisposableEffect(key1 = Unit) {
//Help with garbage collection, GB runs less often and memory graph is not bumpy.
onDispose {
bitmap?.recycle()
bitmap = null
}
}

bitmap?.let {
Image(
bitmap = it.asImageBitmap(),
contentDescription = "Pdf page number: ${page.index}",
)
}
}
}
}

item {
ZoomSpacer(scale, constraints.maxHeight)
}
}
}
}
}
}

/**
* Wee need to overscroll in zoom to zoom last / first page
*/
@Composable
private fun ZoomSpacer(scale: Float, maxHeight: Int) {
val height by remember(scale, maxHeight) {
derivedStateOf {
-(((1 / scale) * maxHeight - maxHeight) / 2.0f)
}
}
val density = LocalDensity.current

val heightDp = with(density) {
height.toDp()
}

Spacer(Modifier.height(height = heightDp))
}

private class PdfRender(
private val fileDescriptor: ParcelFileDescriptor,
val screenWidth: Int
) {
private val pdfiumCore = PdfiumCoreKt(Dispatchers.Default)

lateinit var document: PdfDocumentKt

val pages = MutableStateFlow(listOf())

suspend fun loadPDF() {
document = pdfiumCore.newDocument(fileDescriptor)

pages.value = List(document.getPageCount()) {
Page(
index = it,
pdfRenderer = document,
size = document.openPage(it).use { page ->
Size(
width = screenWidth.toFloat(),
height = ((screenWidth.toFloat() / page.getPageWidthPoint()) * page.getPageHeightPoint())
)
}
)
}
}

fun close() {
document.close()
fileDescriptor.close()
}

class Page(
val index: Int,
val pdfRenderer: PdfDocumentKt,
val size: Size
) {
suspend fun load(): Bitmap {
pdfRenderer.openPage(index).use { currentPage ->
val newBitmap = Bitmap.createBitmap(
size.width.toInt(),
size.height.toInt(),
Bitmap.Config.ARGB_8888
)

currentPage.renderPageBitmap(
newBitmap,
0,
0,
size.width.toInt(),
size.height.toInt(),
renderAnnot = true
)

return newBitmap
}
}
}
}

@Composable
@Preview
private fun CebPdfViewPreview() {
val stream = LocalContext.current.assets.open("apollo.pdf")

PDFViewer(stream = stream)
}


Подробнее здесь: https://stackoverflow.com/questions/792 ... ck-compose
Ответить

Быстрый ответ

Изменение регистра текста: 
Смайлики
:) :( :oops: :roll: :wink: :muza: :clever: :sorry: :angel: :read: *x)
Ещё смайлики…
   
К этому ответу прикреплено по крайней мере одно вложение.

Если вы не хотите добавлять вложения, оставьте поля пустыми.

Максимально разрешённый размер вложения: 15 МБ.

Вернуться в «Android»