diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt index fb1a55f..44579e0 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt @@ -76,7 +76,9 @@ class PdfRendererView @JvmOverloads constructor( // endregion var zoomListener: ZoomListener? = null + var scrollListener: ScrollListener? = null var statusListener: StatusCallBack? = null + var renderQuality: RenderQuality = RenderQuality.NORMAL //region Public APIs fun isZoomedIn(): Boolean = this::recyclerView.isInitialized && recyclerView.isZoomedIn() @@ -149,7 +151,9 @@ class PdfRendererView @JvmOverloads constructor( */ fun initWithFile(file: File, cacheStrategy: CacheStrategy = CacheStrategy.MAXIMIZE_PERFORMANCE) { this.cacheStrategy = cacheStrategy - val cacheIdentifier = file.name + val fileSize = runCatching { file.length() }.getOrElse { -1L } + val lastModified = runCatching { file.lastModified() }.getOrElse { -1L } + val cacheIdentifier = file.name + "_$fileSize" + "_$lastModified" // Notify loading started statusListener?.onPdfRenderStart() @@ -217,7 +221,8 @@ class PdfRendererView @JvmOverloads constructor( pdfRendererCore, this, pageMargin, - enableLoadingForPages + enableLoadingForPages, + renderQuality, ) recyclerView.apply { @@ -230,6 +235,7 @@ class PdfRendererView @JvmOverloads constructor( }.let { addItemDecoration(it) } } setZoomEnabled(isZoomEnabled) + setRenderQuality(renderQuality) } recyclerView.addOnScrollListener( @@ -253,6 +259,9 @@ class PdfRendererView @JvmOverloads constructor( recyclerView.setOnZoomChangeListener { isZoomedIn, scale -> zoomListener?.onZoomChanged(isZoomedIn, scale) } + recyclerView.setScrollListener { isScrolledToTop -> + scrollListener?.onScroll(isScrolledToTop) + } recyclerView.post { postInitializationAction?.invoke() @@ -478,4 +487,8 @@ class PdfRendererView @JvmOverloads constructor( interface ZoomListener { fun onZoomChanged(isZoomedIn: Boolean, scale: Float) } + + interface ScrollListener { + fun onScroll(isScrolledToTop: Boolean) + } } diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt index 637721a..a8233ac 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt @@ -25,7 +25,8 @@ internal class PdfViewAdapter( private val renderer: PdfRendererCore, private val parentView: PdfRendererView, private val pageSpacing: Rect, - private val enableLoadingForPages: Boolean + private val enableLoadingForPages: Boolean, + private val renderQuality: RenderQuality, ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder = @@ -75,6 +76,11 @@ internal class PdfViewAdapter( if (cached != null && currentBoundPage == position) { if (DEBUG_LOGS_ENABLED) Log.d("PdfViewAdapter", "✅ Loaded page $position from cache") + val aspectRatio = runCatching { + cached.width.toFloat() / cached.height.toFloat() + }.getOrElse { 1f } + val height = (displayWidth / aspectRatio).toInt() + itemBinding.updateLayoutParams(height) itemBinding.pageView.setImageBitmap(cached) hasRealBitmap = true applyFadeInAnimation(itemBinding.pageView) @@ -85,11 +91,15 @@ internal class PdfViewAdapter( renderer.getPageDimensionsAsync(position) { size -> if (currentBoundPage != position) return@getPageDimensionsAsync - val aspectRatio = size.width.toFloat() / size.height.toFloat() + val aspectRatio = runCatching { + size.width.toFloat() / size.height.toFloat() + }.getOrElse { 1f } val height = (displayWidth / aspectRatio).toInt() itemBinding.updateLayoutParams(height) - renderAndApplyBitmap(position, displayWidth, height) + val bitmapWidth = (displayWidth * renderQuality.qualityMultiplier).toInt() + val bitmapHeight = (height * renderQuality.qualityMultiplier).toInt() + renderAndApplyBitmap(position, bitmapWidth, bitmapHeight) } } @@ -127,7 +137,7 @@ internal class PdfViewAdapter( } private fun retryRenderOnce(page: Int, width: Int, height: Int) { - val retryBitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height) + val retryBitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, maxOf(1, height)) renderer.renderPage(page, retryBitmap) { success, retryPageNo, rendered -> scope.launch { if (success && retryPageNo == currentBoundPage && !hasRealBitmap) { diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt index d4d9886..b8bb101 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt @@ -23,11 +23,13 @@ class PinchZoomRecyclerView @JvmOverloads constructor( private val gestureDetector = GestureDetector(context, GestureListener()) // Zoom and pan state + private var renderQuality = RenderQuality.NORMAL private var scaleFactor = 1f private var isZoomEnabled = true - private var maxZoom = MAX_ZOOM + private val maxZoom get() = MAX_ZOOM * renderQuality.qualityMultiplier private var zoomDuration = ZOOM_DURATION private var isZoomingInProgress = false + private var isOnTop = true // Panning offsets and touch memory private var lastTouchX = 0f @@ -36,6 +38,7 @@ class PinchZoomRecyclerView @JvmOverloads constructor( private var posY = 0f private var zoomChangeListener: ((Boolean, Float) -> Unit)? = null + private var scrollListener: ((Boolean) -> Unit)? = null init { setWillNotDraw(false) @@ -53,6 +56,14 @@ class PinchZoomRecyclerView @JvmOverloads constructor( zoomChangeListener = listener } + fun setScrollListener(listener: (isScrolledToTop: Boolean) -> Unit) { + scrollListener = listener + } + + fun setRenderQuality(quality: RenderQuality) { + renderQuality = quality + } + /** * Handles touch interactions — zoom, pan, and scroll. */ @@ -67,42 +78,10 @@ class PinchZoomRecyclerView @JvmOverloads constructor( } when (ev.actionMasked) { - MotionEvent.ACTION_DOWN -> { - lastTouchX = ev.x - lastTouchY = ev.y - activePointerId = ev.getPointerId(0) - } - MotionEvent.ACTION_MOVE -> { - if (!scaleDetector.isInProgress && scaleFactor > 1f) { - val pointerIndex = ev.findPointerIndex(activePointerId) - if (pointerIndex != -1) { - val x = ev.getX(pointerIndex) - val y = ev.getY(pointerIndex) - val dx = x - lastTouchX - val dy = y - lastTouchY - posX += dx - posY += dy - clampPosition() - invalidate() - - lastTouchX = x - lastTouchY = y - } - } - } - MotionEvent.ACTION_POINTER_UP -> { - val pointerIndex = ev.actionIndex - val pointerId = ev.getPointerId(pointerIndex) - if (pointerId == activePointerId) { - val newPointerIndex = if (pointerIndex == 0) 1 else 0 - lastTouchX = ev.getX(newPointerIndex) - lastTouchY = ev.getY(newPointerIndex) - activePointerId = ev.getPointerId(newPointerIndex) - } - } - MotionEvent.ACTION_CANCEL -> { - activePointerId = INVALID_POINTER_ID - } + MotionEvent.ACTION_DOWN -> onDown(ev = ev) + MotionEvent.ACTION_MOVE -> onMove(ev = ev) + MotionEvent.ACTION_POINTER_UP -> onUp(ev = ev) + MotionEvent.ACTION_CANCEL -> onCancel(ev = ev) } return if (scaleFactor > 1f) true else super.onTouchEvent(ev) @@ -171,6 +150,56 @@ class PinchZoomRecyclerView @JvmOverloads constructor( return (averageHeight * itemCount * scaleFactor).toInt() } + private fun onDown(ev: MotionEvent) { + lastTouchX = ev.x + lastTouchY = ev.y + activePointerId = ev.getPointerId(0) + } + + private fun onMove(ev: MotionEvent) { + val pointerIndex = ev.findPointerIndex(activePointerId) + if (pointerIndex != -1) { + if (!scaleDetector.isInProgress && scaleFactor > 1f) { + val x = ev.getX(pointerIndex) + val y = ev.getY(pointerIndex) + val dx = x - lastTouchX + val dy = y - lastTouchY + posX += dx + posY += dy + clampPosition() + invalidate() + + lastTouchX = x + lastTouchY = y + } + + val isScrolledOut = !scaleDetector.isInProgress && scaleFactor == 1f + val currentScrollOffset = computeVerticalScrollOffset() + if (currentScrollOffset == 0 && isScrolledOut && !isOnTop) { + scrollListener?.invoke(true) + isOnTop = true + } else if ((currentScrollOffset != 0 || isScrolledOut.not()) && isOnTop) { + scrollListener?.invoke(false) + isOnTop = false + } + } + } + + private fun onUp(ev: MotionEvent) { + val pointerIndex = ev.actionIndex + val pointerId = ev.getPointerId(pointerIndex) + if (pointerId == activePointerId) { + val newPointerIndex = if (pointerIndex == 0) 1 else 0 + lastTouchX = ev.getX(newPointerIndex) + lastTouchY = ev.getY(newPointerIndex) + activePointerId = ev.getPointerId(newPointerIndex) + } + } + + private fun onCancel(ev: MotionEvent) { + activePointerId = INVALID_POINTER_ID + } + /** * Handles pinch-to-zoom scaling with focal-point centering. */ @@ -178,6 +207,8 @@ class PinchZoomRecyclerView @JvmOverloads constructor( override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { isZoomingInProgress = true suppressLayout(true) + scrollListener?.invoke(false) + isOnTop = false return true } diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt new file mode 100644 index 0000000..241e06d --- /dev/null +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt @@ -0,0 +1,5 @@ +package com.rajat.pdfviewer + +enum class RenderQuality(val qualityMultiplier: Float) { + NORMAL(qualityMultiplier = 1f), HIGH(qualityMultiplier = 2f), ULTRA(qualityMultiplier = 3f) +} \ No newline at end of file diff --git a/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt b/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt index 7d6eb41..1a06516 100644 --- a/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt +++ b/pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt @@ -14,6 +14,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.lifecycleScope import com.rajat.pdfviewer.HeaderData import com.rajat.pdfviewer.PdfRendererView +import com.rajat.pdfviewer.RenderQuality import com.rajat.pdfviewer.util.CacheStrategy import com.rajat.pdfviewer.util.FileUtils.fileFromAsset import com.rajat.pdfviewer.util.PdfSource @@ -23,12 +24,14 @@ import java.io.File fun PdfRendererViewCompose( source: PdfSource, modifier: Modifier = Modifier, + renderQuality: RenderQuality = RenderQuality.NORMAL, headers: HeaderData = HeaderData(), cacheStrategy: CacheStrategy = CacheStrategy.MAXIMIZE_PERFORMANCE, lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, jumpToPage: Int? = null, statusCallBack: PdfRendererView.StatusCallBack? = null, zoomListener: PdfRendererView.ZoomListener? = null, + scrollListener: PdfRendererView.ScrollListener? = null, onReady: ((PdfRendererView) -> Unit)? = null, ) { val context = LocalContext.current @@ -69,6 +72,8 @@ fun PdfRendererViewCompose( update = { view -> view.statusListener = combinedCallback view.zoomListener = zoomListener + view.scrollListener = scrollListener + view.renderQuality = renderQuality if (!initialized) { when (source) {