@@ -2,6 +2,9 @@ package com.rajat.pdfviewer
22
33import android.content.Context
44import android.graphics.Rect
5+ import android.os.Handler
6+ import android.os.Looper
7+ import android.util.Log
58import android.view.LayoutInflater
69import android.view.View
710import android.view.ViewGroup
@@ -10,8 +13,10 @@ import android.view.animation.LinearInterpolator
1013import androidx.recyclerview.widget.RecyclerView
1114import com.rajat.pdfviewer.databinding.ListItemPdfPageBinding
1215import com.rajat.pdfviewer.util.CommonUtils
13- import kotlinx.coroutines.CoroutineScope
1416import kotlinx.coroutines.Dispatchers
17+ import kotlinx.coroutines.MainScope
18+ import kotlinx.coroutines.Runnable
19+ import kotlinx.coroutines.cancel
1520import kotlinx.coroutines.launch
1621import kotlinx.coroutines.withContext
1722
@@ -24,66 +29,154 @@ internal class PdfViewAdapter(
2429) : RecyclerView.Adapter<PdfViewAdapter.PdfPageViewHolder>() {
2530
2631 override fun onCreateViewHolder (parent : ViewGroup , viewType : Int ): PdfPageViewHolder =
27- PdfPageViewHolder (ListItemPdfPageBinding .inflate(LayoutInflater .from(parent.context), parent, false ))
32+ PdfPageViewHolder (
33+ ListItemPdfPageBinding .inflate(LayoutInflater .from(parent.context), parent, false )
34+ )
2835
2936 override fun getItemCount (): Int = renderer.getPageCount()
3037
3138 override fun onBindViewHolder (holder : PdfPageViewHolder , position : Int ) {
3239 holder.bind(position)
3340 }
3441
35- inner class PdfPageViewHolder (private val itemBinding : ListItemPdfPageBinding ) : RecyclerView.ViewHolder(itemBinding.root) {
42+ override fun onViewRecycled (holder : PdfPageViewHolder ) {
43+ super .onViewRecycled(holder)
44+ holder.cancelJobs()
45+ }
46+
47+ inner class PdfPageViewHolder (private val itemBinding : ListItemPdfPageBinding ) :
48+ RecyclerView .ViewHolder (itemBinding.root) {
49+
50+ private var currentBoundPage: Int = - 1
51+ private var hasRealBitmap: Boolean = false
52+ private val fallbackHandler = Handler (Looper .getMainLooper())
53+ private var scope = MainScope ()
54+
55+ private val DEBUG_LOGS_ENABLED = false
56+
3657 fun bind (position : Int ) {
37- val width = itemBinding.pageView.width.takeIf { it > 0 }
58+ cancelJobs()
59+ currentBoundPage = position
60+ hasRealBitmap = false
61+ scope = MainScope ()
62+
63+ val displayWidth = itemBinding.pageView.width.takeIf { it > 0 }
3864 ? : context.resources.displayMetrics.widthPixels
3965
40- CoroutineScope (Dispatchers .Main ).launch {
41- itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility =
42- if (enableLoadingForPages) View .VISIBLE else View .GONE
66+ itemBinding.pageView.setImageBitmap(null )
67+
68+ itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility =
69+ if (enableLoadingForPages) View .VISIBLE else View .GONE
4370
71+ scope.launch {
4472 val cached = withContext(Dispatchers .IO ) {
4573 renderer.getBitmapFromCache(position)
4674 }
4775
48- if (cached != null ) {
76+ if (cached != null && currentBoundPage == position) {
77+ if (DEBUG_LOGS_ENABLED ) Log .d(" PdfViewAdapter" , " ✅ Loaded page $position from cache" )
4978 itemBinding.pageView.setImageBitmap(cached)
50- itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View . GONE
79+ hasRealBitmap = true
5180 applyFadeInAnimation(itemBinding.pageView)
81+ itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View .GONE
5282 return @launch
5383 }
5484
5585 renderer.getPageDimensionsAsync(position) { size ->
56- val aspectRatio = size.width.toFloat() / size.height.toFloat()
57- val height = (width / aspectRatio).toInt()
86+ if (currentBoundPage != position) return @getPageDimensionsAsync
5887
88+ val aspectRatio = size.width.toFloat() / size.height.toFloat()
89+ val height = (displayWidth / aspectRatio).toInt()
5990 itemBinding.updateLayoutParams(height)
6091
61- val bitmap = CommonUtils .Companion .BitmapPool .getBitmap(width, maxOf(1 , height))
62- renderer.renderPage(position, bitmap) { success, pageNo, renderedBitmap ->
63- if (success && pageNo == position) {
64- CoroutineScope (Dispatchers .Main ).launch {
65- itemBinding.pageView.setImageBitmap(renderedBitmap ? : bitmap)
66- applyFadeInAnimation(itemBinding.pageView)
67- itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View .GONE
68-
69- // adaptive directional prefetch
70- val direction = parentView.getScrollDirection()
71- val fallbackHeight = itemBinding.pageView.height.takeIf { it > 0 }
72- ? : context.resources.displayMetrics.heightPixels
73-
74- renderer.schedulePrefetch(
75- currentPage = position,
76- width = width,
77- height = fallbackHeight,
78- direction = direction
79- )
80- }
92+ renderAndApplyBitmap(position, displayWidth, height)
93+ }
94+ }
95+
96+ startPersistentFallbackRender(position)
97+ }
98+
99+ private fun renderAndApplyBitmap (page : Int , width : Int , height : Int ) {
100+ val bitmap = CommonUtils .Companion .BitmapPool .getBitmap(width, maxOf(1 , height))
101+
102+ renderer.renderPage(page, bitmap) { success, pageNo, rendered ->
103+ scope.launch {
104+ if (success && currentBoundPage == pageNo) {
105+ if (DEBUG_LOGS_ENABLED ) Log .d(" PdfViewAdapter" , " ✅ Render complete for page $pageNo " )
106+ itemBinding.pageView.setImageBitmap(rendered ? : bitmap)
107+ hasRealBitmap = true
108+ applyFadeInAnimation(itemBinding.pageView)
109+ itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View .GONE
110+
111+ val fallbackHeight = itemBinding.pageView.height.takeIf { it > 0 }
112+ ? : context.resources.displayMetrics.heightPixels
113+
114+ renderer.schedulePrefetch(
115+ currentPage = pageNo,
116+ width = width,
117+ height = fallbackHeight,
118+ direction = parentView.getScrollDirection()
119+ )
120+ } else {
121+ if (DEBUG_LOGS_ENABLED ) Log .w(" PdfViewAdapter" , " 🚫 Skipping render for page $pageNo — ViewHolder now bound to $currentBoundPage " )
122+ CommonUtils .Companion .BitmapPool .recycleBitmap(bitmap)
123+ retryRenderOnce(page, width, height)
124+ }
125+ }
126+ }
127+ }
128+
129+ private fun retryRenderOnce (page : Int , width : Int , height : Int ) {
130+ val retryBitmap = CommonUtils .Companion .BitmapPool .getBitmap(width, height)
131+ renderer.renderPage(page, retryBitmap) { success, retryPageNo, rendered ->
132+ scope.launch {
133+ if (success && retryPageNo == currentBoundPage && ! hasRealBitmap) {
134+ if (DEBUG_LOGS_ENABLED ) Log .d(" PdfViewAdapter" , " 🔁 Retry success for page $retryPageNo " )
135+ itemBinding.pageView.setImageBitmap(rendered ? : retryBitmap)
136+ hasRealBitmap = true
137+ applyFadeInAnimation(itemBinding.pageView)
138+ itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View .GONE
139+ } else {
140+ CommonUtils .Companion .BitmapPool .recycleBitmap(retryBitmap)
141+ }
142+ }
143+ }
144+ }
145+
146+ private fun startPersistentFallbackRender (
147+ page : Int ,
148+ retries : Int = 10,
149+ delayMs : Long = 200L
150+ ) {
151+ var attempt = 0
152+
153+ lateinit var task: Runnable
154+ task = object : Runnable {
155+ override fun run () {
156+ if (currentBoundPage != page || hasRealBitmap) return
157+
158+ scope.launch {
159+ val cached = withContext(Dispatchers .IO ) {
160+ renderer.getBitmapFromCache(page)
161+ }
162+
163+ if (cached != null && currentBoundPage == page) {
164+ if (DEBUG_LOGS_ENABLED ) Log .d(" PdfViewAdapter" , " 🕒 Fallback applied for page $page on attempt $attempt " )
165+ itemBinding.pageView.setImageBitmap(cached)
166+ hasRealBitmap = true
167+ applyFadeInAnimation(itemBinding.pageView)
168+ itemBinding.pageLoadingLayout.pdfViewPageLoadingProgress.visibility = View .GONE
81169 } else {
82- CommonUtils .Companion .BitmapPool .recycleBitmap(bitmap)
170+ attempt++
171+ if (attempt < retries) {
172+ fallbackHandler.postDelayed(task, delayMs)
173+ }
83174 }
84175 }
85176 }
86177 }
178+
179+ fallbackHandler.postDelayed(task, delayMs)
87180 }
88181
89182 private fun ListItemPdfPageBinding.updateLayoutParams (height : Int ) {
@@ -95,11 +188,15 @@ internal class PdfViewAdapter(
95188 }
96189 }
97190
191+ fun cancelJobs () {
192+ scope.cancel()
193+ }
194+
98195 private fun applyFadeInAnimation (view : View ) {
99196 view.startAnimation(AlphaAnimation (0F , 1F ).apply {
100197 interpolator = LinearInterpolator ()
101198 duration = 300
102199 })
103200 }
104201 }
105- }
202+ }
0 commit comments