Skip to content

Commit c9263a8

Browse files
miseke-ffwrschattauer
authored andcommitted
Implement render quality and scroll to top listener
1 parent a9ac5de commit c9263a8

File tree

5 files changed

+101
-41
lines changed

5 files changed

+101
-41
lines changed

pdfViewer/src/main/java/com/rajat/pdfviewer/PdfRendererView.kt

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ class PdfRendererView @JvmOverloads constructor(
7676
// endregion
7777

7878
var zoomListener: ZoomListener? = null
79+
var scrollListener: ScrollListener? = null
7980
var statusListener: StatusCallBack? = null
81+
var renderQuality: RenderQuality = RenderQuality.NORMAL
8082

8183
//region Public APIs
8284
fun isZoomedIn(): Boolean = this::recyclerView.isInitialized && recyclerView.isZoomedIn()
@@ -217,7 +219,8 @@ class PdfRendererView @JvmOverloads constructor(
217219
pdfRendererCore,
218220
this,
219221
pageMargin,
220-
enableLoadingForPages
222+
enableLoadingForPages,
223+
renderQuality,
221224
)
222225

223226
recyclerView.apply {
@@ -230,6 +233,7 @@ class PdfRendererView @JvmOverloads constructor(
230233
}.let { addItemDecoration(it) }
231234
}
232235
setZoomEnabled(isZoomEnabled)
236+
setRenderQuality(renderQuality)
233237
}
234238

