Pdf: rework pdf viewer a bit

This commit is contained in:
ganfra 2023-06-01 17:42:05 +02:00
parent e754d17c13
commit d8fe22a6a1
3 changed files with 225 additions and 120 deletions

View file

@ -20,36 +20,15 @@ import android.annotation.SuppressLint
import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalInspectionMode import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@ -58,22 +37,18 @@ import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView import androidx.media3.ui.PlayerView
import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper import io.element.android.features.messages.impl.media.local.exoplayer.ExoPlayerWrapper
import io.element.android.features.messages.impl.media.local.pdf.ParcelFileDescriptorFactory import io.element.android.features.messages.impl.media.local.pdf.PdfViewer
import io.element.android.features.messages.impl.media.local.pdf.PdfPage import io.element.android.features.messages.impl.media.local.pdf.rememberPdfViewerState
import io.element.android.features.messages.impl.media.local.pdf.PdfRendererManager
import io.element.android.libraries.core.mimetype.MimeTypes import io.element.android.libraries.core.mimetype.MimeTypes
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeImage
import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo import io.element.android.libraries.core.mimetype.MimeTypes.isMimeTypeVideo
import io.element.android.libraries.designsystem.R import io.element.android.libraries.designsystem.R
import io.element.android.libraries.designsystem.utils.OnLifecycleEvent import io.element.android.libraries.designsystem.utils.OnLifecycleEvent
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import me.saket.telephoto.zoomable.ZoomSpec import me.saket.telephoto.zoomable.ZoomSpec
import me.saket.telephoto.zoomable.ZoomableState import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage import me.saket.telephoto.zoomable.coil.ZoomableAsyncImage
import me.saket.telephoto.zoomable.rememberZoomableImageState import me.saket.telephoto.zoomable.rememberZoomableImageState
import me.saket.telephoto.zoomable.rememberZoomableState import me.saket.telephoto.zoomable.rememberZoomableState
import me.saket.telephoto.zoomable.zoomable
@SuppressLint("UnsafeOptInUsageError") @SuppressLint("UnsafeOptInUsageError")
@Composable @Composable
@ -84,7 +59,7 @@ fun LocalMediaView(
onReady: () -> Unit = {}, onReady: () -> Unit = {},
) { ) {
val zoomableState = rememberZoomableState( val zoomableState = rememberZoomableState(
zoomSpec = ZoomSpec(maxZoomFactor = 3f) zoomSpec = ZoomSpec(maxZoomFactor = 5f)
) )
when { when {
mimeType.isMimeTypeImage() -> MediaImageView( mimeType.isMimeTypeImage() -> MediaImageView(
@ -98,14 +73,12 @@ fun LocalMediaView(
onReady = onReady, onReady = onReady,
modifier = modifier modifier = modifier
) )
mimeType == MimeTypes.Pdf -> { mimeType == MimeTypes.Pdf -> MediaPDFView(
MediaPDFView( localMedia = localMedia,
localMedia = localMedia, zoomableState = zoomableState,
zoomableState = zoomableState, onReady = onReady,
onReady = onReady, modifier = modifier
modifier = modifier )
)
}
else -> Unit else -> Unit
} }
} }
@ -195,7 +168,6 @@ fun MediaVideoView(
} }
} }
@UnstableApi
@Composable @Composable
fun MediaPDFView( fun MediaPDFView(
localMedia: LocalMedia?, localMedia: LocalMedia?,
@ -203,89 +175,14 @@ fun MediaPDFView(
onReady: () -> Unit, onReady: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
BoxWithConstraints( val pdfViewerState = rememberPdfViewerState(
modifier = modifier.zoomable(zoomableState), model = localMedia?.model,
contentAlignment = Alignment.Center zoomableState = zoomableState
) { )
val maxWidth = this.maxWidth.dpToPx() LaunchedEffect(pdfViewerState.isLoaded) {
val lazyListState = rememberLazyListState() if (pdfViewerState.isLoaded) {
val context = LocalContext.current onReady()
val coroutineScope = rememberCoroutineScope()
var pdfRendererManager by remember {
mutableStateOf<PdfRendererManager?>(null)
}
DisposableEffect(localMedia) {
ParcelFileDescriptorFactory(context).create(localMedia?.model)
.onSuccess {
pdfRendererManager = PdfRendererManager(it, maxWidth, coroutineScope).apply {
open()
}
onReady()
}
onDispose {
pdfRendererManager?.close()
}
}
pdfRendererManager?.run {
val pdfPages = pdfPages.collectAsState().value
PdfPagesView(pdfPages.toImmutableList(), lazyListState)
} }
} }
PdfViewer(pdfViewerState = pdfViewerState, modifier = modifier)
} }
@Composable
private fun PdfPagesView(
pdfPages: ImmutableList<PdfPage>,
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically)
) {
items(pdfPages.size) { index ->
val pdfPage = pdfPages[index]
PdfPageView(pdfPage)
}
}
}
@Composable
private fun PdfPageView(
pdfPage: PdfPage,
modifier: Modifier = Modifier,
) {
val pdfPageState by pdfPage.stateFlow.collectAsState()
DisposableEffect(pdfPage) {
pdfPage.load()
onDispose {
pdfPage.close()
}
}
when (val state = pdfPageState) {
is PdfPage.State.Loaded -> {
Image(
bitmap = state.bitmap.asImageBitmap(),
contentDescription = "Page ${pdfPage.pageIndex}",
contentScale = ContentScale.FillWidth,
modifier = modifier.fillMaxWidth()
)
}
is PdfPage.State.Loading -> {
Box(
modifier = modifier
.fillMaxWidth()
.height(state.height.pxToDp())
.background(color = Color.White)
)
}
}
}
@Composable
private fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
@Composable
private fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.roundToPx() }

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import me.saket.telephoto.zoomable.zoomable
@Composable
fun PdfViewer(
pdfViewerState: PdfViewerState,
modifier: Modifier = Modifier,
) {
BoxWithConstraints(
modifier = modifier.zoomable(pdfViewerState.zoomableState),
contentAlignment = Alignment.Center
) {
val maxWidthInPx = maxWidth.dpToPx()
DisposableEffect(pdfViewerState) {
pdfViewerState.openForWidth(maxWidthInPx)
onDispose {
pdfViewerState.close()
}
}
val pdfPages = pdfViewerState.getPages()
PdfPagesView(pdfPages.toImmutableList(), pdfViewerState.lazyListState)
}
}
@Composable
private fun PdfPagesView(
pdfPages: ImmutableList<PdfPage>,
lazyListState: LazyListState,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier.fillMaxSize(),
state = lazyListState,
verticalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterVertically)
) {
items(pdfPages.size) { index ->
val pdfPage = pdfPages[index]
PdfPageView(pdfPage)
}
}
}
@Composable
private fun PdfPageView(
pdfPage: PdfPage,
modifier: Modifier = Modifier,
) {
val pdfPageState by pdfPage.stateFlow.collectAsState()
DisposableEffect(pdfPage) {
pdfPage.load()
onDispose {
pdfPage.close()
}
}
when (val state = pdfPageState) {
is PdfPage.State.Loaded -> {
Image(
bitmap = state.bitmap.asImageBitmap(),
contentDescription = "Page ${pdfPage.pageIndex}",
contentScale = ContentScale.FillWidth,
modifier = modifier.fillMaxWidth()
)
}
is PdfPage.State.Loading -> {
Box(
modifier = modifier
.fillMaxWidth()
.height(state.height.pxToDp())
.background(color = Color.White)
)
}
}
}
@Composable
private fun Int.pxToDp() = with(LocalDensity.current) { this@pxToDp.toDp() }
@Composable
private fun Dp.dpToPx() = with(LocalDensity.current) { this@dpToPx.roundToPx() }

