Pdf: rework pdf viewer a bit
This commit is contained in:
parent
e754d17c13
commit
d8fe22a6a1
3 changed files with 225 additions and 120 deletions
|
|
@ -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() }
|
|
||||||
|
|
|
||||||
|
|
@ -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() }
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue