@june24/expo-pdf-reader 0.1.27 → 0.1.28

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,41 +135,51 @@ 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-ийг [dispatchTouchEvent]-ийн эхэнд дуудаж
149
+ * 2 хурууны POINTER_DOWN canvas-д баригдахаас өмнө хүлээнэ. Zoom хийсэн ч босоо scroll хэвээр (зөвхөн pinch үед scroll intercept унтрана).
139
150
  */
140
151
  private inner class ZoomableScrollView(ctx: Context) : ScrollView(ctx) {
141
- /** ScrollView-д [isScrollEnabled] байхгүй тул: false үед босоо scroll (super) ажиллахгүй. */
142
- var allowVerticalScrollFromTouch = true
143
152
 
144
153
  private val pinch = ScaleGestureDetector(
145
154
  ctx,
146
155
  object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
147
156
  override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
148
157
  if (activeTool != null) return false
158
+ stopNestedScroll()
159
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, true)
149
160
  parent?.requestDisallowInterceptTouchEvent(true)
150
161
  return true
151
162
  }
152
163
 
153
164
  override fun onScale(detector: ScaleGestureDetector): Boolean {
154
165
  if (activeTool != null) return false
155
- applyZoom((currentZoom * detector.scaleFactor).coerceIn(minZoom, maxZoom))
166
+ applyZoom(
167
+ (currentZoom * detector.scaleFactor).coerceIn(minZoom, maxZoom),
168
+ detector.focusX,
169
+ detector.focusY
170
+ )
156
171
  return true
157
172
  }
158
173
 
159
174
  override fun onScaleEnd(detector: ScaleGestureDetector) {
160
175
  parent?.requestDisallowInterceptTouchEvent(false)
176
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, false)
161
177
  }
162
178
  }
163
179
  )
164
- private var panLastX = 0f
165
- private var panLastY = 0f
166
180
 
167
181
  override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
168
182
  if (activeTool != null) return super.onInterceptTouchEvent(ev)
169
- // 2+ хуруу эсвэл pinch эхэлсэн үед ScrollView босоо scroll intercept хийхгүй (scroll + zoom өрсөлдөнө)
170
- if (!allowVerticalScrollFromTouch) return false
171
183
  if (ev.pointerCount > 1 || pinch.isInProgress) return false
172
184
  return super.onInterceptTouchEvent(ev)
173
185
  }
@@ -179,8 +191,14 @@ class ExpoPdfReaderView(
179
191
  stopNestedScroll()
180
192
  }
181
193
  if (pinch.isInProgress || ev.pointerCount > 1) {
194
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, true)
182
195
  parent?.requestDisallowInterceptTouchEvent(true)
183
196
  }
197
+ if (ev.actionMasked == MotionEvent.ACTION_UP ||
198
+ ev.actionMasked == MotionEvent.ACTION_CANCEL
199
+ ) {
200
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomableScrollView, false)
201
+ }
184
202
  }
185
203
  return super.dispatchTouchEvent(ev)
186
204
  }
@@ -190,23 +208,6 @@ class ExpoPdfReaderView(
190
208
  if (pinch.isInProgress || ev.pointerCount > 1) {
191
209
  return true
192
210
  }
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
211
  return super.onTouchEvent(ev)
211
212
  }
212
213
  }
@@ -244,13 +245,18 @@ class ExpoPdfReaderView(
244
245
  override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
245
246
  if (newState == RecyclerView.SCROLL_STATE_IDLE) {
246
247
  val lm = rv.layoutManager as? LinearLayoutManager ?: return
247
- val pos = lm.findFirstCompletelyVisibleItemPosition()
248
- .takeIf { it >= 0 } ?: return
248
+ var pos = lm.findFirstCompletelyVisibleItemPosition()
249
+ if (pos < 0) pos = lm.findFirstVisibleItemPosition()
250
+ if (pos < 0) return
249
251
  if (pos != currentPageIndex) {
250
252
  currentPageIndex = pos
251
253
  // Хуудас солигдоход zoom-г reset хийнэ
252
254
  (pager as? ZoomRecyclerView)?.resetZoom()
253
255
  onPageChange(mapOf("currentPage" to pos, "totalPage" to totalPages))
256
+ } else {
257
+ // scrollToPage() нь currentPageIndex-ийг урьдчилан тохируулдаг тул энд орохгүй —
258
+ // гэхдээ zoom transform + Fabric layout-оос хуудас шинэчлэгдэхгүй үлдэхээс сэргийлнэ
259
+ (pager as? ZoomRecyclerView)?.resetZoom()
254
260
  }
255
261
  }
256
262
  }
@@ -268,8 +274,32 @@ class ExpoPdfReaderView(
268
274
  startLoad(it)
269
275
  }
