@june24/expo-pdf-reader 0.1.27 → 0.1.29

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.
@@ -95,6 +95,8 @@ class ExpoPdfReaderView(
95
95
  private var windowStart = 0 // pageEntries[0]-н absolute page index
96
96
  private var chunkLoading = false // chunk дуусах хүртэл дахин trigger хийхгүй
97
97
  private var currentViewWidth = 0 // chunk load-д дахин хэрэглэнэ
98
+ /** Thumbnail panel нээх/хаах зэргээр RN өргөн өөрчлөгдөнө — reflow-ийн суурь */
99
+ private var lastHostWidthForPdf = 0
98
100
  private var loadingFooter: View? = null
99
101
 
100
102
  companion object {
@@ -133,42 +135,61 @@ class ExpoPdfReaderView(
133
135
  private var maxZoom = 5.0f
134
136
  private var currentZoom = 1.0f
135
137
 
138
+ /** RN / Drawer зэрэг дээд scroll хуудас pinch-ийг scroll болгож идэвхжүүлэхээс сэргийлнэ */
139
+ private fun propagateDisallowInterceptFrom(view: View, disallow: Boolean) {
140
+ var p: ViewParent? = view.parent
141
+ while (p is ViewGroup) {
142
+ p.requestDisallowInterceptTouchEvent(disallow)
143
+ p = p.parent
144
+ }
145
+ }
146
+
136
147
  /**
137
- * continuous / twoup / twoupcontinuous — pinch + zoom үед зөвхөн pan (босоо scroll унтрах).
138
- * Pinch-ийг [dispatchTouchEvent]-ийн эхэнд дуудаж хүү canvas POINTER_DOWN-д баригдахаас өмнө 2 хуруу хүлээнэ.
148
+ * continuous / twoup / twoupcontinuous — pinch + zoom хийсэн үед pan (хөндлөн/босоо),
149
+ * scroll-ийг идэвхгүй болгоно. Zoom = 1 үед хэвийн босоо scroll.
139
150
  */
140
151
  private inner class ZoomableScrollView(ctx: Context) : ScrollView(ctx) {
141
- /** ScrollView-д [isScrollEnabled] байхгүй тул: false үед босоо scroll (super) ажиллахгүй. */
142
- var allowVerticalScrollFromTouch = true
152
+
153
+ private var panX = 0f
154
+ private var panY = 0f
155
+
156
+ private fun isZoomedForPan(): Boolean =
157
+ activeTool == null && currentZoom > 1.02f
143
158
 
144
159
  private val pinch = ScaleGestureDetector(
145
160
  ctx,
146
161
  object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
147
162
  override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
148
163
  if (activeTool != null) return false
164
+ stopNestedScroll()
165
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, true)
149
166
  parent?.requestDisallowInterceptTouchEvent(true)
150
167
  return true
151
168
  }
152
169
 
153
170
  override fun onScale(detector: ScaleGestureDetector): Boolean {
154
171
  if (activeTool != null) return false
155
- applyZoom((currentZoom * detector.scaleFactor).coerceIn(minZoom, maxZoom))
172
+ applyZoom(
173
+ (currentZoom * detector.scaleFactor).coerceIn(minZoom, maxZoom),
174
+ detector.focusX,
175
+ detector.focusY
176
+ )
156
177
  return true
157
178
  }
158
179
 
159
180
  override fun onScaleEnd(detector: ScaleGestureDetector) {
160
181
  parent?.requestDisallowInterceptTouchEvent(false)
182
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, false)
161
183
  }
162
184
  }
163
185
  )
164
- private var panLastX = 0f
165
- private var panLastY = 0f
166
186
 
167
187
  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
168
188
  if (activeTool != null) return super.onInterceptTouchEvent(ev)
169
- // 2+ хуруу эсвэл pinch эхэлсэн үед ScrollView босоо scroll intercept хийхгүй (scroll + zoom өрсөлдөнө)
170
- if (!allowVerticalScrollFromTouch) return false
171
189
  if (ev.pointerCount > 1 || pinch.isInProgress) return false
190
+ if (isZoomedForPan() && ev.actionMasked == MotionEvent.ACTION_DOWN) {
191
+ return true
192
+ }
172
193
  return super.onInterceptTouchEvent(ev)
173
194
  }
174
195
 
@@ -179,36 +200,56 @@ class ExpoPdfReaderView(
179
200
  stopNestedScroll()
180
201
  }
181
202
  if (pinch.isInProgress || ev.pointerCount > 1) {
203
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, true)
182
204
  parent?.requestDisallowInterceptTouchEvent(true)
183
205
  }
206
+ if (ev.actionMasked == MotionEvent.ACTION_UP ||
207
+ ev.actionMasked == MotionEvent.ACTION_CANCEL
208
+ ) {
209
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, false)
210
+ }
184
211
  }
185
212
  return super.dispatchTouchEvent(ev)
186
213
  }
187
214
 
188
215
  override fun onTouchEvent(ev: MotionEvent): Boolean {
189
216
  if (activeTool != null) return super.onTouchEvent(ev)
217
+
190
218
  if (pinch.isInProgress || ev.pointerCount > 1) {
191
219
  return true
192
220
  }
193
- if (currentZoom > 1.02f) {
221
+
222
+ if (isZoomedForPan()) {
194
223
  when (ev.actionMasked) {
195
224
  MotionEvent.ACTION_DOWN -> {
196
- panLastX = ev.x
197
- panLastY = ev.y
225
+ stopNestedScroll()
226
+ panX = ev.x
227
+ panY = ev.y
198
228
  }
199
229
  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)
230
+ container.translationX += ev.x - panX
231
+ container.translationY += ev.y - panY
232
+ panX = ev.x
233
+ panY = ev.y
234
+ clampContainerTranslationsInPlace()
205
235
  }
236
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { }
206
237
  }
207
238
  return true
208
239
  }
209
- if (!allowVerticalScrollFromTouch) return true
240
+
210
241
  return super.onTouchEvent(ev)
211
242
  }
243
+
244
+ override fun canScrollVertically(direction: Int): Boolean {
245
+ if (isZoomedForPan()) return false
246
+ return super.canScrollVertically(direction)
247
+ }
248
+
249
+ override fun canScrollHorizontally(direction: Int): Boolean {
250
+ if (isZoomedForPan()) return false
251
+ return super.canScrollHorizontally(direction)
252
+ }
212
253
  }
213
254
 
214
255
  // ── UI ───────────────────────────────────────────────────────────────────
@@ -244,13 +285,18 @@ class ExpoPdfReaderView(
244
285
  override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
245
286
  if (newState == RecyclerView.SCROLL_STATE_IDLE) {
246
287
  val lm = rv.layoutManager as? LinearLayoutManager ?: return
247
- val pos = lm.findFirstCompletelyVisibleItemPosition()
248
- .takeIf { it >= 0 } ?: return
288
+ var pos = lm.findFirstCompletelyVisibleItemPosition()
289
+ if (pos < 0) pos = lm.findFirstVisibleItemPosition()
290
+ if (pos < 0) return
249
291
  if (pos != currentPageIndex) {
250
292
  currentPageIndex = pos
251
293
  // Хуудас солигдоход zoom-г reset хийнэ
252
294
  (pager as? ZoomRecyclerView)?.resetZoom()
253
295
  onPageChange(mapOf("currentPage" to pos, "totalPage" to totalPages))
296
+ } else {
297
+ // scrollToPage() нь currentPageIndex-ийг урьдчилан тохируулдаг тул энд орохгүй —
298
+ // гэхдээ zoom transform + Fabric layout-оос хуудас шинэчлэгдэхгүй үлдэхээс сэргийлнэ
299
+ (pager as? ZoomRecyclerView)?.resetZoom()
254
300
  }
255
301
  }
256
302
  }
@@ -268,8 +314,33 @@ class ExpoPdfReaderView(
268
314
  startLoad(it)
269
315
  }
270
316
  }
271
- // Note: width өөрчлөгдөхөд triggerRender дуудахгүй
272
- // forced layout (forceLayoutScrollView) нь pages-г шууд харагдуулдаг
317
+ // RN: thumbnail panel нээх/хаахад өргөн өөрчлөгдөнө; Fabric child requestLayout сул → reflow шаардлагатай
318
+ if (renderer != null && isLayoutReady && lastHostWidthForPdf > 0 && width > 0) {
319
+ val dw = kotlin.math.abs(width - lastHostWidthForPdf)
320
+ if (dw > 4) {
321
+ post { reflowForHostWidthChange() }
322
+ }
323
+ }
324
+ }
325
+
326
+ /** Хостын өргөн өөрчлөгдсөний дараа PDF-ийг шинэ өргөнөөр дахин тохируулна */
327
+ private fun reflowForHostWidthChange() {
328
+ val w = width.takeIf { it > 0 } ?: return
329
+ if (renderer == null) return
330
+ if (w <= 0) return
331
+ if (lastHostWidthForPdf > 0 && kotlin.math.abs(w - lastHostWidthForPdf) <= 4) return
332
+ lastHostWidthForPdf = w
333
+ currentViewWidth = w
334
+ when (displayMode) {
335
+ "single" -> {
336
+ forcePagerLayout()
337
+ singleAdapter?.notifyDataSetChanged()
338
+ forcePagerLayout()
339
+ val p = currentPageIndex.coerceIn(0, maxOf(0, totalPages - 1))
340
+ post { scrollToPage(p, smooth = false) }
341
+ }
342
+ else -> triggerRender()
343
+ }
273
344
  }
