@june24/expo-pdf-reader 0.1.24 → 0.1.26
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.
|
@@ -91,13 +91,15 @@ class ExpoPdfReaderView(
|
|
|
91
91
|
private set
|
|
92
92
|
|
|
93
93
|
// ── Lazy / chunked rendering ──────────────────────────────────────────────
|
|
94
|
-
private var renderedUpTo = -1 // last rendered page index
|
|
94
|
+
private var renderedUpTo = -1 // last rendered page index (absolute)
|
|
95
|
+
private var windowStart = 0 // pageEntries[0]-н absolute page index
|
|
95
96
|
private var chunkLoading = false // chunk дуусах хүртэл дахин trigger хийхгүй
|
|
96
97
|
private var currentViewWidth = 0 // chunk load-д дахин хэрэглэнэ
|
|
97
98
|
private var loadingFooter: View? = null
|
|
98
99
|
|
|
99
100
|
companion object {
|
|
100
|
-
private const val PAGE_CHUNK = 10
|
|
101
|
+
private const val PAGE_CHUNK = 10 // нэг удаа render хийх хуудасны тоо
|
|
102
|
+
private const val MAX_RENDERED = PAGE_CHUNK * 2 // санах ойд байлгах дээд хязгаар (20 хуудас)
|
|
101
103
|
}
|
|
102
104
|
|
|
103
105
|
// ── Annotation state ─────────────────────────────────────────────────────
|
|
@@ -117,7 +119,8 @@ class ExpoPdfReaderView(
|
|
|
117
119
|
private var appliedFingerprint: String? = null
|
|
118
120
|
|
|
119
121
|
// ── Page view entries (continuous/twoUp modes) ───────────────────────────
|
|
120
|
-
|
|
122
|
+
// frame: container-аас устгахад шууд reference, bmpHeight: layout болохоос өмнө scroll тооцооны тулд
|
|
123
|
+
data class PageEntry(val frame: FrameLayout, val canvasView: AnnotationCanvasView, val bmpHeight: Int)
|
|
121
124
|
val pageEntries = mutableListOf<PageEntry>()
|
|
122
125
|
|
|
123
126
|
// ── Single-page pager (single mode) ──────────────────────────────────────
|
|
@@ -160,10 +163,10 @@ class ExpoPdfReaderView(
|
|
|
160
163
|
maybeTriggerChunkLoad(scrollY)
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
// Single-page pager:
|
|
164
|
-
pager =
|
|
166
|
+
// Single-page pager: ZoomRecyclerView + PagerSnapHelper (vertical)
|
|
167
|
+
pager = ZoomRecyclerView(context).apply {
|
|
165
168
|
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
166
|
-
layoutManager = LinearLayoutManager(context, LinearLayoutManager.
|
|
169
|
+
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
|
167
170
|
PagerSnapHelper().attachToRecyclerView(this)
|
|
168
171
|
setBackgroundColor(Color.parseColor("#E8E8E8"))
|
|
169
172
|
visibility = View.GONE
|
|
@@ -175,6 +178,8 @@ class ExpoPdfReaderView(
|
|
|
175
178
|
.takeIf { it >= 0 } ?: return
|
|
176
179
|
if (pos != currentPageIndex) {
|
|
177
180
|
currentPageIndex = pos
|
|
181
|
+
// Хуудас солигдоход zoom-г reset хийнэ
|
|
182
|
+
(pager as? ZoomRecyclerView)?.resetZoom()
|
|
178
183
|
onPageChange(mapOf("currentPage" to pos, "totalPage" to totalPages))
|
|
179
184
|
}
|
|
180
185
|
}
|
|
@@ -248,7 +253,18 @@ class ExpoPdfReaderView(
|
|
|
248
253
|
fun setInitialPage(page: Int) {
|
|
249
254
|
if (page < 0 || page >= totalPages) return
|
|
250
255
|
currentPageIndex = page
|
|
251
|
-
|
|
256
|
+
if (displayMode == "single") {
|
|
257
|
+
post { scrollToPage(page, smooth = false) }
|
|
258
|
+
return
|
|
259
|
+
}
|
|
260
|
+
val windowEnd = windowStart + pageEntries.size - 1
|
|
261
|
+
if (page in windowStart..windowEnd) {
|
|
262
|
+
// Хуудас render хийгдсэн window дотор байна → зүгээр scroll хийнэ
|
|
263
|
+
post { scrollToPage(page, smooth = false) }
|
|
264
|
+
} else {
|
|
265
|
+
// Хуудас render хийгдээгүй → window-г тухайн хуудас руу шилжүүлнэ
|
|
266
|
+
jumpToPage(page)
|
|
267
|
+
}
|
|
252
268
|
}
|
|
253
269
|
|
|
254
270
|
fun setMinZoom(v: Double) { minZoom = v.toFloat() }
|
|
@@ -405,7 +421,8 @@ class ExpoPdfReaderView(
|
|
|
405
421
|
if (displayMode == "single") {
|
|
406
422
|
singlePageCanvases[pageIndex]?.invalidate()
|
|
407
423
|
} else {
|
|
408
|
-
pageEntries
|
|
424
|
+
// continuous mode: pageEntries[0] = page windowStart → relative index
|
|
425
|
+
pageEntries.getOrNull(pageIndex - windowStart)?.canvasView?.invalidate()
|
|
409
426
|
}
|
|
410
427
|
}
|
|
411
428
|
|
|
@@ -466,17 +483,86 @@ class ExpoPdfReaderView(
|
|
|
466
483
|
// Scroll / page detection
|
|
467
484
|
// ─────────────────────────────────────────────────────────────────────────
|
|
468
485
|
|
|
486
|
+
/** twoupcontinuous: нэг мөр = хос entry (хуудас дараалсан), эсвэл эцсийн ганц entry. */
|
|
487
|
+
private fun twoupStrideAt(entryIdx: Int): Int {
|
|
488
|
+
if (displayMode != "twoupcontinuous") return 1
|
|
489
|
+
if (entryIdx >= pageEntries.size) return 0
|
|
490
|
+
if (entryIdx + 1 < pageEntries.size) {
|
|
491
|
+
val a = pageEntries[entryIdx].canvasView.pageIndex
|
|
492
|
+
val b = pageEntries[entryIdx + 1].canvasView.pageIndex
|
|
493
|
+
if (b == a + 1) return 2
|
|
494
|
+
}
|
|
495
|
+
return 1
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/** scrollView доторх pageIndex хуудасны мөрийн дээд ирмэгийн Y (continuous / twoupcontinuous). */
|
|
499
|
+
private fun scrollYForScrollModePage(pageIndex: Int): Int {
|
|
500
|
+
if (pageEntries.isEmpty()) return 0
|
|
501
|
+
val lastAbsPage = windowStart + pageEntries.size - 1
|
|
502
|
+
val clamped = pageIndex.coerceIn(windowStart, lastAbsPage.coerceAtLeast(windowStart))
|
|
503
|
+
if (displayMode != "twoupcontinuous") {
|
|
504
|
+
val rel = (clamped - windowStart).coerceIn(0, maxOf(0, pageEntries.size - 1))
|
|
505
|
+
var y = 0
|
|
506
|
+
for (i in 0 until rel) {
|
|
507
|
+
y += (pageEntries[i].bmpHeight.takeIf { it > 0 } ?: 0) + 12
|
|
508
|
+
}
|
|
509
|
+
return y
|
|
510
|
+
}
|
|
511
|
+
var y = 0
|
|
512
|
+
var idx = 0
|
|
513
|
+
while (idx < pageEntries.size) {
|
|
514
|
+
val stride = twoupStrideAt(idx)
|
|
515
|
+
val rowH = pageEntries[idx].bmpHeight.takeIf { it > 0 } ?: 0
|
|
516
|
+
val p0 = pageEntries[idx].canvasView.pageIndex
|
|
517
|
+
val p1 = if (stride == 2) pageEntries[idx + 1].canvasView.pageIndex else p0
|
|
518
|
+
if (clamped in p0..p1) return y
|
|
519
|
+
y += rowH + 12
|
|
520
|
+
idx += stride
|
|
521
|
+
}
|
|
522
|
+
return y
|
|
523
|
+
}
|
|
524
|
+
|
|
469
525
|
private fun detectCurrentPage(scrollY: Int) {
|
|
470
526
|
if (pageEntries.isEmpty()) return
|
|
471
|
-
val
|
|
527
|
+
val viewH = scrollView.height.takeIf { it > 0 } ?: height
|
|
528
|
+
val centerY = scrollY + viewH / 2
|
|
529
|
+
if (displayMode == "twoupcontinuous") {
|
|
530
|
+
var accY = 0
|
|
531
|
+
var idx = 0
|
|
532
|
+
while (idx < pageEntries.size) {
|
|
533
|
+
val stride = twoupStrideAt(idx)
|
|
534
|
+
val rowH = pageEntries[idx].bmpHeight.takeIf { it > 0 } ?: 0
|
|
535
|
+
val nextY = accY + rowH + 12
|
|
536
|
+
val p0 = pageEntries[idx].canvasView.pageIndex
|
|
537
|
+
val p1 = if (stride == 2) pageEntries[idx + 1].canvasView.pageIndex else p0
|
|
538
|
+
if (centerY <= nextY || idx + stride >= pageEntries.size) {
|
|
539
|
+
val vx = scrollView.width.takeIf { it > 0 } ?: width
|
|
540
|
+
val absPage = if (stride == 2 && p1 > p0 && vx > 0) {
|
|
541
|
+
val midPix = vx / 2
|
|
542
|
+
val centerX = scrollView.scrollX + midPix
|
|
543
|
+
if (centerX < midPix) p0 else p1
|
|
544
|
+
} else p0
|
|
545
|
+
if (currentPageIndex != absPage) {
|
|
546
|
+
currentPageIndex = absPage
|
|
547
|
+
onPageChange(mapOf("currentPage" to absPage, "totalPage" to totalPages))
|
|
548
|
+
}
|
|
549
|
+
break
|
|
550
|
+
}
|
|
551
|
+
accY = nextY
|
|
552
|
+
idx += stride
|
|
553
|
+
}
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
// continuous — entry бүр нэг босоо мөр
|
|
472
557
|
var accY = 0
|
|
473
558
|
for (i in pageEntries.indices) {
|
|
474
|
-
val h = pageEntries[i].
|
|
559
|
+
val h = pageEntries[i].bmpHeight.takeIf { it > 0 } ?: continue
|
|
475
560
|
accY += h + 12
|
|
476
561
|
if (centerY <= accY || i == pageEntries.size - 1) {
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
562
|
+
val absolutePage = windowStart + i
|
|
563
|
+
if (currentPageIndex != absolutePage) {
|
|
564
|
+
currentPageIndex = absolutePage
|
|
565
|
+
onPageChange(mapOf("currentPage" to absolutePage, "totalPage" to totalPages))
|
|
480
566
|
}
|
|
481
567
|
break
|
|
482
568
|
}
|
|
@@ -492,10 +578,7 @@ class ExpoPdfReaderView(
|
|
|
492
578
|
return
|
|
493
579
|
}
|
|
494
580
|
if (pageEntries.isEmpty()) return
|
|
495
|
-
|
|
496
|
-
for (i in 0 until pageIndex.coerceAtMost(pageEntries.size - 1)) {
|
|
497
|
-
y += (pageEntries[i].canvasView.height.takeIf { it > 0 } ?: 0) + 12
|
|
498
|
-
}
|
|
581
|
+
val y = scrollYForScrollModePage(pageIndex)
|
|
499
582
|
if (smooth) scrollView.smoothScrollTo(0, y) else scrollView.scrollTo(0, y)
|
|
500
583
|
currentPageIndex = pageIndex
|
|
501
584
|
onPageChange(mapOf("currentPage" to pageIndex, "totalPage" to totalPages))
|
|
@@ -517,8 +600,9 @@ class ExpoPdfReaderView(
|
|
|
517
600
|
renderer = PdfRenderer(fileDescriptor!!)
|
|
518
601
|
val r = renderer!!
|
|
519
602
|
totalPages = r.pageCount
|
|
520
|
-
|
|
521
|
-
|
|
603
|
+
// Хэмжээг энд бүгдийг нь уншихгүй — renderPageBitmap нээх бүрт бөглөнө (анхны ачаалал хурдан).
|
|
604
|
+
pagePdfW = IntArray(totalPages) { 0 }
|
|
605
|
+
pagePdfH = IntArray(totalPages) { 0 }
|
|
522
606
|
}
|
|
523
607
|
// annotationMap is intentionally NOT cleared here.
|
|
524
608
|
// setAnnotations() may have already populated it before startLoad() runs
|
|
@@ -559,6 +643,7 @@ class ExpoPdfReaderView(
|
|
|
559
643
|
// Chunk state reset
|
|
560
644
|
currentViewWidth = viewWidth
|
|
561
645
|
renderedUpTo = -1
|
|
646
|
+
windowStart = 0
|
|
562
647
|
chunkLoading = false
|
|
563
648
|
|
|
564
649
|
withContext(Dispatchers.Main) {
|
|
@@ -570,7 +655,12 @@ class ExpoPdfReaderView(
|
|
|
570
655
|
when (displayMode) {
|
|
571
656
|
"single" -> renderSingleMode()
|
|
572
657
|
"twoup" -> renderAllPages(pdf, pageCount, viewWidth)
|
|
573
|
-
else ->
|
|
658
|
+
else -> renderScrollModeAround(
|
|
659
|
+
pdf,
|
|
660
|
+
pageCount,
|
|
661
|
+
viewWidth,
|
|
662
|
+
currentPageIndex.coerceIn(0, pageCount - 1)
|
|
663
|
+
)
|
|
574
664
|
}
|
|
575
665
|
|
|
576
666
|
// onReady: Main thread дээр шууд дуудна
|
|
@@ -623,34 +713,53 @@ class ExpoPdfReaderView(
|
|
|
623
713
|
withContext(Dispatchers.Main) { renderedUpTo = pageCount - 1 }
|
|
624
714
|
}
|
|
625
715
|
|
|
626
|
-
|
|
627
|
-
|
|
716
|
+
/**
|
|
717
|
+
* continuous / twoupcontinuous — [anchorPage]-ийн эргэн тойронд MAX_RENDERED хүртэлх цонх.
|
|
718
|
+
* Горим солиход одоогийн хуудас хадгалагдана.
|
|
719
|
+
*/
|
|
720
|
+
private suspend fun renderScrollModeAround(
|
|
721
|
+
pdf: PdfRenderer,
|
|
722
|
+
pageCount: Int,
|
|
723
|
+
viewWidth: Int,
|
|
724
|
+
anchorPage: Int
|
|
725
|
+
) {
|
|
628
726
|
withContext(Dispatchers.Main) {
|
|
629
727
|
scrollView.visibility = View.VISIBLE
|
|
630
728
|
pager.visibility = View.GONE
|
|
631
729
|
}
|
|
632
|
-
val
|
|
730
|
+
val target = anchorPage.coerceIn(0, pageCount - 1)
|
|
731
|
+
val newStart = maxOf(0, target - PAGE_CHUNK / 2)
|
|
732
|
+
val newEnd = minOf(pageCount, newStart + MAX_RENDERED)
|
|
733
|
+
withContext(Dispatchers.Main) {
|
|
734
|
+
windowStart = newStart
|
|
735
|
+
renderedUpTo = newStart - 1
|
|
736
|
+
}
|
|
633
737
|
if (displayMode == "twoupcontinuous") {
|
|
634
|
-
renderTwoUpRange(pdf, pageCount, viewWidth,
|
|
635
|
-
// Хос тул renderedUpTo-г тэгш болгоно
|
|
738
|
+
renderTwoUpRange(pdf, pageCount, viewWidth, newStart, newEnd)
|
|
636
739
|
withContext(Dispatchers.Main) {
|
|
637
|
-
|
|
740
|
+
val rawUpTo = if (newEnd % 2 == 0) newEnd - 1 else newEnd - 2
|
|
741
|
+
renderedUpTo = rawUpTo.coerceIn(0, pageCount - 1)
|
|
638
742
|
if (renderedUpTo < pageCount - 1) showLoadingFooter()
|
|
639
743
|
forceLayoutScrollView()
|
|
744
|
+
post { scrollToPage(target, smooth = false) }
|
|
640
745
|
}
|
|
641
746
|
} else {
|
|
642
|
-
for (i in
|
|
747
|
+
for (i in newStart until newEnd) {
|
|
643
748
|
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
644
749
|
val idx = i
|
|
645
750
|
withContext(Dispatchers.Main) {
|
|
646
|
-
addPageRow(
|
|
647
|
-
|
|
751
|
+
addPageRow(
|
|
752
|
+
bmp, idx,
|
|
753
|
+
pagePdfW.getOrElse(idx) { 1 }.toFloat(),
|
|
754
|
+
pagePdfH.getOrElse(idx) { 1 }.toFloat()
|
|
755
|
+
)
|
|
648
756
|
}
|
|
649
757
|
}
|
|
650
758
|
withContext(Dispatchers.Main) {
|
|
651
|
-
renderedUpTo =
|
|
652
|
-
if (
|
|
653
|
-
forceLayoutScrollView()
|
|
759
|
+
renderedUpTo = (newEnd - 1).coerceIn(0, pageCount - 1)
|
|
760
|
+
if (newEnd < pageCount) showLoadingFooter()
|
|
761
|
+
forceLayoutScrollView()
|
|
762
|
+
post { scrollToPage(target, smooth = false) }
|
|
654
763
|
}
|
|
655
764
|
}
|
|
656
765
|
}
|
|
@@ -692,18 +801,32 @@ class ExpoPdfReaderView(
|
|
|
692
801
|
|
|
693
802
|
private fun maybeTriggerChunkLoad(scrollY: Int) {
|
|
694
803
|
if (chunkLoading) return
|
|
695
|
-
if (renderedUpTo >= totalPages - 1) return
|
|
696
804
|
if (displayMode !in listOf("continuous", "twoupcontinuous")) return
|
|
697
805
|
|
|
698
|
-
// Render хийгдсэн хэсгийн 2 дэлгэцийн өмнөөс дараагийн chunk-г эхлүүлнэ
|
|
699
806
|
val visibleBottom = scrollY + scrollView.height
|
|
700
|
-
|
|
701
|
-
|
|
807
|
+
|
|
808
|
+
// Доош scroll: дараагийн chunk ачаална
|
|
809
|
+
if (renderedUpTo < totalPages - 1) {
|
|
810
|
+
val forwardTrigger = container.height - scrollView.height * 2
|
|
811
|
+
if (visibleBottom >= forwardTrigger) {
|
|
812
|
+
chunkLoading = true
|
|
813
|
+
scope.launch {
|
|
814
|
+
try { loadNextChunk() }
|
|
815
|
+
catch (e: CancellationException) { throw e }
|
|
816
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "Chunk load error", e) }
|
|
817
|
+
finally { chunkLoading = false }
|
|
818
|
+
}
|
|
819
|
+
return
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Дээш scroll: өмнөх chunk (continuous = нэг багана; twoupcontinuous = хос мөр prepend).
|
|
824
|
+
if (windowStart > 0 && scrollY <= scrollView.height * 2) {
|
|
702
825
|
chunkLoading = true
|
|
703
826
|
scope.launch {
|
|
704
|
-
try {
|
|
827
|
+
try { loadPrevChunk() }
|
|
705
828
|
catch (e: CancellationException) { throw e }
|
|
706
|
-
catch (e: Exception) { Log.e("ExpoPdfReader", "
|
|
829
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "Prev chunk load error", e) }
|
|
707
830
|
finally { chunkLoading = false }
|
|
708
831
|
}
|
|
709
832
|
}
|
|
@@ -736,12 +859,271 @@ class ExpoPdfReaderView(
|
|
|
736
859
|
}
|
|
737
860
|
withContext(Dispatchers.Main) {
|
|
738
861
|
renderedUpTo = end - 1
|
|
862
|
+
// Sliding window: MAX_RENDERED-с илүү болвол дээд хэсгийг устга
|
|
863
|
+
val excess = pageEntries.size - MAX_RENDERED
|
|
864
|
+
if (excess > 0) {
|
|
865
|
+
val removed = pruneTopPages(excess)
|
|
866
|
+
scrollView.scrollBy(0, -removed)
|
|
867
|
+
}
|
|
739
868
|
if (end < totalPages) showLoadingFooter()
|
|
740
869
|
forceLayoutScrollView()
|
|
741
870
|
}
|
|
742
871
|
}
|
|
743
872
|
}
|
|
744
873
|
|
|
874
|
+
/**
|
|
875
|
+
* Дээд хэсгийн [count] хуудсыг устгаж, bitmap санах ойг чөлөөлнэ.
|
|
876
|
+
* @return устгагдсан хуудсуудын нийт өндөр (px) — scroll тохируулахад ашиглана
|
|
877
|
+
*/
|
|
878
|
+
private fun pruneTopPages(count: Int): Int {
|
|
879
|
+
var removedHeight = 0
|
|
880
|
+
repeat(count) {
|
|
881
|
+
val entry = pageEntries.removeFirstOrNull() ?: return@repeat
|
|
882
|
+
removedHeight += entry.bmpHeight + 12 // +12 нь margin (дээш 6 + доош 6)
|
|
883
|
+
// Bitmap-г чөлөөлнэ
|
|
884
|
+
(entry.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
|
|
885
|
+
container.removeView(entry.frame)
|
|
886
|
+
}
|
|
887
|
+
windowStart += count
|
|
888
|
+
return removedHeight
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Өмнөх [PAGE_CHUNK] хуудсыг дээр нэмж, доод хэсгийг цэвэрлэнэ.
|
|
893
|
+
*/
|
|
894
|
+
private suspend fun loadPrevChunk() {
|
|
895
|
+
val pdf = renderer ?: return
|
|
896
|
+
if (displayMode == "twoupcontinuous") {
|
|
897
|
+
loadPrevChunkTwoup(pdf)
|
|
898
|
+
return
|
|
899
|
+
}
|
|
900
|
+
val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
|
|
901
|
+
val prevEnd = windowStart
|
|
902
|
+
if (prevStart >= prevEnd) return
|
|
903
|
+
val viewWidth = currentViewWidth
|
|
904
|
+
|
|
905
|
+
data class PageBmp(val bmp: Bitmap, val idx: Int, val pdfW: Float, val pdfH: Float)
|
|
906
|
+
val pages = mutableListOf<PageBmp>()
|
|
907
|
+
for (i in prevStart until prevEnd) {
|
|
908
|
+
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
909
|
+
pages.add(PageBmp(bmp, i,
|
|
910
|
+
pagePdfW.getOrElse(i) { 1 }.toFloat(),
|
|
911
|
+
pagePdfH.getOrElse(i) { 1 }.toFloat()))
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
withContext(Dispatchers.Main) {
|
|
915
|
+
var insertedHeight = 0
|
|
916
|
+
// Reverse order-оор insert хийж эцэст нь зөв дараалал гарна
|
|
917
|
+
// (бүгд index 0-д insert хийгддэг тул)
|
|
918
|
+
for (page in pages.reversed()) {
|
|
919
|
+
prependPageFrame(page.bmp, page.idx, page.pdfW, page.pdfH)
|
|
920
|
+
insertedHeight += page.bmp.height + 12
|
|
921
|
+
}
|
|
922
|
+
windowStart = prevStart
|
|
923
|
+
renderedUpTo = maxOf(renderedUpTo, windowStart + pageEntries.size - 1)
|
|
924
|
+
|
|
925
|
+
// Доод хэсгийг цэвэрлэнэ
|
|
926
|
+
val excess = pageEntries.size - MAX_RENDERED
|
|
927
|
+
if (excess > 0) pruneBottomPages(excess)
|
|
928
|
+
|
|
929
|
+
// Дээр нэмсэн өндрийг scroll-д нэмж харагдах хэсгийг хэвээр үлдээнэ
|
|
930
|
+
scrollView.scrollBy(0, insertedHeight)
|
|
931
|
+
forceLayoutScrollView()
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** twoupcontinuous: өмнөх chunk-ийг хос мөрөөр дээр нэмнэ. */
|
|
936
|
+
private suspend fun loadPrevChunkTwoup(pdf: PdfRenderer) {
|
|
937
|
+
val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
|
|
938
|
+
val prevEnd = windowStart
|
|
939
|
+
if (prevStart >= prevEnd) return
|
|
940
|
+
val viewWidth = currentViewWidth
|
|
941
|
+
val halfW = viewWidth / 2
|
|
942
|
+
|
|
943
|
+
data class RowBmps(val leftIdx: Int, val leftBmp: Bitmap, val rightBmp: Bitmap?)
|
|
944
|
+
val rows = mutableListOf<RowBmps>()
|
|
945
|
+
var i = prevStart
|
|
946
|
+
while (i < prevEnd) {
|
|
947
|
+
val leftBmp = renderPageBitmap(pdf, i, halfW)
|
|
948
|
+
val rightBmp = if (i + 1 < minOf(prevEnd, totalPages))
|
|
949
|
+
renderPageBitmap(pdf, i + 1, halfW) else null
|
|
950
|
+
rows.add(RowBmps(i, leftBmp, rightBmp))
|
|
951
|
+
i += 2
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
withContext(Dispatchers.Main) {
|
|
955
|
+
var insertedHeight = 0
|
|
956
|
+
for (rb in rows.asReversed()) {
|
|
957
|
+
prependTwoupRowToContainer(
|
|
958
|
+
rb.leftBmp, rb.leftIdx,
|
|
959
|
+
rb.rightBmp,
|
|
960
|
+
if (rb.rightBmp != null) rb.leftIdx + 1 else null,
|
|
961
|
+
viewWidth
|
|
962
|
+
)
|
|
963
|
+
insertedHeight += rb.leftBmp.height + 12
|
|
964
|
+
}
|
|
965
|
+
windowStart = prevStart
|
|
966
|
+
renderedUpTo = windowStart + pageEntries.size - 1
|
|
967
|
+
|
|
968
|
+
while (pageEntries.size > MAX_RENDERED) {
|
|
969
|
+
pruneBottomTwoupOneRow()
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
scrollView.scrollBy(0, insertedHeight)
|
|
973
|
+
forceLayoutScrollView()
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/** twoup мөрийг container + pageEntries-ийн эхэнд оруулна (дээд chunk). */
|
|
978
|
+
private fun prependTwoupRowToContainer(
|
|
979
|
+
leftBmp: Bitmap, li: Int,
|
|
980
|
+
rightBmp: Bitmap?, ri: Int?,
|
|
981
|
+
viewWidth: Int
|
|
982
|
+
) {
|
|
983
|
+
val rowH = leftBmp.height
|
|
984
|
+
val row = LinearLayout(context).apply {
|
|
985
|
+
orientation = LinearLayout.HORIZONTAL
|
|
986
|
+
layoutParams = LinearLayout.LayoutParams(viewWidth, rowH)
|
|
987
|
+
.apply { setMargins(0, 6, 0, 6) }
|
|
988
|
+
}
|
|
989
|
+
val (leftFrame, leftEntry) = buildPageFrameDetached(
|
|
990
|
+
leftBmp, li,
|
|
991
|
+
pagePdfW.getOrElse(li) { 1 }.toFloat(),
|
|
992
|
+
pagePdfH.getOrElse(li) { 1 }.toFloat(),
|
|
993
|
+
1f
|
|
994
|
+
)
|
|
995
|
+
row.addView(leftFrame)
|
|
996
|
+
if (rightBmp != null && ri != null) {
|
|
997
|
+
val (rightFrame, rightEntry) = buildPageFrameDetached(
|
|
998
|
+
rightBmp, ri,
|
|
999
|
+
pagePdfW.getOrElse(ri) { 1 }.toFloat(),
|
|
1000
|
+
pagePdfH.getOrElse(ri) { 1 }.toFloat(),
|
|
1001
|
+
1f
|
|
1002
|
+
)
|
|
1003
|
+
row.addView(rightFrame)
|
|
1004
|
+
pageEntries.add(0, rightEntry)
|
|
1005
|
+
pageEntries.add(0, leftEntry)
|
|
1006
|
+
} else {
|
|
1007
|
+
row.addView(View(context).apply {
|
|
1008
|
+
layoutParams = LinearLayout.LayoutParams(0, rowH, 1f)
|
|
1009
|
+
})
|
|
1010
|
+
pageEntries.add(0, leftEntry)
|
|
1011
|
+
}
|
|
1012
|
+
container.addView(row, 0)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/** twoupcontinuous: доод талын нэг мөрийг (эсвэл сүүлийн entry) устгана. */
|
|
1016
|
+
private fun pruneBottomTwoupOneRow() {
|
|
1017
|
+
val last = pageEntries.lastOrNull() ?: return
|
|
1018
|
+
val row = last.frame.parent as? LinearLayout
|
|
1019
|
+
if (row != null && row.parent === container && row.childCount > 0) {
|
|
1020
|
+
val removeList = pageEntries.filter { it.frame.parent === row }
|
|
1021
|
+
for (e in removeList) {
|
|
1022
|
+
(e.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
|
|
1023
|
+
pageEntries.remove(e)
|
|
1024
|
+
}
|
|
1025
|
+
container.removeView(row)
|
|
1026
|
+
} else {
|
|
1027
|
+
val e = pageEntries.removeLastOrNull() ?: return
|
|
1028
|
+
(e.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
|
|
1029
|
+
container.removeView(e.frame)
|
|
1030
|
+
}
|
|
1031
|
+
renderedUpTo = windowStart + pageEntries.size - 1
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
/**
|
|
1035
|
+
* Дээр (index 0) шинэ хуудас оруулна. windowStart-г дуудагч тал шинэчилнэ.
|
|
1036
|
+
*/
|
|
1037
|
+
private fun prependPageFrame(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float) {
|
|
1038
|
+
val bmpW = bmp.width
|
|
1039
|
+
val bmpH = bmp.height
|
|
1040
|
+
val frame = FrameLayout(context).apply {
|
|
1041
|
+
layoutParams = LinearLayout.LayoutParams(bmpW, bmpH).apply { setMargins(0, 6, 0, 6) }
|
|
1042
|
+
}
|
|
1043
|
+
val iv = ImageView(context).apply {
|
|
1044
|
+
layoutParams = FrameLayout.LayoutParams(bmpW, bmpH)
|
|
1045
|
+
scaleType = ImageView.ScaleType.FIT_XY
|
|
1046
|
+
setImageBitmap(bmp)
|
|
1047
|
+
}
|
|
1048
|
+
val canvasView = AnnotationCanvasView(context, pageIdx, pdfW, pdfH).apply {
|
|
1049
|
+
layoutParams = FrameLayout.LayoutParams(bmpW, bmpH)
|
|
1050
|
+
translationZ = 1f
|
|
1051
|
+
}
|
|
1052
|
+
frame.addView(iv)
|
|
1053
|
+
frame.addView(canvasView)
|
|
1054
|
+
container.addView(frame, 0)
|
|
1055
|
+
pageEntries.add(0, PageEntry(frame, canvasView, bmpH))
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/**
|
|
1059
|
+
* Доод хэсгийн [count] хуудсыг устгаж bitmap санах ойг чөлөөлнэ.
|
|
1060
|
+
* renderedUpTo-г буулгаж дараагийн доош scroll-д re-render хийгдэхийг зөвшөөрнэ.
|
|
1061
|
+
*/
|
|
1062
|
+
private fun pruneBottomPages(count: Int) {
|
|
1063
|
+
repeat(count) {
|
|
1064
|
+
val entry = pageEntries.removeLastOrNull() ?: return@repeat
|
|
1065
|
+
(entry.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
|
|
1066
|
+
container.removeView(entry.frame)
|
|
1067
|
+
}
|
|
1068
|
+
renderedUpTo = windowStart + pageEntries.size - 1
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* Render хийгдээгүй хуудас руу үсрэх.
|
|
1073
|
+
* Бүх одоогийн view-г цэвэрлэж, targetPage-ийн эргэн тойрны хуудсуудыг render хийнэ.
|
|
1074
|
+
*/
|
|
1075
|
+
private fun jumpToPage(targetPage: Int) {
|
|
1076
|
+
val pdf = renderer ?: return
|
|
1077
|
+
val viewWidth = currentViewWidth.takeIf { it > 0 } ?: width.takeIf { it > 0 } ?: return
|
|
1078
|
+
renderJob?.cancel()
|
|
1079
|
+
renderJob = scope.launch {
|
|
1080
|
+
try {
|
|
1081
|
+
// Шинэ window: targetPage-ийн хагас chunk өмнөөс эхэлнэ
|
|
1082
|
+
val newStart = maxOf(0, targetPage - PAGE_CHUNK / 2)
|
|
1083
|
+
val newEnd = minOf(totalPages, newStart + MAX_RENDERED)
|
|
1084
|
+
|
|
1085
|
+
withContext(Dispatchers.Main) {
|
|
1086
|
+
container.removeAllViews()
|
|
1087
|
+
pageEntries.clear()
|
|
1088
|
+
loadingFooter = null
|
|
1089
|
+
windowStart = newStart
|
|
1090
|
+
renderedUpTo = newStart - 1
|
|
1091
|
+
chunkLoading = false
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
if (displayMode == "twoupcontinuous") {
|
|
1095
|
+
renderTwoUpRange(pdf, totalPages, viewWidth, newStart, newEnd)
|
|
1096
|
+
withContext(Dispatchers.Main) {
|
|
1097
|
+
val rawUpTo = if (newEnd % 2 == 0) newEnd - 1 else newEnd - 2
|
|
1098
|
+
renderedUpTo = rawUpTo.coerceIn(0, totalPages - 1)
|
|
1099
|
+
if (renderedUpTo < totalPages - 1) showLoadingFooter()
|
|
1100
|
+
forceLayoutScrollView()
|
|
1101
|
+
post { scrollToPage(targetPage, smooth = false) }
|
|
1102
|
+
}
|
|
1103
|
+
} else {
|
|
1104
|
+
for (i in newStart until newEnd) {
|
|
1105
|
+
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
1106
|
+
val idx = i
|
|
1107
|
+
withContext(Dispatchers.Main) {
|
|
1108
|
+
addPageRow(
|
|
1109
|
+
bmp, idx,
|
|
1110
|
+
pagePdfW.getOrElse(idx) { 1 }.toFloat(),
|
|
1111
|
+
pagePdfH.getOrElse(idx) { 1 }.toFloat()
|
|
1112
|
+
)
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
withContext(Dispatchers.Main) {
|
|
1116
|
+
renderedUpTo = newEnd - 1
|
|
1117
|
+
if (newEnd < totalPages) showLoadingFooter()
|
|
1118
|
+
forceLayoutScrollView()
|
|
1119
|
+
post { scrollToPage(targetPage, smooth = false) }
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
} catch (e: CancellationException) { throw e }
|
|
1123
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "JumpToPage error", e) }
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
745
1127
|
/**
|
|
746
1128
|
* React Native Fabric нь child view-н requestLayout()-г таслах тул
|
|
747
1129
|
* addView() дараа layout pass автоматаар ажиллахгүй.
|
|
@@ -777,9 +1159,9 @@ class ExpoPdfReaderView(
|
|
|
777
1159
|
private fun addPageRow(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float) =
|
|
778
1160
|
container.addView(buildPageFrame(bmp, pageIdx, pdfW, pdfH))
|
|
779
1161
|
|
|
780
|
-
private fun
|
|
781
|
-
|
|
782
|
-
|
|
1162
|
+
private fun buildPageFrameDetached(
|
|
1163
|
+
bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float, weight: Float = 0f
|
|
1164
|
+
): Pair<FrameLayout, PageEntry> {
|
|
783
1165
|
val bmpW = bmp.width
|
|
784
1166
|
val bmpH = bmp.height
|
|
785
1167
|
val frame = FrameLayout(context).apply {
|
|
@@ -800,13 +1182,22 @@ class ExpoPdfReaderView(
|
|
|
800
1182
|
}
|
|
801
1183
|
frame.addView(iv)
|
|
802
1184
|
frame.addView(canvasView)
|
|
803
|
-
|
|
1185
|
+
return frame to PageEntry(frame, canvasView, bmpH)
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
private fun buildPageFrame(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float, weight: Float = 0f): FrameLayout {
|
|
1189
|
+
val (frame, entry) = buildPageFrameDetached(bmp, pageIdx, pdfW, pdfH, weight)
|
|
1190
|
+
pageEntries.add(entry)
|
|
804
1191
|
return frame
|
|
805
1192
|
}
|
|
806
1193
|
|
|
807
1194
|
private suspend fun renderPageBitmap(pdf: PdfRenderer, index: Int, targetWidth: Int): Bitmap =
|
|
808
1195
|
withContext(pdfDispatcher) {
|
|
809
1196
|
pdf.openPage(index).use { page ->
|
|
1197
|
+
if (index < pagePdfW.size) {
|
|
1198
|
+
pagePdfW[index] = page.width
|
|
1199
|
+
pagePdfH[index] = page.height
|
|
1200
|
+
}
|
|
810
1201
|
val s = targetWidth.toFloat() / page.width.toFloat()
|
|
811
1202
|
val h = (page.height * s).toInt().coerceAtLeast(1)
|
|
812
1203
|
val bmp = Bitmap.createBitmap(targetWidth, h, Bitmap.Config.ARGB_8888)
|
|
@@ -844,8 +1235,9 @@ class ExpoPdfReaderView(
|
|
|
844
1235
|
|
|
845
1236
|
// ─────────────────────────────────────────────────────────────────────────
|
|
846
1237
|
// SinglePageAdapter — RecyclerView adapter for "single" display mode
|
|
847
|
-
// •
|
|
848
|
-
// •
|
|
1238
|
+
// • vertical PagerSnapHelper — босоо scroll-оор хуудас солих
|
|
1239
|
+
// • ZoomRecyclerView zoom-г удирдана (adapter энгийн FrameLayout ашиглана)
|
|
1240
|
+
// • lazy bitmap loading
|
|
849
1241
|
// ─────────────────────────────────────────────────────────────────────────
|
|
850
1242
|
|
|
851
1243
|
inner class SinglePageAdapter : RecyclerView.Adapter<SinglePageAdapter.Holder>() {
|
|
@@ -867,9 +1259,6 @@ class ExpoPdfReaderView(
|
|
|
867
1259
|
)
|
|
868
1260
|
setBackgroundColor(Color.parseColor("#E8E8E8"))
|
|
869
1261
|
}
|
|
870
|
-
// ImageView starts WRAP_CONTENT; sized to bitmap in onBindViewHolder so that
|
|
871
|
-
// the AnnotationCanvasView can use the same pixel dimensions — no FIT_CENTER
|
|
872
|
-
// offset mis-alignment.
|
|
873
1262
|
val iv = ImageView(context).apply {
|
|
874
1263
|
layoutParams = FrameLayout.LayoutParams(
|
|
875
1264
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
@@ -886,51 +1275,43 @@ class ExpoPdfReaderView(
|
|
|
886
1275
|
holder.boundPage = position
|
|
887
1276
|
holder.loadJob?.cancel()
|
|
888
1277
|
holder.imageView.setImageBitmap(null)
|
|
1278
|
+
// Recycle хийгдсэн view-н zoom transform-г цэвэрлэнэ
|
|
1279
|
+
holder.frame.scaleX = 1f; holder.frame.scaleY = 1f
|
|
1280
|
+
holder.frame.translationX = 0f; holder.frame.translationY = 0f
|
|
889
1281
|
|
|
890
|
-
// Remove old canvas and register new one for this page
|
|
891
1282
|
holder.currentCanvas?.let {
|
|
892
1283
|
singlePageCanvases.remove(it.pageIndex)
|
|
893
1284
|
holder.frame.removeView(it)
|
|
894
1285
|
}
|
|
895
|
-
|
|
896
|
-
val pdfH = pagePdfH.getOrElse(position) { 1 }.toFloat()
|
|
897
|
-
// Canvas starts with placeholder size; resized to exact bitmap dims after load.
|
|
898
|
-
// translationZ ensures canvas always renders above ImageView regardless of layout order.
|
|
899
|
-
val canvas = AnnotationCanvasView(context, position, pdfW, pdfH).apply {
|
|
900
|
-
layoutParams = FrameLayout.LayoutParams(
|
|
901
|
-
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
902
|
-
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
903
|
-
android.view.Gravity.CENTER
|
|
904
|
-
)
|
|
905
|
-
translationZ = 1f
|
|
906
|
-
}
|
|
907
|
-
holder.frame.addView(canvas)
|
|
908
|
-
holder.currentCanvas = canvas
|
|
909
|
-
singlePageCanvases[position] = canvas
|
|
1286
|
+
holder.currentCanvas = null
|
|
910
1287
|
|
|
911
|
-
// Lazy bitmap rendering
|
|
912
1288
|
holder.loadJob = scope.launch {
|
|
913
1289
|
val pdf = renderer ?: return@launch
|
|
914
1290
|
val bmp = renderPageBitmap(pdf, position, this@ExpoPdfReaderView.width)
|
|
1291
|
+
val pdfW = pagePdfW.getOrElse(position) { 1 }.toFloat()
|
|
1292
|
+
val pdfH = pagePdfH.getOrElse(position) { 1 }.toFloat()
|
|
915
1293
|
withContext(Dispatchers.Main) {
|
|
916
|
-
if (holder.boundPage
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
holder.imageView.setImageBitmap(bmp)
|
|
922
|
-
holder.currentCanvas?.layoutParams = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
|
|
923
|
-
// React Native Fabric blocks requestLayout() on child views.
|
|
924
|
-
// Manually measure+layout the frame so the updated layoutParams
|
|
925
|
-
// take effect immediately without waiting for the next Fabric pass.
|
|
926
|
-
val fw = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: return@withContext
|
|
927
|
-
val fh = this@ExpoPdfReaderView.height.takeIf { it > 0 } ?: return@withContext
|
|
928
|
-
holder.frame.measure(
|
|
929
|
-
MeasureSpec.makeMeasureSpec(fw, MeasureSpec.EXACTLY),
|
|
930
|
-
MeasureSpec.makeMeasureSpec(fh, MeasureSpec.EXACTLY)
|
|
1294
|
+
if (holder.boundPage != position) return@withContext
|
|
1295
|
+
val canvas = AnnotationCanvasView(context, position, pdfW, pdfH).apply {
|
|
1296
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
1297
|
+
bmp.width, bmp.height,
|
|
1298
|
+
android.view.Gravity.CENTER
|
|
931
1299
|
)
|
|
932
|
-
|
|
1300
|
+
translationZ = 1f
|
|
933
1301
|
}
|
|
1302
|
+
holder.frame.addView(canvas)
|
|
1303
|
+
holder.currentCanvas = canvas
|
|
1304
|
+
singlePageCanvases[position] = canvas
|
|
1305
|
+
val lp = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
|
|
1306
|
+
holder.imageView.layoutParams = lp
|
|
1307
|
+
holder.imageView.setImageBitmap(bmp)
|
|
1308
|
+
val fw = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: return@withContext
|
|
1309
|
+
val fh = this@ExpoPdfReaderView.height.takeIf { it > 0 } ?: return@withContext
|
|
1310
|
+
holder.frame.measure(
|
|
1311
|
+
MeasureSpec.makeMeasureSpec(fw, MeasureSpec.EXACTLY),
|
|
1312
|
+
MeasureSpec.makeMeasureSpec(fh, MeasureSpec.EXACTLY)
|
|
1313
|
+
)
|
|
1314
|
+
holder.frame.layout(0, 0, fw, fh)
|
|
934
1315
|
}
|
|
935
1316
|
}
|
|
936
1317
|
}
|
|
@@ -946,6 +1327,101 @@ class ExpoPdfReaderView(
|
|
|
946
1327
|
}
|
|
947
1328
|
}
|
|
948
1329
|
|
|
1330
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1331
|
+
// ZoomRecyclerView — single mode-д pinch-to-zoom + pan дэмждэг RecyclerView
|
|
1332
|
+
//
|
|
1333
|
+
// Яагаад RecyclerView subclass хийх шаардлагатай вэ:
|
|
1334
|
+
// AnnotationCanvasView ACTION_DOWN-д false буцаана → RecyclerView
|
|
1335
|
+
// gesture-г эзэмшиж авна → дараагийн ACTION_POINTER_DOWN (2-р хуруу)
|
|
1336
|
+
// шууд RecyclerView.onTouchEvent-д очдог.
|
|
1337
|
+
// RecyclerView subclass хийснээр onTouchEvent-д multi-touch-г зохицуулна.
|
|
1338
|
+
//
|
|
1339
|
+
// Zoom тооцоо (pivot=0,0 + translation):
|
|
1340
|
+
// Item view point (px,py) → дэлгэц: (px*scale + tx, py*scale + ty)
|
|
1341
|
+
// Focus (fx,fy) тогтвортой байхад: new_tx = fx - (fx - tx) * scaleFactor
|
|
1342
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1343
|
+
|
|
1344
|
+
inner class ZoomRecyclerView(context: Context) : RecyclerView(context) {
|
|
1345
|
+
private var currentScale = 1f
|
|
1346
|
+
private var tx = 0f
|
|
1347
|
+
private var ty = 0f
|
|
1348
|
+
private var panX = 0f
|
|
1349
|
+
private var panY = 0f
|
|
1350
|
+
|
|
1351
|
+
private val zoomDetector = ScaleGestureDetector(context,
|
|
1352
|
+
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
1353
|
+
override fun onScale(d: ScaleGestureDetector): Boolean {
|
|
1354
|
+
val newScale = (currentScale * d.scaleFactor).coerceIn(minZoom, maxZoom)
|
|
1355
|
+
val af = newScale / currentScale
|
|
1356
|
+
// Focus point тогтвортой байхын zoom-to-point тооцоо
|
|
1357
|
+
tx = d.focusX - (d.focusX - tx) * af
|
|
1358
|
+
ty = d.focusY - (d.focusY - ty) * af
|
|
1359
|
+
currentScale = newScale
|
|
1360
|
+
clampAndApply()
|
|
1361
|
+
return true
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
)
|
|
1365
|
+
|
|
1366
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
1367
|
+
// Zoom хийгдсэн + tool идэвхгүй → pan хийхийн тулд ACTION_DOWN-г intercept
|
|
1368
|
+
if (currentScale > 1f && activeTool == null && ev.action == MotionEvent.ACTION_DOWN) {
|
|
1369
|
+
return true
|
|
1370
|
+
}
|
|
1371
|
+
return super.onInterceptTouchEvent(ev)
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
1375
|
+
zoomDetector.onTouchEvent(ev)
|
|
1376
|
+
|
|
1377
|
+
// 2 хуруу байвал RecyclerView scroll хийхгүйгээр zoom-г л хийнэ
|
|
1378
|
+
if (ev.pointerCount >= 2 || zoomDetector.isInProgress) {
|
|
1379
|
+
return true
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Zoom хийгдсэн үед нэг хуруугаар pan (tool идэвхгүй)
|
|
1383
|
+
if (currentScale > 1f && activeTool == null) {
|
|
1384
|
+
when (ev.actionMasked) {
|
|
1385
|
+
MotionEvent.ACTION_DOWN -> { panX = ev.x; panY = ev.y }
|
|
1386
|
+
MotionEvent.ACTION_MOVE -> {
|
|
1387
|
+
tx += ev.x - panX
|
|
1388
|
+
ty += ev.y - panY
|
|
1389
|
+
panX = ev.x; panY = ev.y
|
|
1390
|
+
clampAndApply()
|
|
1391
|
+
}
|
|
1392
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { }
|
|
1393
|
+
}
|
|
1394
|
+
return true
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
return super.onTouchEvent(ev)
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
fun resetZoom() {
|
|
1401
|
+
currentScale = 1f; tx = 0f; ty = 0f
|
|
1402
|
+
applyToCurrentItem()
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
private fun clampAndApply() {
|
|
1406
|
+
val w = width.toFloat(); val h = height.toFloat()
|
|
1407
|
+
if (w > 0f && h > 0f) {
|
|
1408
|
+
val sw = w * currentScale; val sh = h * currentScale
|
|
1409
|
+
tx = if (sw > w) tx.coerceIn(w - sw, 0f) else 0f
|
|
1410
|
+
ty = if (sh > h) ty.coerceIn(h - sh, 0f) else 0f
|
|
1411
|
+
}
|
|
1412
|
+
applyToCurrentItem()
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
private fun applyToCurrentItem() {
|
|
1416
|
+
val lm = layoutManager as? LinearLayoutManager ?: return
|
|
1417
|
+
val pos = lm.findFirstVisibleItemPosition().takeIf { it >= 0 } ?: return
|
|
1418
|
+
val itemView = lm.findViewByPosition(pos) ?: return
|
|
1419
|
+
itemView.pivotX = 0f; itemView.pivotY = 0f
|
|
1420
|
+
itemView.scaleX = currentScale; itemView.scaleY = currentScale
|
|
1421
|
+
itemView.translationX = tx; itemView.translationY = ty
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
|
|
949
1425
|
// ─────────────────────────────────────────────────────────────────────────
|
|
950
1426
|
// AnnotationCanvasView (inner class — full access to outer view state)
|
|
951
1427
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -53,6 +53,9 @@ final class ExpoPdfReaderView: ExpoView {
|
|
|
53
53
|
private var draggingPage: PDFPage?
|
|
54
54
|
private var dragStartPagePoint: CGPoint?
|
|
55
55
|
private var dragStartBounds: CGRect?
|
|
56
|
+
// Zoom range: minZoom=1.0 (fit width), maxZoom=5.0 → pinch-to-zoom дэмжинэ
|
|
57
|
+
private var minZoom: CGFloat = 1.0
|
|
58
|
+
private var maxZoom: CGFloat = 5.0
|
|
56
59
|
|
|
57
60
|
required init(appContext: AppContext? = nil) {
|
|
58
61
|
super.init(appContext: appContext)
|
|
@@ -141,11 +144,12 @@ final class ExpoPdfReaderView: ExpoView {
|
|
|
141
144
|
|
|
142
145
|
guard viewWidth > 0, pageBounds.width > 0 else { return }
|
|
143
146
|
|
|
144
|
-
let
|
|
145
|
-
//
|
|
146
|
-
pdfView.minScaleFactor =
|
|
147
|
-
pdfView.maxScaleFactor =
|
|
148
|
-
|
|
147
|
+
let fitScale = viewWidth / pageBounds.width
|
|
148
|
+
// min/max өөр байх ёстой — pinch-to-zoom ажиллана (Android-тай ижил)
|
|
149
|
+
pdfView.minScaleFactor = fitScale * minZoom
|
|
150
|
+
pdfView.maxScaleFactor = fitScale * maxZoom
|
|
151
|
+
// Анхны scale = fit width. PDFView default scaleFactor=1.0 байдаг тул үргэлж fitScale тохируулна
|
|
152
|
+
pdfView.scaleFactor = fitScale
|
|
149
153
|
|
|
150
154
|
// Also make sure the underlying scroll view never scrolls horizontally
|
|
151
155
|
if let scrollView = findScrollView(in: pdfView) {
|
|
@@ -291,8 +295,19 @@ final class ExpoPdfReaderView: ExpoView {
|
|
|
291
295
|
lastPageIndex = index
|
|
292
296
|
}
|
|
293
297
|
|
|
294
|
-
func setMinZoom(_ v: Double) {
|
|
295
|
-
|
|
298
|
+
func setMinZoom(_ v: Double) {
|
|
299
|
+
minZoom = CGFloat(v)
|
|
300
|
+
// scaleToFitWidth дахин дуудагдаагүй бол одоогийн fitScale-аар тооцоо
|
|
301
|
+
if pdfView.document != nil {
|
|
302
|
+
scaleToFitWidth()
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
func setMaxZoom(_ v: Double) {
|
|
306
|
+
maxZoom = CGFloat(v)
|
|
307
|
+
if pdfView.document != nil {
|
|
308
|
+
scaleToFitWidth()
|
|
309
|
+
}
|
|
310
|
+
}
|
|
296
311
|
|
|
297
312
|
func setTool(_ tool: String?) {
|
|
298
313
|
currentTool = tool != nil ? AnnotationTool(rawValue: tool!) : nil
|
package/package.json
CHANGED