235239
recyclerView.addOnScrollListener(
@@ -253,6 +257,9 @@ class PdfRendererView @JvmOverloads constructor(
253257
recyclerView.setOnZoomChangeListener { isZoomedIn, scale ->
254258
zoomListener?.onZoomChanged(isZoomedIn, scale)
255259
}
260+
recyclerView.setScrollListener { isScrolledToTop ->
261+
scrollListener?.onScroll(isScrolledToTop)
262+
}
256263

257264
recyclerView.post {
258265
postInitializationAction?.invoke()
@@ -478,4 +485,8 @@ class PdfRendererView @JvmOverloads constructor(
478485
interface ZoomListener {
479486
fun onZoomChanged(isZoomedIn: Boolean, scale: Float)
480487
}
488+
489+
interface ScrollListener {
490+
fun onScroll(isScrolledToTop: Boolean)
491+
}
481492
}

pdfViewer/src/main/java/com/rajat/pdfviewer/PdfViewAdapter.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ internal class PdfViewAdapter(
2525
private val renderer: PdfRendererCore,
2626
private val parentView: PdfRendererView,
2727
private val pageSpacing: Rect,
28-
private val enableLoadingForPages: Boolean
28+
private val enableLoadingForPages: Boolean,
29+
private val renderQuality: RenderQuality,
2930
) : RecyclerView.Adapter<PdfViewAdapter.PdfPageViewHolder>() {
3031

3132
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PdfPageViewHolder =
@@ -75,6 +76,11 @@ internal class PdfViewAdapter(
7576

7677
if (cached != null && currentBoundPage == position) {
7778
if (DEBUG_LOGS_ENABLED) Log.d("PdfViewAdapter", "✅ Loaded page $position from cache")
79+
val aspectRatio = runCatching {
80+
cached.width.toFloat() / cached.height.toFloat()
81+
}.getOrElse { 1f }
82+
val height = (displayWidth / aspectRatio).toInt()
83+
itemBinding.updateLayoutParams(height)
7884
itemBinding.pageView.setImageBitmap(cached)
7985
hasRealBitmap = true
8086
applyFadeInAnimation(itemBinding.pageView)
@@ -89,7 +95,9 @@ internal class PdfViewAdapter(
8995
val height = (displayWidth / aspectRatio).toInt()
9096
itemBinding.updateLayoutParams(height)
9197

92-
renderAndApplyBitmap(position, displayWidth, height)
98+
val bitmapWidth = (displayWidth * renderQuality.qualityMultiplier).toInt()
99+
val bitmapHeight = (height * renderQuality.qualityMultiplier).toInt()
100+
renderAndApplyBitmap(position, bitmapWidth, bitmapHeight)
93101
}
94102
}
95103

@@ -127,7 +135,7 @@ internal class PdfViewAdapter(
127135
}
128136

129137
private fun retryRenderOnce(page: Int, width: Int, height: Int) {
130-
val retryBitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, height)
138+
val retryBitmap = CommonUtils.Companion.BitmapPool.getBitmap(width, maxOf(1, height))
131139
renderer.renderPage(page, retryBitmap) { success, retryPageNo, rendered ->
132140
scope.launch {
133141
if (success && retryPageNo == currentBoundPage && !hasRealBitmap) {

pdfViewer/src/main/java/com/rajat/pdfviewer/PinchZoomRecyclerView.kt

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
2323
private val gestureDetector = GestureDetector(context, GestureListener())
2424

2525
// Zoom and pan state
26+
private var renderQuality = RenderQuality.NORMAL
2627
private var scaleFactor = 1f
2728
private var isZoomEnabled = true
28-
private var maxZoom = MAX_ZOOM
29+
private val maxZoom get() = MAX_ZOOM * renderQuality.qualityMultiplier
2930
private var zoomDuration = ZOOM_DURATION
3031
private var isZoomingInProgress = false
32+
private var isOnTop = true
3133

3234
// Panning offsets and touch memory
3335
private var lastTouchX = 0f
@@ -36,6 +38,7 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
3638
private var posY = 0f
3739

3840
private var zoomChangeListener: ((Boolean, Float) -> Unit)? = null
41+
private var scrollListener: ((Boolean) -> Unit)? = null
3942

4043
init {
4144
setWillNotDraw(false)
@@ -53,6 +56,14 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
5356
zoomChangeListener = listener
5457
}
5558

59+
fun setScrollListener(listener: (isScrolledToTop: Boolean) -> Unit) {
60+
scrollListener = listener
61+
}
62+
63+
fun setRenderQuality(quality: RenderQuality) {
64+
renderQuality = quality
65+
}
66+
5667
/**
5768
* Handles touch interactions — zoom, pan, and scroll.
5869
*/
@@ -67,42 +78,10 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
6778
}
6879

6980
when (ev.actionMasked) {
70-
MotionEvent.ACTION_DOWN -> {
71-
lastTouchX = ev.x
72-
lastTouchY = ev.y
73-
activePointerId = ev.getPointerId(0)
74-
}
75-
MotionEvent.ACTION_MOVE -> {
76-
if (!scaleDetector.isInProgress && scaleFactor > 1f) {
77-
val pointerIndex = ev.findPointerIndex(activePointerId)
78-
if (pointerIndex != -1) {
79-
val x = ev.getX(pointerIndex)
80-
val y = ev.getY(pointerIndex)
81-
val dx = x - lastTouchX
82-
val dy = y - lastTouchY
83-
posX += dx
84-
posY += dy
85-
clampPosition()
86-
invalidate()
87-
88-
lastTouchX = x
89-
lastTouchY = y
90-
}
91-
}
92-
}
93-
MotionEvent.ACTION_POINTER_UP -> {
94-
val pointerIndex = ev.actionIndex
95-
val pointerId = ev.getPointerId(pointerIndex)
96-
if (pointerId == activePointerId) {
97-
val newPointerIndex = if (pointerIndex == 0) 1 else 0
98-
lastTouchX = ev.getX(newPointerIndex)
99-
lastTouchY = ev.getY(newPointerIndex)
100-
activePointerId = ev.getPointerId(newPointerIndex)
101-
}
102-
}
103-
MotionEvent.ACTION_CANCEL -> {
104-
activePointerId = INVALID_POINTER_ID
105-
}
81+
MotionEvent.ACTION_DOWN -> onDown(ev = ev)
82+
MotionEvent.ACTION_MOVE -> onMove(ev = ev)
83+
MotionEvent.ACTION_POINTER_UP -> onUp(ev = ev)
84+
MotionEvent.ACTION_CANCEL -> onCancel(ev = ev)
10685
}
10786

10887
return if (scaleFactor > 1f) true else super.onTouchEvent(ev)
@@ -171,13 +150,65 @@ class PinchZoomRecyclerView @JvmOverloads constructor(
171150
return (averageHeight * itemCount * scaleFactor).toInt()
172151
}
173152

153+
private fun onDown(ev: MotionEvent) {
154+
lastTouchX = ev.x
155+
lastTouchY = ev.y
156+
activePointerId = ev.getPointerId(0)
157+
}
158+
159+
private fun onMove(ev: MotionEvent) {
160+
val pointerIndex = ev.findPointerIndex(activePointerId)
161+
if (pointerIndex != -1) {
162+
if (!scaleDetector.isInProgress && scaleFactor > 1f) {
163+
val x = ev.getX(pointerIndex)
164+
val y = ev.getY(pointerIndex)
165+
val dx = x - lastTouchX
166+
val dy = y - lastTouchY
167+
posX += dx
168+
posY += dy
169+
clampPosition()
170+
invalidate()
171+
172+
lastTouchX = x
173+
lastTouchY = y
174+
}
175+
176+
val isScrolledOut = !scaleDetector.isInProgress && scaleFactor == 1f
177+
val currentScrollOffset = computeVerticalScrollOffset()
178+
if (currentScrollOffset == 0 && isScrolledOut && !isOnTop) {
179+
scrollListener?.invoke(true)
180+
isOnTop = true
181+
} else if ((currentScrollOffset != 0 || isScrolledOut.not()) && isOnTop) {
182+
scrollListener?.invoke(false)
183+
isOnTop = false
184+
}
185+
}
186+
}
187+
188+
private fun onUp(ev: MotionEvent) {
189+
val pointerIndex = ev.actionIndex
190+
val pointerId = ev.getPointerId(pointerIndex)
191+
if (pointerId == activePointerId) {
192+
val newPointerIndex = if (pointerIndex == 0) 1 else 0
193+
lastTouchX = ev.getX(newPointerIndex)
194+
lastTouchY = ev.getY(newPointerIndex)
195+
activePointerId = ev.getPointerId(newPointerIndex)
196+
}
197+
}
198+
199+
private fun onCancel(ev: MotionEvent) {
200+
activePointerId = INVALID_POINTER_ID
201+
}
202+
174203
/**
175204
* Handles pinch-to-zoom scaling with focal-point centering.
176205
*/
177206
private inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
178207
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
179208
isZoomingInProgress = true
180209
suppressLayout(true)
210+
scrollListener?.invoke(false)
211+
isOnTop = false
181212
return true
182213
}
183214

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package com.rajat.pdfviewer
2+
3+
enum class RenderQuality(val qualityMultiplier: Float) {
4+
NORMAL(qualityMultiplier = 1f), HIGH(qualityMultiplier = 2f), ULTRA(qualityMultiplier = 3f)
5+
}

pdfViewer/src/main/java/com/rajat/pdfviewer/compose/PdfRendererCompose.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import androidx.lifecycle.compose.LocalLifecycleOwner
1414
import androidx.lifecycle.lifecycleScope
1515
import com.rajat.pdfviewer.HeaderData
1616
import com.rajat.pdfviewer.PdfRendererView
17+
import com.rajat.pdfviewer.RenderQuality
1718
import com.rajat.pdfviewer.util.CacheStrategy
1819
import com.rajat.pdfviewer.util.FileUtils.fileFromAsset
1920
import com.rajat.pdfviewer.util.PdfSource
@@ -23,12 +24,14 @@ import java.io.File
2324
fun PdfRendererViewCompose(
2425
source: PdfSource,
2526
modifier: Modifier = Modifier,
27+
renderQuality: RenderQuality = RenderQuality.NORMAL,
2628
headers: HeaderData = HeaderData(),
2729
cacheStrategy: CacheStrategy = CacheStrategy.MAXIMIZE_PERFORMANCE,
2830
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
2931
jumpToPage: Int? = null,
3032
statusCallBack: PdfRendererView.StatusCallBack? = null,
3133
zoomListener: PdfRendererView.ZoomListener? = null,
34+
scrollListener: PdfRendererView.ScrollListener? = null,
3235
onReady: ((PdfRendererView) -> Unit)? = null,
3336
) {
3437
val context = LocalContext.current
@@ -69,6 +72,8 @@ fun PdfRendererViewCompose(
6972
update = { view ->
7073
view.statusListener = combinedCallback
7174
view.zoomListener = zoomListener
75+
view.scrollListener = scrollListener
76+
view.renderQuality = renderQuality
7277

7378
if (!initialized) {
7479
when (source) {

0 commit comments

Comments
 (0)