Created
June 4, 2024 15:04
-
-
Save Sagar0-0/5843f5bbc5033dd166e9985a3559ed35 to your computer and use it in GitHub Desktop.
Custom PDFViewer in Compose
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import android.graphics.Bitmap | |
import android.graphics.pdf.PdfRenderer | |
import android.os.ParcelFileDescriptor | |
import android.util.Log | |
import androidx.compose.foundation.Image | |
import androidx.compose.foundation.background | |
import androidx.compose.foundation.border | |
import androidx.compose.foundation.gestures.rememberTransformableState | |
import androidx.compose.foundation.gestures.transformable | |
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.padding | |
import androidx.compose.foundation.lazy.LazyColumn | |
import androidx.compose.material.IconButton | |
import androidx.compose.material.MaterialTheme | |
import androidx.compose.material.Text | |
import androidx.compose.material.TextButton | |
import androidx.compose.material.icons.Icons | |
import androidx.compose.material.icons.rounded.Close | |
import androidx.compose.material3.Icon | |
import androidx.compose.runtime.Composable | |
import androidx.compose.runtime.DisposableEffect | |
import androidx.compose.runtime.LaunchedEffect | |
import androidx.compose.runtime.derivedStateOf | |
import androidx.compose.runtime.getValue | |
import androidx.compose.runtime.mutableFloatStateOf | |
import androidx.compose.runtime.mutableStateOf | |
import androidx.compose.runtime.produceState | |
import androidx.compose.runtime.remember | |
import androidx.compose.runtime.rememberCoroutineScope | |
import androidx.compose.runtime.saveable.rememberSaveable | |
import androidx.compose.runtime.setValue | |
import androidx.compose.ui.Alignment | |
import androidx.compose.ui.Modifier | |
import androidx.compose.ui.geometry.Offset | |
import androidx.compose.ui.graphics.Color | |
import androidx.compose.ui.graphics.graphicsLayer | |
import androidx.compose.ui.layout.ContentScale | |
import androidx.compose.ui.platform.LocalContext | |
import androidx.compose.ui.platform.LocalDensity | |
import androidx.compose.ui.res.stringResource | |
import androidx.compose.ui.unit.dp | |
import androidx.compose.ui.unit.sp | |
import coil.compose.rememberAsyncImagePainter | |
import coil.imageLoader | |
import coil.memory.MemoryCache | |
import coil.request.ImageRequest | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.async | |
import kotlinx.coroutines.isActive | |
import kotlinx.coroutines.launch | |
import kotlinx.coroutines.sync.Mutex | |
import kotlinx.coroutines.sync.withLock | |
import java.io.File | |
import kotlin.math.sqrt | |
//Add other Imports specific to you | |
@Composable | |
fun AppPdfViewer( | |
modifier: Modifier = Modifier, | |
url: String, | |
fileName: String, | |
verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(0.dp), | |
onClose: () -> Unit | |
) { | |
var file: File? by remember { | |
mutableStateOf(null) | |
} | |
LaunchedEffect(key1 = Unit) { | |
file = async { downloadAndGetFile(url, fileName) }.await() | |
} | |
if (file == null) { | |
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { | |
Loader() | |
} | |
} else { | |
val rendererScope = rememberCoroutineScope() | |
val mutex = remember { Mutex() } | |
val renderer by produceState<PdfRenderer?>(null, file) { | |
rendererScope.launch(Dispatchers.IO) { | |
val input = ParcelFileDescriptor.open(file, 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 | |
.fillMaxSize() | |
.background(MaterialTheme.colors.onSecondary) | |
// .aspectRatio(1f / sqrt(2f)) | |
) { | |
val width = with(LocalDensity.current) { maxWidth.toPx() }.toInt() | |
val height = (width * sqrt(2f)).toInt() | |
val pageCount by remember(renderer) { derivedStateOf { renderer?.pageCount ?: 0 } } | |
var scale by rememberSaveable { | |
mutableFloatStateOf(1f) | |
} | |
var offset by remember { | |
mutableStateOf(Offset.Zero) | |
} | |
val state = | |
rememberTransformableState { zoomChange, panChange, rotationChange -> | |
scale = (scale * zoomChange).coerceIn(1f, 5f) | |
val extraWidth = (scale - 1) * constraints.maxWidth | |
val extraHeight = (scale - 1) * constraints.maxHeight | |
val maxX = extraWidth / 2 | |
val maxY = extraHeight / 2 | |
offset = Offset( | |
x = (offset.x + scale * panChange.x).coerceIn(-maxX, maxX), | |
y = (offset.y + scale * panChange.y).coerceIn(-maxY, maxY), | |
) | |
} | |
LazyColumn( | |
modifier = Modifier | |
.fillMaxSize() | |
.graphicsLayer { | |
scaleX = scale | |
scaleY = scale | |
translationX = offset.x | |
translationX = offset.y | |
} | |
.transformable(state), | |
verticalArrangement = verticalArrangement | |
) { | |
items( | |
count = pageCount, | |
key = { index -> "${file!!.name}-$index" } | |
) { index -> | |
val cacheKey = MemoryCache.Key("${file!!.name}-$index") | |
val cacheValue: Bitmap? = imageLoader.memoryCache?.get(cacheKey)?.bitmap | |
var bitmap: Bitmap? by remember { mutableStateOf(cacheValue) } | |
if (bitmap == null) { | |
DisposableEffect(file, index) { | |
val job = imageLoadingScope.launch(Dispatchers.IO) { | |
val destinationBitmap = | |
Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) | |
mutex.withLock { | |
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) | |
.fillMaxWidth() | |
) | |
} else { | |
val request = ImageRequest.Builder(context) | |
.size(width, height) | |
.memoryCacheKey(cacheKey) | |
.data(bitmap) | |
.build() | |
Image( | |
modifier = Modifier | |
.background(Color.Transparent) | |
.border(1.dp, MaterialTheme.colors.background) | |
// .aspectRatio(1f / sqrt(2f)) | |
.fillMaxSize(), | |
contentScale = ContentScale.Fit, | |
painter = rememberAsyncImagePainter(request), | |
contentDescription = "Page ${index + 1} of $pageCount" | |
) | |
} | |
} | |
} | |
IconButton( | |
modifier = Modifier | |
.padding(10.dp) | |
.align(Alignment.TopStart), | |
onClick = onClose | |
) { | |
Icon(imageVector = Icons.Rounded.Close, contentDescription = null, tint = Teal) | |
} | |
TextButton( | |
modifier = Modifier | |
.padding(10.dp) | |
.align(Alignment.TopEnd), | |
onClick = { | |
context.sharePdf(file!!) | |
}, | |
) { | |
Text( | |
modifier = Modifier | |
.padding(vertical = 7.dp, horizontal = 15.dp), | |
text = stringResource(id = R.string.share), | |
style = newTitleStyle(fontSize = 14.sp, color = Teal) | |
) | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment