@june24/expo-pdf-reader 0.1.25 → 0.1.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -132,19 +132,87 @@ class ExpoPdfReaderView(
132
132
  private var minZoom = 1.0f
133
133
  private var maxZoom = 5.0f
134
134
  private var currentZoom = 1.0f
135
- private val scaleDetector = ScaleGestureDetector(
136
- context,
137
- object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
138
- override fun onScale(d: ScaleGestureDetector): Boolean {
139
- if (activeTool != null) return true
140
- applyZoom((currentZoom * d.scaleFactor).coerceIn(minZoom, maxZoom))
135
+
136
+ /**
137
+ * continuous / twoup / twoupcontinuous — pinch + zoom үед зөвхөн pan (босоо scroll унтрах).
138
+ * Pinch-ийг [dispatchTouchEvent]-ийн эхэнд дуудаж хүү canvas POINTER_DOWN-д баригдахаас өмнө 2 хуруу хүлээнэ.
139
+ */
140
+ private inner class ZoomableScrollView(ctx: Context) : ScrollView(ctx) {
141
+ /** ScrollView-д [isScrollEnabled] байхгүй тул: false үед босоо scroll (super) ажиллахгүй. */
142
+ var allowVerticalScrollFromTouch = true
143
+
144
+ private val pinch = ScaleGestureDetector(
145
+ ctx,
146
+ object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
147
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
148
+ if (activeTool != null) return false
149
+ parent?.requestDisallowInterceptTouchEvent(true)
150
+ return true
151
+ }
152
+
153
+ override fun onScale(detector: ScaleGestureDetector): Boolean {
154
+ if (activeTool != null) return false
155
+ applyZoom((currentZoom * detector.scaleFactor).coerceIn(minZoom, maxZoom))
156
+ return true
157
+ }
158
+
159
+ override fun onScaleEnd(detector: ScaleGestureDetector) {
160
+ parent?.requestDisallowInterceptTouchEvent(false)
161
+ }
162
+ }
163
+ )
164
+ private var panLastX = 0f
165
+ private var panLastY = 0f
166
+
167
+ override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
168
+ if (activeTool != null) return super.onInterceptTouchEvent(ev)
169
+ // 2+ хуруу эсвэл pinch эхэлсэн үед ScrollView босоо scroll intercept хийхгүй (scroll + zoom өрсөлдөнө)
170
+ if (!allowVerticalScrollFromTouch) return false
171
+ if (ev.pointerCount > 1 || pinch.isInProgress) return false
172
+ return super.onInterceptTouchEvent(ev)
173
+ }
174
+
175
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
176
+ if (activeTool == null) {
177
+ pinch.onTouchEvent(ev)
178
+ if (ev.actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
179
+ stopNestedScroll()
180
+ }
181
+ if (pinch.isInProgress || ev.pointerCount > 1) {
182
+ parent?.requestDisallowInterceptTouchEvent(true)
183
+ }
184
+ }
185
+ return super.dispatchTouchEvent(ev)
186
+ }
187
+
188
+ override fun onTouchEvent(ev: MotionEvent): Boolean {
189
+ if (activeTool != null) return super.onTouchEvent(ev)
190
+ if (pinch.isInProgress || ev.pointerCount > 1) {
141
191
  return true
142
192
  }
193
+ if (currentZoom > 1.02f) {
194
+ when (ev.actionMasked) {
195
+ MotionEvent.ACTION_DOWN -> {
196
+ panLastX = ev.x
197
+ panLastY = ev.y
198
+ }
199
+ MotionEvent.ACTION_MOVE -> {
200
+ val dx = ev.x - panLastX
201
+ val dy = ev.y - panLastY
202
+ panLastX = ev.x
203
+ panLastY = ev.y
204
+ applyContainerPanDelta(dx, dy)
205
+ }
206
+ }
207
+ return true
208
+ }
209
+ if (!allowVerticalScrollFromTouch) return true
210
+ return super.onTouchEvent(ev)
143
211
  }
144
- )
212
+ }
145
213
 
146
214
  // ── UI ───────────────────────────────────────────────────────────────────
147
- val scrollView = ScrollView(context).apply {
215
+ private val scrollView = ZoomableScrollView(context).apply {
148
216
  layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
149
217
  isFillViewport = true
150
218
  }
@@ -169,6 +237,8 @@ class ExpoPdfReaderView(
169
237
  layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
170
238
  PagerSnapHelper().attachToRecyclerView(this)
171
239
  setBackgroundColor(Color.parseColor("#E8E8E8"))
240
+ overScrollMode = View.OVER_SCROLL_NEVER
241
+ isNestedScrollingEnabled = false
172
242
  visibility = View.GONE
173
243
  addOnScrollListener(object : RecyclerView.OnScrollListener() {
174
244
  override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
@@ -209,11 +279,6 @@ class ExpoPdfReaderView(
209
279
  safeCloseRenderer()
210
280
  }
211
281
 
212
- override fun onTouchEvent(event: MotionEvent): Boolean {
213
- scaleDetector.onTouchEvent(event)
214
- return super.onTouchEvent(event)
215
- }
216
-
217
282
  // ─────────────────────────────────────────────────────────────────────────
218
283
  // Props
219
284
  // ─────────────────────────────────────────────────────────────────────────
@@ -472,20 +537,114 @@ class ExpoPdfReaderView(
472
537
  // ─────────────────────────────────────────────────────────────────────────
473
538
 
474
539
  private fun applyZoom(newZoom: Float) {
475
- currentZoom = newZoom
540
+ currentZoom = newZoom.coerceIn(minZoom, maxZoom)
476
541
  container.pivotX = 0f
477
542
  container.pivotY = 0f
478
- container.scaleX = newZoom
479
- container.scaleY = newZoom
543
+ container.scaleX = currentZoom
544
+ container.scaleY = currentZoom
545
+ if (displayMode != "single") {
546
+ if (currentZoom <= 1.02f) {
547
+ scrollView.allowVerticalScrollFromTouch = true
548
+ container.translationX = 0f
549
+ container.translationY = 0f
550
+ } else {
551
+ scrollView.allowVerticalScrollFromTouch = false
552
+ }
553
+ }
554
+ }
555
+
556
+ /** Zoom > 1 үед container-ийг translation-оор pan (хуудас хоорондын scroll унтраасан) */
557
+ private fun applyContainerPanDelta(dx: Float, dy: Float) {
558
+ val ch = container.height.toFloat()
559
+ val cw = container.width.toFloat()
560
+ if (ch <= 0f || cw <= 0f) return
561
+ val scaledH = ch * currentZoom
562
+ val scaledW = cw * currentZoom
563
+ val vw = scrollView.width.toFloat()
564
+ val vh = scrollView.height.toFloat()
565
+ var tx = container.translationX + dx
566
+ var ty = container.translationY + dy
567
+ if (scaledW <= vw) tx = 0f else tx = tx.coerceIn(vw - scaledW, 0f)
568
+ if (scaledH <= vh) ty = 0f else ty = ty.coerceIn(vh - scaledH, 0f)
569
+ container.translationX = tx
570
+ container.translationY = ty
480
571
  }
481
572
 
482
573
  // ─────────────────────────────────────────────────────────────────────────
483
574
  // Scroll / page detection
484
575
  // ─────────────────────────────────────────────────────────────────────────
485
576
 
577
+ /** twoupcontinuous: нэг мөр = хос entry (хуудас дараалсан), эсвэл эцсийн ганц entry. */
578
+ private fun twoupStrideAt(entryIdx: Int): Int {
579
+ if (displayMode != "twoupcontinuous") return 1
580
+ if (entryIdx >= pageEntries.size) return 0
581
+ if (entryIdx + 1 < pageEntries.size) {
582
+ val a = pageEntries[entryIdx].canvasView.pageIndex
583
+ val b = pageEntries[entryIdx + 1].canvasView.pageIndex
584
+ if (b == a + 1) return 2
585
+ }
586
+ return 1
587
+ }
588
+
589
+ /** scrollView доторх pageIndex хуудасны мөрийн дээд ирмэгийн Y (continuous / twoupcontinuous). */
590
+ private fun scrollYForScrollModePage(pageIndex: Int): Int {
591
+ if (pageEntries.isEmpty()) return 0
592
+ val lastAbsPage = windowStart + pageEntries.size - 1
593
+ val clamped = pageIndex.coerceIn(windowStart, lastAbsPage.coerceAtLeast(windowStart))
594
+ if (displayMode != "twoupcontinuous") {
595
+ val rel = (clamped - windowStart).coerceIn(0, maxOf(0, pageEntries.size - 1))
596
+ var y = 0
597
+ for (i in 0 until rel) {
598
+ y += (pageEntries[i].bmpHeight.takeIf { it > 0 } ?: 0) + 12
599
+ }
600
+ return y
601
+ }
602
+ var y = 0
603
+ var idx = 0
604
+ while (idx < pageEntries.size) {
605
+ val stride = twoupStrideAt(idx)
606
+ val rowH = pageEntries[idx].bmpHeight.takeIf { it > 0 } ?: 0
607
+ val p0 = pageEntries[idx].canvasView.pageIndex
608
+ val p1 = if (stride == 2) pageEntries[idx + 1].canvasView.pageIndex else p0
609
+ if (clamped in p0..p1) return y
610
+ y += rowH + 12
611
+ idx += stride
612
+ }
613
+ return y
614
+ }
615
+
486
616
  private fun detectCurrentPage(scrollY: Int) {
487
617
  if (pageEntries.isEmpty()) return
488
- val centerY = scrollY + height / 2
618
+ val viewH = scrollView.height.takeIf { it > 0 } ?: height
619
+ val centerY = scrollY + viewH / 2
620
+ if (displayMode == "twoupcontinuous") {
621
+ var accY = 0
622
+ var idx = 0
623
+ while (idx < pageEntries.size) {
624
+ val stride = twoupStrideAt(idx)
625
+ val rowH = pageEntries[idx].bmpHeight.takeIf { it > 0 } ?: 0
626
+ val nextY = accY + rowH + 12
627
+ val p0 = pageEntries[idx].canvasView.pageIndex
628
+ val p1 = if (stride == 2) pageEntries[idx + 1].canvasView.pageIndex else p0
629
+ if (centerY <= nextY || idx + stride >= pageEntries.size) {
630
+ val vx = scrollView.width.takeIf { it > 0 } ?: width
631
+ val absPage = if (stride == 2 && p1 > p0 && vx > 0) {
632
+ val midPix = vx / 2
633
+ val centerX = scrollView.scrollX + midPix
634
+ if (centerX < midPix) p0 else p1
635
+ } else p0
636
+ if (currentPageIndex != absPage) {
637
+ currentPageIndex = absPage
638
+ onPageChange(mapOf("currentPage" to absPage, "totalPage" to totalPages))
639
+ }
640
+ break
641
+ }
642
+ accY = nextY
643
+ idx += stride
644
+ }
645
+ return
646
+ }
647
+ // continuous — entry бүр нэг босоо мөр
489
648
  var accY = 0
490
649
  for (i in pageEntries.indices) {
491
650
  val h = pageEntries[i].bmpHeight.takeIf { it > 0 } ?: continue
@@ -510,12 +669,7 @@ class ExpoPdfReaderView(
510
669
  return
511
670
  }
512
671
  if (pageEntries.isEmpty()) return
513
- // pageEntries[0] нь windowStart-р эхэлдэг тул relative index ашиглана
514
- val relativeIndex = (pageIndex - windowStart).coerceIn(0, pageEntries.size - 1)
515
- var y = 0
516
- for (i in 0 until relativeIndex) {
517
- y += (pageEntries[i].bmpHeight.takeIf { it > 0 } ?: 0) + 12
518
- }
672
+ val y = scrollYForScrollModePage(pageIndex)
519
673
  if (smooth) scrollView.smoothScrollTo(0, y) else scrollView.scrollTo(0, y)
520
674
  currentPageIndex = pageIndex
521
675
  onPageChange(mapOf("currentPage" to pageIndex, "totalPage" to totalPages))
@@ -537,8 +691,9 @@ class ExpoPdfReaderView(
537
691
  renderer = PdfRenderer(fileDescriptor!!)
538
692
  val r = renderer!!
539
693
  totalPages = r.pageCount
540
- pagePdfW = IntArray(totalPages) { i -> r.openPage(i).use { it.width } }
541
- pagePdfH = IntArray(totalPages) { i -> r.openPage(i).use { it.height } }
694
+ // Хэмжээг энд бүгдийг нь уншихгүй — renderPageBitmap нээх бүрт бөглөнө (анхны ачаалал хурдан).
695
+ pagePdfW = IntArray(totalPages) { 0 }
696
+ pagePdfH = IntArray(totalPages) { 0 }
542
697
  }
543
698
  // annotationMap is intentionally NOT cleared here.
544
699
  // setAnnotations() may have already populated it before startLoad() runs
@@ -583,6 +738,12 @@ class ExpoPdfReaderView(
583
738
  chunkLoading = false
584
739
 
585
740
  withContext(Dispatchers.Main) {
741
+ currentZoom = 1f
742
+ container.scaleX = 1f
743
+ container.scaleY = 1f
744
+ container.translationX = 0f
745
+ container.translationY = 0f
746
+ scrollView.allowVerticalScrollFromTouch = true
586
747
  container.removeAllViews()
587
748
  pageEntries.clear()
588
749
  loadingFooter = null
@@ -591,7 +752,12 @@ class ExpoPdfReaderView(
591
752
  when (displayMode) {
592
753
  "single" -> renderSingleMode()
593
754
  "twoup" -> renderAllPages(pdf, pageCount, viewWidth)
594
- else -> renderFirstChunk(pdf, pageCount, viewWidth) // continuous / twoUpContinuous
755
+ else -> renderScrollModeAround(
756
+ pdf,
757
+ pageCount,
758
+ viewWidth,
759
+ currentPageIndex.coerceIn(0, pageCount - 1)
760
+ )
595
761
  }
596
762
 
597
763
  // onReady: Main thread дээр шууд дуудна
@@ -644,34 +810,53 @@ class ExpoPdfReaderView(
644
810
  withContext(Dispatchers.Main) { renderedUpTo = pageCount - 1 }
645
811
  }
646
812
 
647
- // continuous / twoUpContinuous — эхний PAGE_CHUNK хуудсыг render хийнэ
648
- private suspend fun renderFirstChunk(pdf: PdfRenderer, pageCount: Int, viewWidth: Int) {
813
+ /**
814
+ * continuous / twoupcontinuous [anchorPage]-ийн эргэн тойронд MAX_RENDERED хүртэлх цонх.
815
+ * Горим солиход одоогийн хуудас хадгалагдана.
816
+ */
817
+ private suspend fun renderScrollModeAround(
818
+ pdf: PdfRenderer,
819
+ pageCount: Int,
820
+ viewWidth: Int,
821
+ anchorPage: Int
822
+ ) {
649
823
  withContext(Dispatchers.Main) {
650
824
  scrollView.visibility = View.VISIBLE
651
825
  pager.visibility = View.GONE
652
826
  }
653
- val end = minOf(PAGE_CHUNK, pageCount)
827
+ val target = anchorPage.coerceIn(0, pageCount - 1)
828
+ val newStart = maxOf(0, target - PAGE_CHUNK / 2)
829
+ val newEnd = minOf(pageCount, newStart + MAX_RENDERED)
830
+ withContext(Dispatchers.Main) {
831
+ windowStart = newStart
832
+ renderedUpTo = newStart - 1
833
+ }
654
834
  if (displayMode == "twoupcontinuous") {
655
- renderTwoUpRange(pdf, pageCount, viewWidth, 0, end)
656
- // Хос тул renderedUpTo-г тэгш болгоно
835
+ renderTwoUpRange(pdf, pageCount, viewWidth, newStart, newEnd)
657
836
  withContext(Dispatchers.Main) {
658
- renderedUpTo = if (end % 2 == 0) end - 1 else end - 2
837
+ val rawUpTo = if (newEnd % 2 == 0) newEnd - 1 else newEnd - 2
838
+ renderedUpTo = rawUpTo.coerceIn(0, pageCount - 1)
659
839
  if (renderedUpTo < pageCount - 1) showLoadingFooter()
660
840
  forceLayoutScrollView()
841
+ post { scrollToPage(target, smooth = false) }
661
842
  }
662
843
  } else {
663
- for (i in 0 until end) {
844
+ for (i in newStart until newEnd) {
664
845
  val bmp = renderPageBitmap(pdf, i, viewWidth)
665
846
  val idx = i
666
847
  withContext(Dispatchers.Main) {
667
- addPageRow(bmp, idx, pagePdfW.getOrElse(idx) { 1 }.toFloat(),
668
- pagePdfH.getOrElse(idx) { 1 }.toFloat())
848
+ addPageRow(
849
+ bmp, idx,
850
+ pagePdfW.getOrElse(idx) { 1 }.toFloat(),
851
+ pagePdfH.getOrElse(idx) { 1 }.toFloat()
852
+ )
669
853
  }
670
854
  }
671
855
  withContext(Dispatchers.Main) {
672
- renderedUpTo = end - 1
673
- if (end < pageCount) showLoadingFooter()
674
- forceLayoutScrollView() // pages-г шууд харагдуулна
856
+ renderedUpTo = (newEnd - 1).coerceIn(0, pageCount - 1)
857
+ if (newEnd < pageCount) showLoadingFooter()
858
+ forceLayoutScrollView()
859
+ post { scrollToPage(target, smooth = false) }
675
860
  }
676
861
  }
677
862
  }
@@ -732,7 +917,7 @@ class ExpoPdfReaderView(
732
917
  }
733
918
  }
734
919
 
735
- // Дээш scroll: өмнөх chunk ачаална
920
+ // Дээш scroll: өмнөх chunk (continuous = нэг багана; twoupcontinuous = хос мөр prepend).
736
921
  if (windowStart > 0 && scrollY <= scrollView.height * 2) {
737
922
  chunkLoading = true
738
923
  scope.launch {
@@ -805,6 +990,10 @@ class ExpoPdfReaderView(
805
990
  */
806
991
  private suspend fun loadPrevChunk() {
807
992
  val pdf = renderer ?: return
993
+ if (displayMode == "twoupcontinuous") {
994
+ loadPrevChunkTwoup(pdf)
995
+ return
996
+ }
808
997
  val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
809
998
  val prevEnd = windowStart
810
999
  if (prevStart >= prevEnd) return
@@ -840,6 +1029,105 @@ class ExpoPdfReaderView(
840
1029
  }
841
1030
  }
842
1031
 
1032
+ /** twoupcontinuous: өмнөх chunk-ийг хос мөрөөр дээр нэмнэ. */
1033
+ private suspend fun loadPrevChunkTwoup(pdf: PdfRenderer) {
1034
+ val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
1035
+ val prevEnd = windowStart
1036
+ if (prevStart >= prevEnd) return
1037
+ val viewWidth = currentViewWidth
1038
+ val halfW = viewWidth / 2
1039
+
1040
+ data class RowBmps(val leftIdx: Int, val leftBmp: Bitmap, val rightBmp: Bitmap?)
1041
+ val rows = mutableListOf<RowBmps>()
1042
+ var i = prevStart
1043
+ while (i < prevEnd) {
1044
+ val leftBmp = renderPageBitmap(pdf, i, halfW)
1045
+ val rightBmp = if (i + 1 < minOf(prevEnd, totalPages))
1046
+ renderPageBitmap(pdf, i + 1, halfW) else null
1047
+ rows.add(RowBmps(i, leftBmp, rightBmp))
1048
+ i += 2
1049
+ }
1050
+
1051
+ withContext(Dispatchers.Main) {
1052
+ var insertedHeight = 0
1053
+ for (rb in rows.asReversed()) {
1054
+ prependTwoupRowToContainer(
1055
+ rb.leftBmp, rb.leftIdx,
1056
+ rb.rightBmp,
1057
+ if (rb.rightBmp != null) rb.leftIdx + 1 else null,
1058
+ viewWidth
1059
+ )
1060
+ insertedHeight += rb.leftBmp.height + 12
1061
+ }
1062
+ windowStart = prevStart
1063
+ renderedUpTo = windowStart + pageEntries.size - 1
1064
+
1065
+ while (pageEntries.size > MAX_RENDERED) {
1066
+ pruneBottomTwoupOneRow()
1067
+ }
1068
+
1069
+ scrollView.scrollBy(0, insertedHeight)
1070
+ forceLayoutScrollView()
1071
+ }
1072
+ }
1073
+
1074
+ /** twoup мөрийг container + pageEntries-ийн эхэнд оруулна (дээд chunk). */
1075
+ private fun prependTwoupRowToContainer(
1076
+ leftBmp: Bitmap, li: Int,
1077
+ rightBmp: Bitmap?, ri: Int?,
1078
+ viewWidth: Int
1079
+ ) {
1080
+ val rowH = leftBmp.height
1081
+ val row = LinearLayout(context).apply {
1082
+ orientation = LinearLayout.HORIZONTAL
1083
+ layoutParams = LinearLayout.LayoutParams(viewWidth, rowH)
1084
+ .apply { setMargins(0, 6, 0, 6) }
1085
+ }
1086
+ val (leftFrame, leftEntry) = buildPageFrameDetached(
1087
+ leftBmp, li,
1088
+ pagePdfW.getOrElse(li) { 1 }.toFloat(),
1089
+ pagePdfH.getOrElse(li) { 1 }.toFloat(),
1090
+ 1f
1091
+ )
1092
+ row.addView(leftFrame)
1093
+ if (rightBmp != null && ri != null) {
1094
+ val (rightFrame, rightEntry) = buildPageFrameDetached(
1095
+ rightBmp, ri,
1096
+ pagePdfW.getOrElse(ri) { 1 }.toFloat(),
1097
+ pagePdfH.getOrElse(ri) { 1 }.toFloat(),
1098
+ 1f
1099
+ )
1100
+ row.addView(rightFrame)
1101
+ pageEntries.add(0, rightEntry)
1102
+ pageEntries.add(0, leftEntry)
1103
+ } else {
1104
+ row.addView(View(context).apply {
1105
+ layoutParams = LinearLayout.LayoutParams(0, rowH, 1f)
1106
+ })
1107
+ pageEntries.add(0, leftEntry)
1108
+ }
1109
+ container.addView(row, 0)
1110
+ }
1111
+
1112
+ /** twoupcontinuous: доод талын нэг мөрийг (эсвэл сүүлийн entry) устгана. */
1113
+ private fun pruneBottomTwoupOneRow() {
1114
+ val last = pageEntries.lastOrNull() ?: return
1115
+ val row = last.frame.parent as? LinearLayout
1116
+ if (row != null && row.parent === container && row.childCount > 0) {
1117
+ val removeList = pageEntries.filter { it.frame.parent === row }
1118
+ for (e in removeList) {
1119
+ (e.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
1120
+ pageEntries.remove(e)
1121
+ }
1122
+ container.removeView(row)
1123
+ } else {
1124
+ val e = pageEntries.removeLastOrNull() ?: return
1125
+ (e.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
1126
+ container.removeView(e.frame)
1127
+ }
1128
+ renderedUpTo = windowStart + pageEntries.size - 1
1129
+ }
1130
+
843
1131
  /**
844
1132
  * Дээр (index 0) шинэ хуудас оруулна. windowStart-г дуудагч тал шинэчилнэ.
845
1133
  */
@@ -900,21 +1188,33 @@ class ExpoPdfReaderView(
900
1188
  chunkLoading = false
901
1189
  }
902
1190
 
903
- for (i in newStart until newEnd) {
904
- val bmp = renderPageBitmap(pdf, i, viewWidth)
905
- val idx = i
1191
+ if (displayMode == "twoupcontinuous") {
1192
+ renderTwoUpRange(pdf, totalPages, viewWidth, newStart, newEnd)
906
1193
  withContext(Dispatchers.Main) {
907
- addPageRow(bmp, idx, pagePdfW.getOrElse(idx) { 1 }.toFloat(),
908
- pagePdfH.getOrElse(idx) { 1 }.toFloat())
1194
+ val rawUpTo = if (newEnd % 2 == 0) newEnd - 1 else newEnd - 2
1195
+ renderedUpTo = rawUpTo.coerceIn(0, totalPages - 1)
1196
+ if (renderedUpTo < totalPages - 1) showLoadingFooter()
1197
+ forceLayoutScrollView()
1198
+ post { scrollToPage(targetPage, smooth = false) }
1199
+ }
1200
+ } else {
1201
+ for (i in newStart until newEnd) {
1202
+ val bmp = renderPageBitmap(pdf, i, viewWidth)
1203
+ val idx = i
1204
+ withContext(Dispatchers.Main) {
1205
+ addPageRow(
1206
+ bmp, idx,
1207
+ pagePdfW.getOrElse(idx) { 1 }.toFloat(),
1208
+ pagePdfH.getOrElse(idx) { 1 }.toFloat()
1209
+ )
1210
+ }
1211
+ }
1212
+ withContext(Dispatchers.Main) {
1213
+ renderedUpTo = newEnd - 1
1214
+ if (newEnd < totalPages) showLoadingFooter()
1215
+ forceLayoutScrollView()
1216
+ post { scrollToPage(targetPage, smooth = false) }
909
1217
  }
910
- }
911
-
912
- withContext(Dispatchers.Main) {
913
- renderedUpTo = newEnd - 1
914
- if (newEnd < totalPages) showLoadingFooter()
915
- forceLayoutScrollView()
916
- // Layout болсны дараа scroll хийнэ
917
- post { scrollToPage(targetPage, smooth = false) }
918
1218
  }
919
1219
  } catch (e: CancellationException) { throw e }
920
1220
  catch (e: Exception) { Log.e("ExpoPdfReader", "JumpToPage error", e) }
@@ -956,9 +1256,9 @@ class ExpoPdfReaderView(
956
1256
  private fun addPageRow(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float) =
957
1257
  container.addView(buildPageFrame(bmp, pageIdx, pdfW, pdfH))
958
1258
 
959
- private fun buildPageFrame(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float, weight: Float = 0f): FrameLayout {
960
- // MATCH_PARENT нь React Native managed view дотор layout pass болохоос өмнө
961
- // width = 0 авдаг тул explicit pixel dimension ашиглана → шууд харагдана
1259
+ private fun buildPageFrameDetached(
1260
+ bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float, weight: Float = 0f
1261
+ ): Pair<FrameLayout, PageEntry> {
962
1262
  val bmpW = bmp.width
963
1263
  val bmpH = bmp.height
964
1264
  val frame = FrameLayout(context).apply {
@@ -979,13 +1279,22 @@ class ExpoPdfReaderView(
979
1279
  }
980
1280
  frame.addView(iv)
981
1281
  frame.addView(canvasView)
982
- pageEntries.add(PageEntry(frame, canvasView, bmpH))
1282
+ return frame to PageEntry(frame, canvasView, bmpH)
1283
+ }
1284
+
1285
+ private fun buildPageFrame(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float, weight: Float = 0f): FrameLayout {
1286
+ val (frame, entry) = buildPageFrameDetached(bmp, pageIdx, pdfW, pdfH, weight)
1287
+ pageEntries.add(entry)
983
1288
  return frame
984
1289
  }
985
1290
 
986
1291
  private suspend fun renderPageBitmap(pdf: PdfRenderer, index: Int, targetWidth: Int): Bitmap =
987
1292
  withContext(pdfDispatcher) {
988
1293
  pdf.openPage(index).use { page ->
1294
+ if (index < pagePdfW.size) {
1295
+ pagePdfW[index] = page.width
1296
+ pagePdfH[index] = page.height
1297
+ }
989
1298
  val s = targetWidth.toFloat() / page.width.toFloat()
990
1299
  val h = (page.height * s).toInt().coerceAtLeast(1)
991
1300
  val bmp = Bitmap.createBitmap(targetWidth, h, Bitmap.Config.ARGB_8888)
@@ -1071,37 +1380,35 @@ class ExpoPdfReaderView(
1071
1380
  singlePageCanvases.remove(it.pageIndex)
1072
1381
  holder.frame.removeView(it)
1073
1382
  }
1074
- val pdfW = pagePdfW.getOrElse(position) { 1 }.toFloat()
1075
- val pdfH = pagePdfH.getOrElse(position) { 1 }.toFloat()
1076
- val canvas = AnnotationCanvasView(context, position, pdfW, pdfH).apply {
1077
- layoutParams = FrameLayout.LayoutParams(
1078
- FrameLayout.LayoutParams.WRAP_CONTENT,
1079
- FrameLayout.LayoutParams.WRAP_CONTENT,
1080
- android.view.Gravity.CENTER
1081
- )
1082
- translationZ = 1f
1083
- }
1084
- holder.frame.addView(canvas)
1085
- holder.currentCanvas = canvas
1086
- singlePageCanvases[position] = canvas
1383
+ holder.currentCanvas = null
1087
1384
 
1088
1385
  holder.loadJob = scope.launch {
1089
1386
  val pdf = renderer ?: return@launch
1090
1387
  val bmp = renderPageBitmap(pdf, position, this@ExpoPdfReaderView.width)
1388
+ val pdfW = pagePdfW.getOrElse(position) { 1 }.toFloat()
1389
+ val pdfH = pagePdfH.getOrElse(position) { 1 }.toFloat()
1091
1390
  withContext(Dispatchers.Main) {
1092
- if (holder.boundPage == position) {
1093
- val lp = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
1094
- holder.imageView.layoutParams = lp
1095
- holder.imageView.setImageBitmap(bmp)
1096
- holder.currentCanvas?.layoutParams = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
1097
- val fw = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: return@withContext
1098
- val fh = this@ExpoPdfReaderView.height.takeIf { it > 0 } ?: return@withContext
1099
- holder.frame.measure(
1100
- MeasureSpec.makeMeasureSpec(fw, MeasureSpec.EXACTLY),
1101
- MeasureSpec.makeMeasureSpec(fh, MeasureSpec.EXACTLY)
1391
+ if (holder.boundPage != position) return@withContext
1392
+ val canvas = AnnotationCanvasView(context, position, pdfW, pdfH).apply {
1393
+ layoutParams = FrameLayout.LayoutParams(
1394
+ bmp.width, bmp.height,
1395
+ android.view.Gravity.CENTER
1102
1396
  )
1103
- holder.frame.layout(0, 0, fw, fh)
1397
+ translationZ = 1f
1104
1398
  }
1399
+ holder.frame.addView(canvas)
1400
+ holder.currentCanvas = canvas
1401
+ singlePageCanvases[position] = canvas
1402
+ val lp = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
1403
+ holder.imageView.layoutParams = lp
1404
+ holder.imageView.setImageBitmap(bmp)
1405
+ val fw = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: return@withContext
1406
+ val fh = this@ExpoPdfReaderView.height.takeIf { it > 0 } ?: return@withContext
1407
+ holder.frame.measure(
1408
+ MeasureSpec.makeMeasureSpec(fw, MeasureSpec.EXACTLY),
1409
+ MeasureSpec.makeMeasureSpec(fh, MeasureSpec.EXACTLY)
1410
+ )
1411
+ holder.frame.layout(0, 0, fw, fh)
1105
1412
  }
1106
1413
  }
1107
1414
  }
@@ -1153,16 +1460,32 @@ class ExpoPdfReaderView(
1153
1460
  }
1154
1461
  )
1155
1462
 
1463
+ override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
1464
+ if (activeTool == null) {
1465
+ zoomDetector.onTouchEvent(ev)
1466
+ if (ev.actionMasked == MotionEvent.ACTION_POINTER_DOWN) {
1467
+ stopScroll()
1468
+ }
1469
+ if (ev.pointerCount > 1 || zoomDetector.isInProgress) {
1470
+ parent?.requestDisallowInterceptTouchEvent(true)
1471
+ }
1472
+ }
1473
+ return super.dispatchTouchEvent(ev)
1474
+ }
1475
+
1156
1476
  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
1477
+ if (activeTool != null) return super.onInterceptTouchEvent(ev)
1478
+ // Pinch үед RecyclerView хэвийн scroll intercept хийхгүй
1479
+ if (ev.pointerCount > 1 || zoomDetector.isInProgress) return false
1157
1480
  // Zoom хийгдсэн + tool идэвхгүй → pan хийхийн тулд ACTION_DOWN-г intercept
1158
- if (currentScale > 1f && activeTool == null && ev.action == MotionEvent.ACTION_DOWN) {
1481
+ if (currentScale > 1.02f && ev.action == MotionEvent.ACTION_DOWN) {
1159
1482
  return true
1160
1483
  }
1161
1484
  return super.onInterceptTouchEvent(ev)
1162
1485
  }
1163
1486
 
1164
1487
  override fun onTouchEvent(ev: MotionEvent): Boolean {
1165
- zoomDetector.onTouchEvent(ev)
1488
+ // zoomDetector-ийг dispatchTouchEvent-д аль хэдийн дамжуулсан
1166
1489
 
1167
1490
  // 2 хуруу байвал RecyclerView scroll хийхгүйгээр zoom-г л хийнэ
1168
1491
  if (ev.pointerCount >= 2 || zoomDetector.isInProgress) {
@@ -1170,9 +1493,12 @@ class ExpoPdfReaderView(
1170
1493
  }
1171
1494
 
1172
1495
  // Zoom хийгдсэн үед нэг хуруугаар pan (tool идэвхгүй)
1173
- if (currentScale > 1f && activeTool == null) {
1496
+ if (currentScale > 1.02f && activeTool == null) {
1174
1497
  when (ev.actionMasked) {
1175
- MotionEvent.ACTION_DOWN -> { panX = ev.x; panY = ev.y }
1498
+ MotionEvent.ACTION_DOWN -> {
1499
+ stopScroll()
1500
+ panX = ev.x; panY = ev.y
1501
+ }
1176
1502
  MotionEvent.ACTION_MOVE -> {
1177
1503
  tx += ev.x - panX
1178
1504
  ty += ev.y - panY
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@june24/expo-pdf-reader",
3
- "version": "0.1.25",
3
+ "version": "0.1.27",
4
4
  "description": "A PDF reader for Expo apps with annotation and zoom controls",
5
5
  "homepage": "git@gitlab.com:june_241/expo-pdf-reader.git",
6
6
  "main": "build/index.js",