270
276
  }
271
- // Note: width өөрчлөгдөхөд triggerRender дуудахгүй
272
- // forced layout (forceLayoutScrollView) нь pages-г шууд харагдуулдаг
277
+ // RN: thumbnail panel нээх/хаахад өргөн өөрчлөгдөнө; Fabric child requestLayout сул → reflow шаардлагатай
278
+ if (renderer != null && isLayoutReady && lastHostWidthForPdf > 0 && width > 0) {
279
+ val dw = kotlin.math.abs(width - lastHostWidthForPdf)
280
+ if (dw > 4) {
281
+ post { reflowForHostWidthChange() }
282
+ }
283
+ }
284
+ }
285
+
286
+ /** Хостын өргөн өөрчлөгдсөний дараа PDF-ийг шинэ өргөнөөр дахин тохируулна */
287
+ private fun reflowForHostWidthChange() {
288
+ val w = width.takeIf { it > 0 } ?: return
289
+ if (renderer == null) return
290
+ if (lastHostWidthForPdf > 0 && kotlin.math.abs(w - lastHostWidthForPdf) <= 4) return
291
+ lastHostWidthForPdf = w
292
+ currentViewWidth = w
293
+ when (displayMode) {
294
+ "single" -> {
295
+ forcePagerLayout()
296
+ singleAdapter?.notifyDataSetChanged()
297
+ forcePagerLayout()
298
+ val p = currentPageIndex.coerceIn(0, maxOf(0, totalPages - 1))
299
+ post { scrollToPage(p, smooth = false) }
300
+ }
301
+ else -> triggerRender()
302
+ }
273
303
  }
274
304
 