274
345
 
275
346
  override fun onDetachedFromWindow() {
@@ -536,25 +607,46 @@ class ExpoPdfReaderView(
536
607
  // Zoom
537
608
  // ─────────────────────────────────────────────────────────────────────────
538
609
 
539
- private fun applyZoom(newZoom: Float) {
610
+ /**
611
+ * Scroll mode: pivot (0,0) + translation — pinch төв (focus) тогтвортой үлдэхийн тулд translation-ийг
612
+ * scrollX/scrollY-тай нийлүүлэн шинэчилнэ (ZoomRecyclerView-тай ижил математик).
613
+ */
614
+ private fun applyZoom(
615
+ newZoom: Float,
616
+ pinchFocusX: Float = Float.NaN,
617
+ pinchFocusY: Float = Float.NaN
618
+ ) {
619
+ val prevZoom = currentZoom
540
620
  currentZoom = newZoom.coerceIn(minZoom, maxZoom)
541
621
  container.pivotX = 0f
542
622
  container.pivotY = 0f
543
- container.scaleX = currentZoom
544
- container.scaleY = currentZoom
623
+
545
624
  if (displayMode != "single") {
546
625
  if (currentZoom <= 1.02f) {
547
- scrollView.allowVerticalScrollFromTouch = true
548
626
  container.translationX = 0f
549
627
  container.translationY = 0f
550
- } else {
551
- scrollView.allowVerticalScrollFromTouch = false
628
+ } else if (!pinchFocusX.isNaN() && !pinchFocusY.isNaN() && prevZoom > 0.01f) {
629
+ val af = currentZoom / prevZoom
630
+ if (kotlin.math.abs(af - 1f) > 1e-5f) {
631
+ val scrX = scrollView.scrollX.toFloat()
632
+ val scrY = scrollView.scrollY.toFloat()
633
+ val tx = container.translationX
634
+ val ty = container.translationY
635
+ val contentFx = pinchFocusX + scrX
636
+ val contentFy = pinchFocusY + scrY
637
+ container.translationX = contentFx - (contentFx - tx) * af
638
+ container.translationY = contentFy - (contentFy - ty) * af
639
+ }
552
640
  }
553
641
  }
642
+ container.scaleX = currentZoom
643
+ container.scaleY = currentZoom
644
+ if (displayMode != "single" && currentZoom > 1.02f) {
645
+ clampContainerTranslationsInPlace()
646
+ }
554
647
  }
555
648
 
556
- /** Zoom > 1 үед container-ийг translation-оор pan (хуудас хоорондын scroll унтраасан) */
557
- private fun applyContainerPanDelta(dx: Float, dy: Float) {
649
+ private fun clampContainerTranslationsInPlace() {
558
650
  val ch = container.height.toFloat()
559
651
  val cw = container.width.toFloat()
560
652
  if (ch <= 0f || cw <= 0f) return
@@ -562,8 +654,8 @@ class ExpoPdfReaderView(
562
654
  val scaledW = cw * currentZoom
563
655
  val vw = scrollView.width.toFloat()
564
656
  val vh = scrollView.height.toFloat()
565
- var tx = container.translationX + dx
566
- var ty = container.translationY + dy
657
+ var tx = container.translationX
658
+ var ty = container.translationY
567
659
  if (scaledW <= vw) tx = 0f else tx = tx.coerceIn(vw - scaledW, 0f)
568
660
  if (scaledH <= vh) ty = 0f else ty = ty.coerceIn(vh - scaledH, 0f)
569
661
  container.translationX = tx
@@ -662,10 +754,17 @@ class ExpoPdfReaderView(
662
754
 
663
755
  private fun scrollToPage(pageIndex: Int, smooth: Boolean = true) {
664
756
  if (displayMode == "single") {
665
- if (smooth) pager.smoothScrollToPosition(pageIndex)
666
- else pager.scrollToPosition(pageIndex)
757
+ pager.stopScroll()
667
758
  currentPageIndex = pageIndex
668
759
  onPageChange(mapOf("currentPage" to pageIndex, "totalPage" to totalPages))
760
+ // scrollToPosition нь заримдаа layout/bind хойшлуулна (RN Fabric); post + forcePagerLayout
761
+ pager.post {
762
+ if (smooth) pager.smoothScrollToPosition(pageIndex)
763
+ else pager.scrollToPosition(pageIndex)
764
+ forcePagerLayout()
765
+ singleAdapter?.notifyItemChanged(pageIndex)
766
+ (pager as? ZoomRecyclerView)?.resetZoom()
767
+ }
669
768
  return
670
769
  }
671
770
  if (pageEntries.isEmpty()) return
@@ -743,7 +842,6 @@ class ExpoPdfReaderView(
743
842
  container.scaleY = 1f
744
843
  container.translationX = 0f
745
844
  container.translationY = 0f
746
- scrollView.allowVerticalScrollFromTouch = true
747
845
  container.removeAllViews()
748
846
  pageEntries.clear()
749
847
  loadingFooter = null
@@ -771,6 +869,7 @@ class ExpoPdfReaderView(
771
869
  "height" to (this@ExpoPdfReaderView.height / density).toDouble()
772
870
  )
773
871
  )
872
+ lastHostWidthForPdf = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: viewWidth
774
873
  }
775
874
  }
776
875
 
@@ -865,7 +964,7 @@ class ExpoPdfReaderView(
865
964
  private suspend fun renderTwoUpRange(
866
965
  pdf: PdfRenderer, pageCount: Int, viewWidth: Int, startPage: Int, endPage: Int
867
966
  ) {
868
- val halfWidth = viewWidth / 2
967
+ val halfWidth = (viewWidth / 2).coerceAtLeast(1)
869
968
  var i = startPage
870
969
  while (i < endPage) {
871
970
  val leftBmp = renderPageBitmap(pdf, i, halfWidth)
@@ -929,12 +1028,21 @@ class ExpoPdfReaderView(
929
1028
  }
930
1029
  }
931
1030
 
1031
+ private fun resolveViewWidthForRender(): Int {
1032
+ val w = when {
1033
+ currentViewWidth > 0 -> currentViewWidth
1034
+ width > 0 -> width
1035
+ else -> resources.displayMetrics.widthPixels
1036
+ }
1037
+ return w.coerceAtLeast(1)
1038
+ }
1039
+
932
1040
  private suspend fun loadNextChunk() {
933
1041
  val pdf = renderer ?: return
934
1042
  val start = renderedUpTo + 1
935
1043
  if (start >= totalPages) return
936
1044
  val end = minOf(start + PAGE_CHUNK, totalPages)
937
- val viewWidth = currentViewWidth
1045
+ val viewWidth = resolveViewWidthForRender()
938
1046
 
939
1047
  withContext(Dispatchers.Main) { removeLoadingFooter() }
940
1048
 
@@ -997,7 +1105,7 @@ class ExpoPdfReaderView(
997
1105
  val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
998
1106
  val prevEnd = windowStart
999
1107
  if (prevStart >= prevEnd) return
1000
- val viewWidth = currentViewWidth
1108
+ val viewWidth = resolveViewWidthForRender()
1001
1109
 
1002
1110
  data class PageBmp(val bmp: Bitmap, val idx: Int, val pdfW: Float, val pdfH: Float)
1003
1111
  val pages = mutableListOf<PageBmp>()
@@ -1034,8 +1142,8 @@ class ExpoPdfReaderView(
1034
1142
  val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
1035
1143
  val prevEnd = windowStart
1036
1144
  if (prevStart >= prevEnd) return
1037
- val viewWidth = currentViewWidth
1038
- val halfW = viewWidth / 2
1145
+ val viewWidth = resolveViewWidthForRender()
1146
+ val halfW = (viewWidth / 2).coerceAtLeast(1)
1039
1147
 
1040
1148
  data class RowBmps(val leftIdx: Int, val leftBmp: Bitmap, val rightBmp: Bitmap?)
1041
1149
  val rows = mutableListOf<RowBmps>()
@@ -1171,7 +1279,7 @@ class ExpoPdfReaderView(
1171
1279
  */
1172
1280
  private fun jumpToPage(targetPage: Int) {
1173
1281
  val pdf = renderer ?: return
1174
- val viewWidth = currentViewWidth.takeIf { it > 0 } ?: width.takeIf { it > 0 } ?: return
1282
+ val viewWidth = resolveViewWidthForRender()
1175
1283
  renderJob?.cancel()
1176
1284
  renderJob = scope.launch {
1177
1285
  try {
@@ -1290,14 +1398,15 @@ class ExpoPdfReaderView(
1290
1398
 
1291
1399
  private suspend fun renderPageBitmap(pdf: PdfRenderer, index: Int, targetWidth: Int): Bitmap =
1292
1400
  withContext(pdfDispatcher) {
1401
+ val safeWidth = targetWidth.coerceAtLeast(1)
1293
1402
  pdf.openPage(index).use { page ->
1294
1403
  if (index < pagePdfW.size) {
1295
1404
  pagePdfW[index] = page.width
1296
1405
  pagePdfH[index] = page.height
1297
1406
  }
1298
- val s = targetWidth.toFloat() / page.width.toFloat()
1407
+ val s = safeWidth.toFloat() / page.width.toFloat()
1299
1408
  val h = (page.height * s).toInt().coerceAtLeast(1)
1300
- val bmp = Bitmap.createBitmap(targetWidth, h, Bitmap.Config.ARGB_8888)
1409
+ val bmp = Bitmap.createBitmap(safeWidth, h, Bitmap.Config.ARGB_8888)
1301
1410
  bmp.eraseColor(Color.WHITE)
1302
1411
  page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
1303
1412
  bmp
@@ -1307,7 +1416,11 @@ class ExpoPdfReaderView(
1307
1416
  private fun safeCloseRenderer() {
1308
1417
  try { renderer?.close(); fileDescriptor?.close() }
1309
1418
  catch (_: Exception) { }
1310
- finally { renderer = null; fileDescriptor = null }
1419
+ finally {
1420
+ renderer = null
1421
+ fileDescriptor = null
1422
+ lastHostWidthForPdf = 0
1423
+ }
1311
1424
  }
1312
1425
 
1313
1426
  private suspend fun resolveFile(url: String): File = withContext(pdfDispatcher) {
@@ -1384,7 +1497,18 @@ class ExpoPdfReaderView(
1384
1497
 
1385
1498
  holder.loadJob = scope.launch {
1386
1499
  val pdf = renderer ?: return@launch
1387
- val bmp = renderPageBitmap(pdf, position, this@ExpoPdfReaderView.width)
1500
+ val viewWidth = this@ExpoPdfReaderView.width
1501
+ if (viewWidth <= 0) {
1502
+ withContext(Dispatchers.Main) {
1503
+ post {
1504
+ if (holder.boundPage == position && this@ExpoPdfReaderView.width > 0) {
1505
+ notifyItemChanged(position)
1506
+ }
1507
+ }
1508
+ }
1509
+ return@launch
1510
+ }
1511
+ val bmp = renderPageBitmap(pdf, position, viewWidth)
1388
1512
  val pdfW = pagePdfW.getOrElse(position) { 1 }.toFloat()
1389
1513
  val pdfH = pagePdfH.getOrElse(position) { 1 }.toFloat()
1390
1514
  withContext(Dispatchers.Main) {
@@ -1447,6 +1571,14 @@ class ExpoPdfReaderView(
1447
1571
 
1448
1572
  private val zoomDetector = ScaleGestureDetector(context,
1449
1573
  object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
1574
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
1575
+ if (activeTool != null) return false
1576
+ stopScroll()
1577
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, true)
1578
+ parent?.requestDisallowInterceptTouchEvent(true)
1579
+ return true
1580
+ }
1581
+
1450
1582
  override fun onScale(d: ScaleGestureDetector): Boolean {
1451
1583
  val newScale = (currentScale * d.scaleFactor).coerceIn(minZoom, maxZoom)
1452
1584
  val af = newScale / currentScale
@@ -1457,6 +1589,11 @@ class ExpoPdfReaderView(
1457
1589
  clampAndApply()
1458
1590
  return true
1459
1591
  }
1592
+
1593
+ override fun onScaleEnd(detector: ScaleGestureDetector) {
1594
+ parent?.requestDisallowInterceptTouchEvent(false)
1595
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, false)
1596
+ }
1460
1597
  }
1461
1598
  )
1462
1599
 
@@ -1467,8 +1604,14 @@ class ExpoPdfReaderView(
1467
1604
  stopScroll()
1468
1605
  }
1469
1606
  if (ev.pointerCount > 1 || zoomDetector.isInProgress) {
1607
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, true)
1470
1608
  parent?.requestDisallowInterceptTouchEvent(true)
1471
1609
  }
1610
+ if (ev.actionMasked == MotionEvent.ACTION_UP ||
1611
+ ev.actionMasked == MotionEvent.ACTION_CANCEL
1612
+ ) {
1613
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, false)
1614
+ }
1472
1615
  }
1473
1616
  return super.dispatchTouchEvent(ev)
1474
1617
  }
@@ -56,6 +56,8 @@ final class ExpoPdfReaderView: ExpoView {
56
56
  // Zoom range: minZoom=1.0 (fit width), maxZoom=5.0 → pinch-to-zoom дэмжинэ
57
57
  private var minZoom: CGFloat = 1.0
58
58
  private var maxZoom: CGFloat = 5.0
59
+ /// RN thumbnail panel нээх/хаахад bounds өргөн өөрчлөгдөнө — scaleToFit дахин хийх суурь
60
+ private var lastHostWidthForPdf: CGFloat = 0
59
61
 
60
62
  required init(appContext: AppContext? = nil) {
61
63
  super.init(appContext: appContext)
@@ -164,11 +166,31 @@ final class ExpoPdfReaderView: ExpoView {
164
166
  super.layoutSubviews()
165
167
  pdfView.frame = bounds
166
168
 
167
- // Only auto-scale when there is a document.
168
- // Do NOT constantly override user zoom – only when scaleFactor is "uninitialized".
169
- if pdfView.document != nil, pdfView.scaleFactor == 0 {
170
- scaleToFitWidth()
169
+ guard let document = pdfView.document, document.pageCount > 0 else { return }
170
+ let w = bounds.width
171
+ guard w > 1 else { return }
172
+
173
+ // Анх: зөвхөн scaleFactor идэвхгүй үед fit (өмнөх зан)
174
+ if lastHostWidthForPdf <= 0 {
175
+ lastHostWidthForPdf = w
176
+ if pdfView.scaleFactor == 0 {
177
+ scaleToFitWidth()
178
+ }
179
+ return
171
180
  }
181
+
182
+ // Thumbnail / side panel нээх эсвэл хаахад өргөн өөрчлөгдөнө → fit width дахин (жижиг үлдэхээс сэргийлнэ)
183
+ let dw = abs(w - lastHostWidthForPdf)
184
+ guard dw > 4 else { return }
185
+
186
+ let maxIdx = max(0, document.pageCount - 1)
187
+ let idx = min(max(0, lastPageIndex), maxIdx)
188
+ lastHostWidthForPdf = w
189
+ scaleToFitWidth()
190
+ if let page = document.page(at: idx) {
191
+ pdfView.go(to: page)
192
+ }
193
+ requestDisplayUpdate()
172
194
  }
173
195
 
174
196
  /// Custom hitTest:
@@ -198,6 +220,7 @@ final class ExpoPdfReaderView: ExpoView {
198
220
 
199
221
  func load(url: URL) {
200
222
  if pdfView.document?.documentURL == url { return }
223
+ lastHostWidthForPdf = 0
201
224
  if let document = PDFDocument(url: url) {
202
225
  pdfView.document = document
203
226
  // New document → start from first page logically
@@ -209,6 +232,7 @@ final class ExpoPdfReaderView: ExpoView {
209
232
  // Defer scaling until the view has correct bounds
210
233
  DispatchQueue.main.async {
211
234
  self.scaleToFitWidth()
235
+ self.lastHostWidthForPdf = self.bounds.width
212
236
  // Document load хийгдсэний дараа pending annotation-уудыг load хийх
213
237
  if let pending = self.pendingAnnotations {
214
238
  self.pendingAnnotations = nil
@@ -286,6 +310,7 @@ final class ExpoPdfReaderView: ExpoView {
286
310
  self.pdfView.go(to: page)
287
311
  }
288
312
  self.scaleToFitWidth()
313
+ self.lastHostWidthForPdf = self.bounds.width
289
314
  }
290
315
  }
291
316
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@june24/expo-pdf-reader",
3
- "version": "0.1.27",
3
+ "version": "0.1.29",
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",