Skip to content

Add PdfSource and RenderQuality #167

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -217,7 +219,8 @@ class PdfRendererView @JvmOverloads constructor(
pdfRendererCore,
this,
pageMargin,
enableLoadingForPages
enableLoadingForPages,
renderQuality,
)

recyclerView.apply {
Expand All @@ -230,6 +233,7 @@ class PdfRendererView @JvmOverloads constructor(
}.let { addItemDecoration(it) }
}
setZoomEnabled(isZoomEnabled)
setRenderQuality(renderQuality)
}

recyclerView.addOnScrollListener(
Expand All @@ -253,6 +257,9 @@ class PdfRendererView @JvmOverloads constructor(
recyclerView.setOnZoomChangeListener { isZoomedIn, scale ->
zoomListener?.onZoomChanged(isZoomedIn, scale)
}
recyclerView.setScrollListener { isScrolledToTop ->
scrollListener?.onScroll(isScrolledToTop)
}

recyclerView.post {
postInitializationAction?.invoke()
Expand Down Expand Up @@ -478,4 +485,8 @@ class PdfRendererView @JvmOverloads constructor(
interface ZoomListener {
fun onZoomChanged(isZoomedIn: Boolean, scale: Float)
}

interface ScrollListener {
fun onScroll(isScrolledToTop: Boolean)
}
}
18 changes: 14 additions & 4 deletions pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<PdfViewAdapter.PdfPageViewHolder>() {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder =
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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) {
Expand Down
105 changes: 68 additions & 37 deletions pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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.
*/
Expand All @@ -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)
Expand Down Expand Up @@ -171,13 +150,65 @@ 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.
*/
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
isZoomingInProgress = true
suppressLayout(true)
scrollListener?.invoke(false)
isOnTop = false
return true
}

Expand Down
5 changes: 5 additions & 0 deletions pdfViewer/src/main/java/com/rajat/pdfviewer/RenderQuality.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.rajat.pdfviewer

enum class RenderQuality(val qualityMultiplier: Float) {
NORMAL(qualityMultiplier = 1f), HIGH(qualityMultiplier = 2f), ULTRA(qualityMultiplier = 3f)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -69,6 +72,8 @@ fun PdfRendererViewCompose(
update = { view ->
view.statusListener = combinedCallback
view.zoomListener = zoomListener
view.scrollListener = scrollListener
view.renderQuality = renderQuality

if (!initialized) {
when (source) {
Expand Down