275
305
  override fun onDetachedFromWindow() {
@@ -536,25 +566,46 @@ class ExpoPdfReaderView(
536
566
  // Zoom
537
567
  // ─────────────────────────────────────────────────────────────────────────
538
568
 
539
- private fun applyZoom(newZoom: Float) {
569
+ /**
570
+ * Scroll mode: pivot (0,0) + translation — pinch төв (focus) тогтвортой үлдэхийн тулд translation-ийг
571
+ * scrollX/scrollY-тай нийлүүлэн шинэчилнэ (ZoomRecyclerView-тай ижил математик).
572
+ */
573
+ private fun applyZoom(
574
+ newZoom: Float,
575
+ pinchFocusX: Float = Float.NaN,
576
+ pinchFocusY: Float = Float.NaN
577
+ ) {
578
+ val prevZoom = currentZoom
540
579
  currentZoom = newZoom.coerceIn(minZoom, maxZoom)
541
580
  container.pivotX = 0f
542
581
  container.pivotY = 0f
543
- container.scaleX = currentZoom
544
- container.scaleY = currentZoom
582
+
545
583
  if (displayMode != "single") {
546
584
  if (currentZoom <= 1.02f) {
547
- scrollView.allowVerticalScrollFromTouch = true
548
585
  container.translationX = 0f
549
586
  container.translationY = 0f
550
- } else {
551
- scrollView.allowVerticalScrollFromTouch = false
587
+ } else if (!pinchFocusX.isNaN() && !pinchFocusY.isNaN() && prevZoom > 0.01f) {
588
+ val af = currentZoom / prevZoom
589
+ if (kotlin.math.abs(af - 1f) > 1e-5f) {
590
+ val scrX = scrollView.scrollX.toFloat()
591
+ val scrY = scrollView.scrollY.toFloat()
592
+ val tx = container.translationX
593
+ val ty = container.translationY
594
+ val contentFx = pinchFocusX + scrX
595
+ val contentFy = pinchFocusY + scrY
596
+ container.translationX = contentFx - (contentFx - tx) * af
597
+ container.translationY = contentFy - (contentFy - ty) * af
598
+ }
552
599
  }
553
600
  }
601
+ container.scaleX = currentZoom
602
+ container.scaleY = currentZoom
603
+ if (displayMode != "single" && currentZoom > 1.02f) {
604
+ clampContainerTranslationsInPlace()
605
+ }
554
606
  }
555
607
 
556
- /** Zoom > 1 үед container-ийг translation-оор pan (хуудас хоорондын scroll унтраасан) */
557
- private fun applyContainerPanDelta(dx: Float, dy: Float) {
608
+ private fun clampContainerTranslationsInPlace() {
558
609
  val ch = container.height.toFloat()
559
610
  val cw = container.width.toFloat()
560
611
  if (ch <= 0f || cw <= 0f) return
@@ -562,8 +613,8 @@ class ExpoPdfReaderView(
562
613
  val scaledW = cw * currentZoom
563
614
  val vw = scrollView.width.toFloat()
564
615
  val vh = scrollView.height.toFloat()
565
- var tx = container.translationX + dx
566
- var ty = container.translationY + dy
616
+ var tx = container.translationX
617
+ var ty = container.translationY
567
618
  if (scaledW <= vw) tx = 0f else tx = tx.coerceIn(vw - scaledW, 0f)
568
619
  if (scaledH <= vh) ty = 0f else ty = ty.coerceIn(vh - scaledH, 0f)
569
620
  container.translationX = tx
@@ -662,10 +713,17 @@ class ExpoPdfReaderView(
662
713
 
663
714
  private fun scrollToPage(pageIndex: Int, smooth: Boolean = true) {
664
715
  if (displayMode == "single") {
665
- if (smooth) pager.smoothScrollToPosition(pageIndex)
666
- else pager.scrollToPosition(pageIndex)
716
+ pager.stopScroll()
667
717
  currentPageIndex = pageIndex
668
718
  onPageChange(mapOf("currentPage" to pageIndex, "totalPage" to totalPages))
719
+ // scrollToPosition нь заримдаа layout/bind хойшлуулна (RN Fabric); post + forcePagerLayout
720
+ pager.post {
721
+ if (smooth) pager.smoothScrollToPosition(pageIndex)
722
+ else pager.scrollToPosition(pageIndex)
723
+ forcePagerLayout()
724
+ singleAdapter?.notifyItemChanged(pageIndex)
725
+ (pager as? ZoomRecyclerView)?.resetZoom()
726
+ }
669
727
  return
670
728
  }
671
729
  if (pageEntries.isEmpty()) return
@@ -743,7 +801,6 @@ class ExpoPdfReaderView(
743
801
  container.scaleY = 1f
744
802
  container.translationX = 0f
745
803
  container.translationY = 0f
746
- scrollView.allowVerticalScrollFromTouch = true
747
804
  container.removeAllViews()
748
805
  pageEntries.clear()
749
806
  loadingFooter = null
@@ -771,6 +828,7 @@ class ExpoPdfReaderView(
771
828
  "height" to (this@ExpoPdfReaderView.height / density).toDouble()
772
829
  )
773
830
  )
831
+ lastHostWidthForPdf = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: viewWidth
774
832
  }
775
833
  }
776
834
 
@@ -1307,7 +1365,11 @@ class ExpoPdfReaderView(
1307
1365
  private fun safeCloseRenderer() {
1308
1366
  try { renderer?.close(); fileDescriptor?.close() }
1309
1367
  catch (_: Exception) { }
1310
- finally { renderer = null; fileDescriptor = null }
1368
+ finally {
1369
+ renderer = null
1370
+ fileDescriptor = null
1371
+ lastHostWidthForPdf = 0
1372
+ }
1311
1373
  }
1312
1374
 
1313
1375
  private suspend fun resolveFile(url: String): File = withContext(pdfDispatcher) {
@@ -1447,6 +1509,14 @@ class ExpoPdfReaderView(
1447
1509
 
1448
1510
  private val zoomDetector = ScaleGestureDetector(context,
1449
1511
  object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
1512
+ override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
1513
+ if (activeTool != null) return false
1514
+ stopScroll()
1515
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, true)
1516
+ parent?.requestDisallowInterceptTouchEvent(true)
1517
+ return true
1518
+ }
1519
+
1450
1520
  override fun onScale(d: ScaleGestureDetector): Boolean {
1451
1521
  val newScale = (currentScale * d.scaleFactor).coerceIn(minZoom, maxZoom)
1452
1522
  val af = newScale / currentScale
@@ -1457,6 +1527,11 @@ class ExpoPdfReaderView(
1457
1527
  clampAndApply()
1458
1528
  return true
1459
1529
  }
1530
+
1531
+ override fun onScaleEnd(detector: ScaleGestureDetector) {
1532
+ parent?.requestDisallowInterceptTouchEvent(false)
1533
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, false)
1534
+ }
1460
1535
  }
1461
1536
  )
1462
1537
 
@@ -1467,8 +1542,14 @@ class ExpoPdfReaderView(
1467
1542
  stopScroll()
1468
1543
  }
1469
1544
  if (ev.pointerCount > 1 || zoomDetector.isInProgress) {
1545
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, true)
1470
1546
  parent?.requestDisallowInterceptTouchEvent(true)
1471
1547
  }
1548
+ if (ev.actionMasked == MotionEvent.ACTION_UP ||
1549
+ ev.actionMasked == MotionEvent.ACTION_CANCEL
1550
+ ) {
1551
+ this@ExpoPdfReaderView.propagateDisallowInterceptFrom(this@ZoomRecyclerView, false)
1552
+ }
1472
1553
  }
1473
1554
  return super.dispatchTouchEvent(ev)
1474
1555
  }
@@ -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.28",
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",