@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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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 =
|
|
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 =
|
|
479
|
-
container.scaleY =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
541
|
-
|
|
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 ->
|
|
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
|
-
|
|
648
|
-
|
|
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
|
|
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,
|
|
656
|
-
// Хос тул renderedUpTo-г тэгш болгоно
|
|
835
|
+
renderTwoUpRange(pdf, pageCount, viewWidth, newStart, newEnd)
|
|
657
836
|
withContext(Dispatchers.Main) {
|
|
658
|
-
|
|
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
|
|
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(
|
|
668
|
-
|
|
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 =
|
|
673
|
-
if (
|
|
674
|
-
forceLayoutScrollView()
|
|
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
|
-
|
|
904
|
-
|
|
905
|
-
val idx = i
|
|
1191
|
+
if (displayMode == "twoupcontinuous") {
|
|
1192
|
+
renderTwoUpRange(pdf, totalPages, viewWidth, newStart, newEnd)
|
|
906
1193
|
withContext(Dispatchers.Main) {
|
|
907
|
-
|
|
908
|
-
|
|
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
|
|
960
|
-
|
|
961
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
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 >
|
|
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
|
|
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 >
|
|
1496
|
+
if (currentScale > 1.02f && activeTool == null) {
|
|
1174
1497
|
when (ev.actionMasked) {
|
|
1175
|
-
MotionEvent.ACTION_DOWN -> {
|
|
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