Skip to content

Instantly share code, notes, and snippets.

@Sagar0-0
Created June 4, 2024 15:04
Show Gist options
  • Save Sagar0-0/5843f5bbc5033dd166e9985a3559ed35 to your computer and use it in GitHub Desktop.
Save Sagar0-0/5843f5bbc5033dd166e9985a3559ed35 to your computer and use it in GitHub Desktop.
Custom PDFViewer in Compose
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