View file

@ -0,0 +1,87 @@
/*
* Copyright (c) 2023 New Vector Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.element.android.features.messages.impl.media.local.pdf
import android.content.Context
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import kotlinx.coroutines.CoroutineScope
import me.saket.telephoto.zoomable.ZoomableState
import me.saket.telephoto.zoomable.rememberZoomableState
@Stable
class PdfViewerState(
private val model: Any?,
private val coroutineScope: CoroutineScope,
private val context: Context,
val zoomableState: ZoomableState,
val lazyListState: LazyListState,
) {
var isLoaded by mutableStateOf(false)
private var pdfRendererManager by mutableStateOf<PdfRendererManager?>(null)
@Composable
fun getPages(): List<PdfPage>{
return pdfRendererManager?.run {
pdfPages.collectAsState().value
}?: emptyList()
}
fun openForWidth(maxWidth: Int) {
ParcelFileDescriptorFactory(context).create(model)
.onSuccess {
pdfRendererManager = PdfRendererManager(it, maxWidth, coroutineScope).apply {
open()
}
isLoaded = true
}
}
fun close() {
pdfRendererManager?.close()
isLoaded = false
}
}
@Composable
fun rememberPdfViewerState(
model: Any?,
zoomableState: ZoomableState = rememberZoomableState(),
lazyListState: LazyListState = rememberLazyListState(),
context: Context = LocalContext.current,
coroutineScope: CoroutineScope = rememberCoroutineScope(),
): PdfViewerState {
return remember(model) {
PdfViewerState(
model = model,
coroutineScope = coroutineScope,
context = context,
zoomableState = zoomableState,
lazyListState = lazyListState
)
}
}