@june24/expo-pdf-reader 0.1.23 → 0.1.25
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.
- package/README.md +50 -50
- package/android/build.gradle +24 -23
- package/android/src/main/AndroidManifest.xml +2 -2
- package/android/src/main/java/expo/modules/pdfreader/ExpoPdfReaderModule.kt +53 -74
- package/android/src/main/java/expo/modules/pdfreader/ExpoPdfReaderView.kt +1555 -1046
- package/build/ExpoPdfReader.types.d.ts +7 -0
- package/build/ExpoPdfReader.types.d.ts.map +1 -1
- package/build/ExpoPdfReader.types.js.map +1 -1
- package/build/ExpoPdfReaderView.js.map +1 -1
- package/build/index.js.map +1 -1
- package/expo-module.config.json +16 -16
- package/ios/ExpoPdfReader.podspec +27 -27
- package/ios/ExpoPdfReaderModule.swift +68 -68
- package/ios/ExpoPdfReaderView.swift +1072 -1047
- package/package.json +39 -39
- package/src/ExpoPdfReader.types.ts +49 -48
- package/src/ExpoPdfReaderView.tsx +17 -17
- package/src/index.ts +11 -11
- package/tsconfig.json +9 -9
|
@@ -1,1046 +1,1555 @@
|
|
|
1
|
-
package expo.modules.pdfreader
|
|
2
|
-
|
|
3
|
-
import android.
|
|
4
|
-
import android.
|
|
5
|
-
import android.graphics.
|
|
6
|
-
import android.
|
|
7
|
-
import android.
|
|
8
|
-
import android.
|
|
9
|
-
import android.
|
|
10
|
-
import android.
|
|
11
|
-
import android.
|
|
12
|
-
import android.
|
|
13
|
-
import
|
|
14
|
-
import
|
|
15
|
-
import
|
|
16
|
-
import
|
|
17
|
-
import
|
|
18
|
-
import
|
|
19
|
-
import
|
|
20
|
-
import
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
private
|
|
76
|
-
private var
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
private var
|
|
80
|
-
private var
|
|
81
|
-
private var
|
|
82
|
-
private
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
private
|
|
88
|
-
private
|
|
89
|
-
private
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
private var
|
|
95
|
-
private var
|
|
96
|
-
private var
|
|
97
|
-
private var
|
|
98
|
-
private var
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
private var
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
private var
|
|
128
|
-
private var
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
fun
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
val
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
val
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
val
|
|
654
|
-
if (
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
)
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
val
|
|
814
|
-
val
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
(
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
val
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
)
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
frame.
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
val
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1
|
+
package expo.modules.pdfreader
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.*
|
|
5
|
+
import android.graphics.pdf.PdfRenderer
|
|
6
|
+
import android.os.ParcelFileDescriptor
|
|
7
|
+
import android.text.Layout
|
|
8
|
+
import android.text.StaticLayout
|
|
9
|
+
import android.text.TextPaint
|
|
10
|
+
import android.util.Log
|
|
11
|
+
import android.view.*
|
|
12
|
+
import android.widget.*
|
|
13
|
+
import androidx.recyclerview.widget.LinearLayoutManager
|
|
14
|
+
import androidx.recyclerview.widget.PagerSnapHelper
|
|
15
|
+
import androidx.recyclerview.widget.RecyclerView
|
|
16
|
+
import expo.modules.kotlin.AppContext
|
|
17
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
18
|
+
import expo.modules.kotlin.views.ExpoView
|
|
19
|
+
import kotlinx.coroutines.*
|
|
20
|
+
import java.io.File
|
|
21
|
+
import java.io.FileOutputStream
|
|
22
|
+
import java.net.URL
|
|
23
|
+
|
|
24
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
|
+
// Data classes
|
|
26
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
data class AnnPath(val points: MutableList<PointF> = mutableListOf())
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Bounds stored in PDF page coordinate space:
|
|
32
|
+
* origin = bottom-left of the page, y-axis points UP.
|
|
33
|
+
*
|
|
34
|
+
* RectF.left = left edge (PDF x)
|
|
35
|
+
* RectF.top = lower y in PDF space (screen-bottom side, smaller PDF-y value)
|
|
36
|
+
* RectF.right = right edge (PDF x)
|
|
37
|
+
* RectF.bottom = upper y in PDF space (screen-top side, larger PDF-y value)
|
|
38
|
+
*
|
|
39
|
+
* Matches iOS PDFKit: CGRect(x, y=lowerPdfY, width, height).
|
|
40
|
+
*/
|
|
41
|
+
data class PdfAnnotation(
|
|
42
|
+
val type: String, // "pen" | "highlighter" | "line" | "text" | "note"
|
|
43
|
+
val pageIndex: Int,
|
|
44
|
+
var bounds: RectF,
|
|
45
|
+
val paths: MutableList<AnnPath> = mutableListOf(),
|
|
46
|
+
var color: String = "#FF0000",
|
|
47
|
+
var strokeWidth: Float = 2f,
|
|
48
|
+
var contents: String = "",
|
|
49
|
+
var fontSize: Float = 14f,
|
|
50
|
+
var bold: Boolean = false,
|
|
51
|
+
var italic: Boolean = false
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
data class UndoEntry(val annotation: PdfAnnotation)
|
|
55
|
+
|
|
56
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
57
|
+
// Main view
|
|
58
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
class ExpoPdfReaderView(
|
|
61
|
+
context: Context,
|
|
62
|
+
appContext: AppContext
|
|
63
|
+
) : ExpoView(context, appContext) {
|
|
64
|
+
|
|
65
|
+
// ── Expo Events (ViewEventDelegate — Expo Modules-ын зөв механизм) ────────
|
|
66
|
+
val onReady by EventDispatcher()
|
|
67
|
+
val onPageChange by EventDispatcher()
|
|
68
|
+
val onAnnotationChange by EventDispatcher()
|
|
69
|
+
val onUndoRedoStateChange by EventDispatcher()
|
|
70
|
+
val onNotePress by EventDispatcher()
|
|
71
|
+
val onTextPress by EventDispatcher()
|
|
72
|
+
|
|
73
|
+
// ── Coroutines ───────────────────────────────────────────────────────────
|
|
74
|
+
private val pdfDispatcher = Dispatchers.IO.limitedParallelism(1)
|
|
75
|
+
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
|
76
|
+
private var renderJob: Job? = null
|
|
77
|
+
|
|
78
|
+
// ── PDF state ────────────────────────────────────────────────────────────
|
|
79
|
+
private var fileDescriptor: ParcelFileDescriptor? = null
|
|
80
|
+
private var renderer: PdfRenderer? = null
|
|
81
|
+
private var pagePdfW = intArrayOf() // PDF points per page
|
|
82
|
+
private var pagePdfH = intArrayOf()
|
|
83
|
+
var totalPages = 0
|
|
84
|
+
private set
|
|
85
|
+
|
|
86
|
+
// ── Display state ────────────────────────────────────────────────────────
|
|
87
|
+
private var displayMode = "continuous"
|
|
88
|
+
private var pendingUrl: String? = null
|
|
89
|
+
private var isLayoutReady = false
|
|
90
|
+
var currentPageIndex = 0
|
|
91
|
+
private set
|
|
92
|
+
|
|
93
|
+
// ── Lazy / chunked rendering ──────────────────────────────────────────────
|
|
94
|
+
private var renderedUpTo = -1 // last rendered page index (absolute)
|
|
95
|
+
private var windowStart = 0 // pageEntries[0]-н absolute page index
|
|
96
|
+
private var chunkLoading = false // chunk дуусах хүртэл дахин trigger хийхгүй
|
|
97
|
+
private var currentViewWidth = 0 // chunk load-д дахин хэрэглэнэ
|
|
98
|
+
private var loadingFooter: View? = null
|
|
99
|
+
|
|
100
|
+
companion object {
|
|
101
|
+
private const val PAGE_CHUNK = 10 // нэг удаа render хийх хуудасны тоо
|
|
102
|
+
private const val MAX_RENDERED = PAGE_CHUNK * 2 // санах ойд байлгах дээд хязгаар (20 хуудас)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── Annotation state ─────────────────────────────────────────────────────
|
|
106
|
+
val annotationMap = mutableMapOf<Int, MutableList<PdfAnnotation>>()
|
|
107
|
+
var activeTool: String? = null
|
|
108
|
+
var strokeColorVal = "#FF0000"
|
|
109
|
+
var strokeWidthVal = 2f
|
|
110
|
+
var textContentVal = ""
|
|
111
|
+
var textColorVal = "#000000"
|
|
112
|
+
var textFontSizeVal = 14f
|
|
113
|
+
var textBoldVal = false
|
|
114
|
+
var textItalicVal = false
|
|
115
|
+
var noteColorVal = "#FFFF00"
|
|
116
|
+
|
|
117
|
+
val undoStack = ArrayDeque<UndoEntry>()
|
|
118
|
+
val redoStack = ArrayDeque<UndoEntry>()
|
|
119
|
+
private var appliedFingerprint: String? = null
|
|
120
|
+
|
|
121
|
+
// ── Page view entries (continuous/twoUp modes) ───────────────────────────
|
|
122
|
+
// frame: container-аас устгахад шууд reference, bmpHeight: layout болохоос өмнө scroll тооцооны тулд
|
|
123
|
+
data class PageEntry(val frame: FrameLayout, val canvasView: AnnotationCanvasView, val bmpHeight: Int)
|
|
124
|
+
val pageEntries = mutableListOf<PageEntry>()
|
|
125
|
+
|
|
126
|
+
// ── Single-page pager (single mode) ──────────────────────────────────────
|
|
127
|
+
private lateinit var pager: RecyclerView
|
|
128
|
+
private var singleAdapter: SinglePageAdapter? = null
|
|
129
|
+
private val singlePageCanvases = mutableMapOf<Int, AnnotationCanvasView>()
|
|
130
|
+
|
|
131
|
+
// ── Zoom ─────────────────────────────────────────────────────────────────
|
|
132
|
+
private var minZoom = 1.0f
|
|
133
|
+
private var maxZoom = 5.0f
|
|
134
|
+
private var currentZoom = 1.0f
|
|
135
|
+
private val scaleDetector = ScaleGestureDetector(
|
|
136
|
+
context,
|
|
137
|
+
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
138
|
+
override fun onScale(d: ScaleGestureDetector): Boolean {
|
|
139
|
+
if (activeTool != null) return true
|
|
140
|
+
applyZoom((currentZoom * d.scaleFactor).coerceIn(minZoom, maxZoom))
|
|
141
|
+
return true
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
// ── UI ───────────────────────────────────────────────────────────────────
|
|
147
|
+
val scrollView = ScrollView(context).apply {
|
|
148
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
149
|
+
isFillViewport = true
|
|
150
|
+
}
|
|
151
|
+
private val container = LinearLayout(context).apply {
|
|
152
|
+
orientation = LinearLayout.VERTICAL
|
|
153
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)
|
|
154
|
+
setBackgroundColor(Color.parseColor("#E8E8E8"))
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
init {
|
|
158
|
+
setBackgroundColor(Color.parseColor("#E8E8E8"))
|
|
159
|
+
scrollView.addView(container)
|
|
160
|
+
addView(scrollView)
|
|
161
|
+
scrollView.setOnScrollChangeListener { _, _, scrollY, _, _ ->
|
|
162
|
+
detectCurrentPage(scrollY)
|
|
163
|
+
maybeTriggerChunkLoad(scrollY)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Single-page pager: ZoomRecyclerView + PagerSnapHelper (vertical)
|
|
167
|
+
pager = ZoomRecyclerView(context).apply {
|
|
168
|
+
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
|
|
169
|
+
layoutManager = LinearLayoutManager(context, LinearLayoutManager.VERTICAL, false)
|
|
170
|
+
PagerSnapHelper().attachToRecyclerView(this)
|
|
171
|
+
setBackgroundColor(Color.parseColor("#E8E8E8"))
|
|
172
|
+
visibility = View.GONE
|
|
173
|
+
addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
|
174
|
+
override fun onScrollStateChanged(rv: RecyclerView, newState: Int) {
|
|
175
|
+
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
|
|
176
|
+
val lm = rv.layoutManager as? LinearLayoutManager ?: return
|
|
177
|
+
val pos = lm.findFirstCompletelyVisibleItemPosition()
|
|
178
|
+
.takeIf { it >= 0 } ?: return
|
|
179
|
+
if (pos != currentPageIndex) {
|
|
180
|
+
currentPageIndex = pos
|
|
181
|
+
// Хуудас солигдоход zoom-г reset хийнэ
|
|
182
|
+
(pager as? ZoomRecyclerView)?.resetZoom()
|
|
183
|
+
onPageChange(mapOf("currentPage" to pos, "totalPage" to totalPages))
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
}
|
|
189
|
+
addView(pager)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
|
|
193
|
+
super.onLayout(changed, l, t, r, b)
|
|
194
|
+
if (width > 0 && !isLayoutReady) {
|
|
195
|
+
isLayoutReady = true
|
|
196
|
+
pendingUrl?.let {
|
|
197
|
+
pendingUrl = null
|
|
198
|
+
startLoad(it)
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// Note: width өөрчлөгдөхөд triggerRender дуудахгүй —
|
|
202
|
+
// forced layout (forceLayoutScrollView) нь pages-г шууд харагдуулдаг
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
override fun onDetachedFromWindow() {
|
|
206
|
+
super.onDetachedFromWindow()
|
|
207
|
+
renderJob?.cancel()
|
|
208
|
+
scope.cancel()
|
|
209
|
+
safeCloseRenderer()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
213
|
+
scaleDetector.onTouchEvent(event)
|
|
214
|
+
return super.onTouchEvent(event)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
218
|
+
// Props
|
|
219
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
fun setUrl(url: String?) {
|
|
222
|
+
if (url.isNullOrBlank()) return
|
|
223
|
+
pendingUrl = url
|
|
224
|
+
if (isLayoutReady) {
|
|
225
|
+
startLoad(url)
|
|
226
|
+
} else {
|
|
227
|
+
// Layout ирээгүй байж болно — дараагийн frame-д шалгана
|
|
228
|
+
post { tryStartLoad() }
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private fun tryStartLoad() {
|
|
233
|
+
val url = pendingUrl ?: return // onLayout аль хэдийн startLoad дуудсан бол pendingUrl = null → exit
|
|
234
|
+
if (width > 0) {
|
|
235
|
+
isLayoutReady = true
|
|
236
|
+
pendingUrl = null // давхар дуудлагаас сэргийлнэ
|
|
237
|
+
startLoad(url)
|
|
238
|
+
} else {
|
|
239
|
+
scope.launch {
|
|
240
|
+
delay(50)
|
|
241
|
+
tryStartLoad()
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
fun setDisplayMode(mode: String?) {
|
|
247
|
+
val newMode = mode?.lowercase() ?: "continuous"
|
|
248
|
+
if (newMode == displayMode) return
|
|
249
|
+
displayMode = newMode
|
|
250
|
+
if (renderer != null && isLayoutReady) triggerRender()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fun setInitialPage(page: Int) {
|
|
254
|
+
if (page < 0 || page >= totalPages) return
|
|
255
|
+
currentPageIndex = page
|
|
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
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fun setMinZoom(v: Double) { minZoom = v.toFloat() }
|
|
271
|
+
fun setMaxZoom(v: Double) { maxZoom = v.toFloat() }
|
|
272
|
+
|
|
273
|
+
fun setTool(tool: String?) {
|
|
274
|
+
activeTool = tool
|
|
275
|
+
if (displayMode == "single") {
|
|
276
|
+
pager.requestDisallowInterceptTouchEvent(tool != null)
|
|
277
|
+
} else {
|
|
278
|
+
scrollView.requestDisallowInterceptTouchEvent(tool != null)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
fun setStrokeColor(hex: String) { strokeColorVal = hex }
|
|
283
|
+
fun setStrokeWidth(w: Double) { strokeWidthVal = w.toFloat() }
|
|
284
|
+
fun setTextContent(t: String) { textContentVal = t }
|
|
285
|
+
fun setTextColor(hex: String) { textColorVal = hex }
|
|
286
|
+
fun setTextFontSize(size: Double) { textFontSizeVal = size.toFloat() }
|
|
287
|
+
fun setTextBold(v: Boolean) { textBoldVal = v }
|
|
288
|
+
fun setTextItalic(v: Boolean) { textItalicVal = v }
|
|
289
|
+
fun setNoteColor(hex: String) { noteColorVal = hex }
|
|
290
|
+
|
|
291
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
292
|
+
// Commands
|
|
293
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
fun undo() {
|
|
296
|
+
val e = undoStack.removeLastOrNull() ?: return
|
|
297
|
+
redoStack.addLast(e)
|
|
298
|
+
annotationMap[e.annotation.pageIndex]?.remove(e.annotation)
|
|
299
|
+
invalidateCanvas(e.annotation.pageIndex)
|
|
300
|
+
notifyAnnotationChange()
|
|
301
|
+
notifyUndoRedoState()
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
fun redo() {
|
|
305
|
+
val e = redoStack.removeLastOrNull() ?: return
|
|
306
|
+
undoStack.addLast(e)
|
|
307
|
+
annotationMap.getOrPut(e.annotation.pageIndex) { mutableListOf() }.add(e.annotation)
|
|
308
|
+
invalidateCanvas(e.annotation.pageIndex)
|
|
309
|
+
notifyAnnotationChange()
|
|
310
|
+
notifyUndoRedoState()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
fun setAnnotations(data: List<Map<String, Any?>>) {
|
|
314
|
+
val fp = buildFingerprint(data)
|
|
315
|
+
if (fp == appliedFingerprint) return
|
|
316
|
+
appliedFingerprint = fp
|
|
317
|
+
annotationMap.clear()
|
|
318
|
+
|
|
319
|
+
for (item in data) {
|
|
320
|
+
val pageIndex = (item["page"] as? Number)?.toInt() ?: continue
|
|
321
|
+
val typeStr = item["type"] as? String ?: continue
|
|
322
|
+
val b = item["bounds"] as? Map<*, *> ?: continue
|
|
323
|
+
val bX = (b["x"] as? Number)?.toFloat() ?: 0f
|
|
324
|
+
val bY = (b["y"] as? Number)?.toFloat() ?: 0f
|
|
325
|
+
val bW = (b["width"] as? Number)?.toFloat() ?: 0f
|
|
326
|
+
val bH = (b["height"] as? Number)?.toFloat() ?: 0f
|
|
327
|
+
|
|
328
|
+
val bounds = RectF(bX, bY, bX + bW, bY + bH)
|
|
329
|
+
val ann = PdfAnnotation(
|
|
330
|
+
type = typeStr, pageIndex = pageIndex, bounds = bounds,
|
|
331
|
+
color = item["color"] as? String ?: strokeColorVal,
|
|
332
|
+
strokeWidth = (item["strokeWidth"] as? Number)?.toFloat() ?: strokeWidthVal,
|
|
333
|
+
contents = item["contents"] as? String ?: "",
|
|
334
|
+
fontSize = (item["fontSize"] as? Number)?.toFloat() ?: textFontSizeVal,
|
|
335
|
+
bold = item["bold"] as? Boolean ?: false,
|
|
336
|
+
italic = item["italic"] as? Boolean ?: false
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
(item["paths"] as? List<*>)?.forEach { pathData ->
|
|
340
|
+
val pts = (pathData as? List<*>)?.mapNotNull { pt ->
|
|
341
|
+
val m = pt as? Map<*, *> ?: return@mapNotNull null
|
|
342
|
+
PointF(
|
|
343
|
+
(m["x"] as? Number)?.toFloat() ?: 0f,
|
|
344
|
+
(m["y"] as? Number)?.toFloat() ?: 0f
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
if (!pts.isNullOrEmpty()) ann.paths.add(AnnPath(pts.toMutableList()))
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
annotationMap.getOrPut(pageIndex) { mutableListOf() }.add(ann)
|
|
351
|
+
}
|
|
352
|
+
if (displayMode == "single") {
|
|
353
|
+
singlePageCanvases.values.forEach { it.invalidate() }
|
|
354
|
+
} else {
|
|
355
|
+
pageEntries.forEach { it.canvasView.invalidate() }
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
fun updateText(pageIndex: Int, index: Int, contents: String) {
|
|
360
|
+
val anns = annotationMap[pageIndex]?.filter { it.type == "text" } ?: return
|
|
361
|
+
if (index >= anns.size) return
|
|
362
|
+
val ann = anns[index]
|
|
363
|
+
ann.contents = contents.ifBlank { " " }
|
|
364
|
+
autoSizeTextAnnotation(ann)
|
|
365
|
+
invalidateCanvas(pageIndex)
|
|
366
|
+
notifyAnnotationChange()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Recalculate the annotation's bounds so the text fits without clipping.
|
|
371
|
+
* Width is capped at 70% of the view width; height grows with line count.
|
|
372
|
+
*/
|
|
373
|
+
private fun autoSizeTextAnnotation(ann: PdfAnnotation) {
|
|
374
|
+
val text = ann.contents.trim().ifEmpty { return }
|
|
375
|
+
val pageW = pagePdfW.getOrElse(ann.pageIndex) { 0 }
|
|
376
|
+
if (pageW <= 0 || width <= 0) return
|
|
377
|
+
val s = width.toFloat() / pageW.toFloat()
|
|
378
|
+
val tp = android.text.TextPaint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
379
|
+
textSize = ann.fontSize * s
|
|
380
|
+
typeface = when {
|
|
381
|
+
ann.bold && ann.italic -> Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC)
|
|
382
|
+
ann.bold -> Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
383
|
+
ann.italic -> Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
|
|
384
|
+
else -> Typeface.DEFAULT
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
val pad = 16f // screen px total padding (8px each side)
|
|
388
|
+
val maxW = (width * 0.7f - pad).toInt().coerceAtLeast(80)
|
|
389
|
+
val layout = StaticLayout.Builder
|
|
390
|
+
.obtain(text, 0, text.length, tp, maxW)
|
|
391
|
+
.setAlignment(android.text.Layout.Alignment.ALIGN_NORMAL)
|
|
392
|
+
.build()
|
|
393
|
+
val newW = (layout.width + pad) / s
|
|
394
|
+
val newH = (layout.height + pad) / s
|
|
395
|
+
// Keep top-left anchor fixed
|
|
396
|
+
ann.bounds = RectF(ann.bounds.left, ann.bounds.top, ann.bounds.left + newW, ann.bounds.top + newH)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
fun updateNote(pageIndex: Int, index: Int, contents: String) {
|
|
400
|
+
val anns = annotationMap[pageIndex]?.filter { it.type == "note" } ?: return
|
|
401
|
+
if (index >= anns.size) return
|
|
402
|
+
anns[index].contents = contents.ifBlank { " " }
|
|
403
|
+
invalidateCanvas(pageIndex)
|
|
404
|
+
notifyAnnotationChange()
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
408
|
+
// Internal helpers (used by inner AnnotationCanvasView)
|
|
409
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
410
|
+
|
|
411
|
+
fun addAnnotationAndCommit(ann: PdfAnnotation) {
|
|
412
|
+
annotationMap.getOrPut(ann.pageIndex) { mutableListOf() }.add(ann)
|
|
413
|
+
undoStack.addLast(UndoEntry(ann))
|
|
414
|
+
redoStack.clear()
|
|
415
|
+
invalidateCanvas(ann.pageIndex)
|
|
416
|
+
notifyAnnotationChange()
|
|
417
|
+
notifyUndoRedoState()
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
fun invalidateCanvas(pageIndex: Int) {
|
|
421
|
+
if (displayMode == "single") {
|
|
422
|
+
singlePageCanvases[pageIndex]?.invalidate()
|
|
423
|
+
} else {
|
|
424
|
+
// continuous mode: pageEntries[0] = page windowStart → relative index
|
|
425
|
+
pageEntries.getOrNull(pageIndex - windowStart)?.canvasView?.invalidate()
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
fun notifyAnnotationChange() {
|
|
430
|
+
val all = mutableListOf<Map<String, Any>>()
|
|
431
|
+
for ((pageIndex, anns) in annotationMap) {
|
|
432
|
+
for (ann in anns) {
|
|
433
|
+
val m = mutableMapOf<String, Any>(
|
|
434
|
+
"type" to ann.type,
|
|
435
|
+
"page" to pageIndex,
|
|
436
|
+
"bounds" to mapOf(
|
|
437
|
+
"x" to ann.bounds.left.toDouble(),
|
|
438
|
+
"y" to ann.bounds.top.toDouble(),
|
|
439
|
+
"width" to ann.bounds.width().toDouble(),
|
|
440
|
+
"height" to ann.bounds.height().toDouble()
|
|
441
|
+
),
|
|
442
|
+
"color" to ann.color,
|
|
443
|
+
"strokeWidth" to ann.strokeWidth.toDouble(),
|
|
444
|
+
"contents" to ann.contents
|
|
445
|
+
)
|
|
446
|
+
if (ann.type == "text") {
|
|
447
|
+
m["fontSize"] = ann.fontSize.toDouble()
|
|
448
|
+
m["bold"] = ann.bold
|
|
449
|
+
m["italic"] = ann.italic
|
|
450
|
+
}
|
|
451
|
+
if (ann.paths.isNotEmpty()) {
|
|
452
|
+
m["paths"] = ann.paths.map { p ->
|
|
453
|
+
p.points.map { pt ->
|
|
454
|
+
mapOf("x" to pt.x.toDouble(), "y" to pt.y.toDouble())
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
all.add(m)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
onAnnotationChange(mapOf("annotations" to all))
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fun notifyUndoRedoState() {
|
|
465
|
+
onUndoRedoStateChange(
|
|
466
|
+
mapOf("canUndo" to undoStack.isNotEmpty(), "canRedo" to redoStack.isNotEmpty())
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
471
|
+
// Zoom
|
|
472
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
473
|
+
|
|
474
|
+
private fun applyZoom(newZoom: Float) {
|
|
475
|
+
currentZoom = newZoom
|
|
476
|
+
container.pivotX = 0f
|
|
477
|
+
container.pivotY = 0f
|
|
478
|
+
container.scaleX = newZoom
|
|
479
|
+
container.scaleY = newZoom
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
483
|
+
// Scroll / page detection
|
|
484
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
485
|
+
|
|
486
|
+
private fun detectCurrentPage(scrollY: Int) {
|
|
487
|
+
if (pageEntries.isEmpty()) return
|
|
488
|
+
val centerY = scrollY + height / 2
|
|
489
|
+
var accY = 0
|
|
490
|
+
for (i in pageEntries.indices) {
|
|
491
|
+
val h = pageEntries[i].bmpHeight.takeIf { it > 0 } ?: continue
|
|
492
|
+
accY += h + 12
|
|
493
|
+
if (centerY <= accY || i == pageEntries.size - 1) {
|
|
494
|
+
val absolutePage = windowStart + i
|
|
495
|
+
if (currentPageIndex != absolutePage) {
|
|
496
|
+
currentPageIndex = absolutePage
|
|
497
|
+
onPageChange(mapOf("currentPage" to absolutePage, "totalPage" to totalPages))
|
|
498
|
+
}
|
|
499
|
+
break
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
private fun scrollToPage(pageIndex: Int, smooth: Boolean = true) {
|
|
505
|
+
if (displayMode == "single") {
|
|
506
|
+
if (smooth) pager.smoothScrollToPosition(pageIndex)
|
|
507
|
+
else pager.scrollToPosition(pageIndex)
|
|
508
|
+
currentPageIndex = pageIndex
|
|
509
|
+
onPageChange(mapOf("currentPage" to pageIndex, "totalPage" to totalPages))
|
|
510
|
+
return
|
|
511
|
+
}
|
|
512
|
+
if (pageEntries.isEmpty()) return
|
|
513
|
+
// pageEntries[0] нь windowStart-р эхэлдэг тул relative index ашиглана
|
|
514
|
+
val relativeIndex = (pageIndex - windowStart).coerceIn(0, pageEntries.size - 1)
|
|
515
|
+
var y = 0
|
|
516
|
+
for (i in 0 until relativeIndex) {
|
|
517
|
+
y += (pageEntries[i].bmpHeight.takeIf { it > 0 } ?: 0) + 12
|
|
518
|
+
}
|
|
519
|
+
if (smooth) scrollView.smoothScrollTo(0, y) else scrollView.scrollTo(0, y)
|
|
520
|
+
currentPageIndex = pageIndex
|
|
521
|
+
onPageChange(mapOf("currentPage" to pageIndex, "totalPage" to totalPages))
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
525
|
+
// Load & Render
|
|
526
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
private fun startLoad(url: String) {
|
|
529
|
+
Log.d("ExpoPdfReader", "startLoad: $url")
|
|
530
|
+
renderJob?.cancel()
|
|
531
|
+
renderJob = scope.launch {
|
|
532
|
+
try {
|
|
533
|
+
withContext(pdfDispatcher) { safeCloseRenderer() }
|
|
534
|
+
val file = withContext(pdfDispatcher) { resolveFile(url) }
|
|
535
|
+
withContext(pdfDispatcher) {
|
|
536
|
+
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
537
|
+
renderer = PdfRenderer(fileDescriptor!!)
|
|
538
|
+
val r = renderer!!
|
|
539
|
+
totalPages = r.pageCount
|
|
540
|
+
pagePdfW = IntArray(totalPages) { i -> r.openPage(i).use { it.width } }
|
|
541
|
+
pagePdfH = IntArray(totalPages) { i -> r.openPage(i).use { it.height } }
|
|
542
|
+
}
|
|
543
|
+
// annotationMap is intentionally NOT cleared here.
|
|
544
|
+
// setAnnotations() may have already populated it before startLoad() runs
|
|
545
|
+
// (Expo prop ordering: url first, then initialAnnotations).
|
|
546
|
+
// Canvases created in renderDocument() read annotationMap on their first onDraw().
|
|
547
|
+
undoStack.clear(); redoStack.clear()
|
|
548
|
+
appliedFingerprint = null; currentPageIndex = 0
|
|
549
|
+
renderDocument()
|
|
550
|
+
// After canvases are created, re-invalidate all of them so initialAnnotations appear.
|
|
551
|
+
withContext(Dispatchers.Main) {
|
|
552
|
+
if (displayMode == "single") {
|
|
553
|
+
singlePageCanvases.values.forEach { it.invalidate() }
|
|
554
|
+
} else {
|
|
555
|
+
pageEntries.forEach { it.canvasView.invalidate() }
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch (e: CancellationException) { throw e }
|
|
559
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "Load error", e) }
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
private fun triggerRender() {
|
|
564
|
+
renderJob?.cancel()
|
|
565
|
+
renderJob = scope.launch {
|
|
566
|
+
try { renderDocument() }
|
|
567
|
+
catch (e: CancellationException) { throw e }
|
|
568
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "Render error", e) }
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
private suspend fun renderDocument() {
|
|
573
|
+
val pdf = renderer ?: return
|
|
574
|
+
val pageCount = withContext(pdfDispatcher) { pdf.pageCount }
|
|
575
|
+
if (pageCount <= 0) return
|
|
576
|
+
val viewWidth = width.takeIf { it > 0 } ?: resources.displayMetrics.widthPixels
|
|
577
|
+
if (viewWidth <= 0) return
|
|
578
|
+
|
|
579
|
+
// Chunk state reset
|
|
580
|
+
currentViewWidth = viewWidth
|
|
581
|
+
renderedUpTo = -1
|
|
582
|
+
windowStart = 0
|
|
583
|
+
chunkLoading = false
|
|
584
|
+
|
|
585
|
+
withContext(Dispatchers.Main) {
|
|
586
|
+
container.removeAllViews()
|
|
587
|
+
pageEntries.clear()
|
|
588
|
+
loadingFooter = null
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
when (displayMode) {
|
|
592
|
+
"single" -> renderSingleMode()
|
|
593
|
+
"twoup" -> renderAllPages(pdf, pageCount, viewWidth)
|
|
594
|
+
else -> renderFirstChunk(pdf, pageCount, viewWidth) // continuous / twoUpContinuous
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// onReady: Main thread дээр шууд дуудна
|
|
598
|
+
withContext(Dispatchers.Main) {
|
|
599
|
+
val density = resources.displayMetrics.density
|
|
600
|
+
Log.d("ExpoPdfReader", "onReady firing: totalPages=$pageCount")
|
|
601
|
+
onReady(
|
|
602
|
+
mapOf(
|
|
603
|
+
"totalPages" to pageCount,
|
|
604
|
+
"width" to (this@ExpoPdfReaderView.width / density).toDouble(),
|
|
605
|
+
"height" to (this@ExpoPdfReaderView.height / density).toDouble()
|
|
606
|
+
)
|
|
607
|
+
)
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// single mode: RecyclerView + PagerSnapHelper — нэг хуудас бүрэн харагдана
|
|
612
|
+
private suspend fun renderSingleMode() {
|
|
613
|
+
withContext(Dispatchers.Main) {
|
|
614
|
+
singlePageCanvases.clear()
|
|
615
|
+
scrollView.visibility = View.GONE
|
|
616
|
+
pager.visibility = View.VISIBLE
|
|
617
|
+
singleAdapter = SinglePageAdapter()
|
|
618
|
+
pager.adapter = singleAdapter
|
|
619
|
+
pager.scrollToPosition(currentPageIndex)
|
|
620
|
+
// React Native Fabric blocks child requestLayout() calls, so the RecyclerView
|
|
621
|
+
// never binds its items until the next Fabric layout pass (e.g. thumbnail panel
|
|
622
|
+
// opening/closing). Force a manual measure+layout to make items bind immediately.
|
|
623
|
+
forcePagerLayout()
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
private fun forcePagerLayout() {
|
|
628
|
+
val w = width.takeIf { it > 0 } ?: return
|
|
629
|
+
val h = height.takeIf { it > 0 } ?: return
|
|
630
|
+
pager.measure(
|
|
631
|
+
MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
|
|
632
|
+
MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
|
|
633
|
+
)
|
|
634
|
+
pager.layout(0, 0, w, h)
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
// twoup — бүх хуудсыг render хийнэ
|
|
638
|
+
private suspend fun renderAllPages(pdf: PdfRenderer, pageCount: Int, viewWidth: Int) {
|
|
639
|
+
withContext(Dispatchers.Main) {
|
|
640
|
+
scrollView.visibility = View.VISIBLE
|
|
641
|
+
pager.visibility = View.GONE
|
|
642
|
+
}
|
|
643
|
+
renderTwoUpRange(pdf, pageCount, viewWidth, 0, pageCount)
|
|
644
|
+
withContext(Dispatchers.Main) { renderedUpTo = pageCount - 1 }
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// continuous / twoUpContinuous — эхний PAGE_CHUNK хуудсыг render хийнэ
|
|
648
|
+
private suspend fun renderFirstChunk(pdf: PdfRenderer, pageCount: Int, viewWidth: Int) {
|
|
649
|
+
withContext(Dispatchers.Main) {
|
|
650
|
+
scrollView.visibility = View.VISIBLE
|
|
651
|
+
pager.visibility = View.GONE
|
|
652
|
+
}
|
|
653
|
+
val end = minOf(PAGE_CHUNK, pageCount)
|
|
654
|
+
if (displayMode == "twoupcontinuous") {
|
|
655
|
+
renderTwoUpRange(pdf, pageCount, viewWidth, 0, end)
|
|
656
|
+
// Хос тул renderedUpTo-г тэгш болгоно
|
|
657
|
+
withContext(Dispatchers.Main) {
|
|
658
|
+
renderedUpTo = if (end % 2 == 0) end - 1 else end - 2
|
|
659
|
+
if (renderedUpTo < pageCount - 1) showLoadingFooter()
|
|
660
|
+
forceLayoutScrollView()
|
|
661
|
+
}
|
|
662
|
+
} else {
|
|
663
|
+
for (i in 0 until end) {
|
|
664
|
+
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
665
|
+
val idx = i
|
|
666
|
+
withContext(Dispatchers.Main) {
|
|
667
|
+
addPageRow(bmp, idx, pagePdfW.getOrElse(idx) { 1 }.toFloat(),
|
|
668
|
+
pagePdfH.getOrElse(idx) { 1 }.toFloat())
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
withContext(Dispatchers.Main) {
|
|
672
|
+
renderedUpTo = end - 1
|
|
673
|
+
if (end < pageCount) showLoadingFooter()
|
|
674
|
+
forceLayoutScrollView() // pages-г шууд харагдуулна
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// twoUp row-г render хийх helper (start..end page range, pairs)
|
|
680
|
+
private suspend fun renderTwoUpRange(
|
|
681
|
+
pdf: PdfRenderer, pageCount: Int, viewWidth: Int, startPage: Int, endPage: Int
|
|
682
|
+
) {
|
|
683
|
+
val halfWidth = viewWidth / 2
|
|
684
|
+
var i = startPage
|
|
685
|
+
while (i < endPage) {
|
|
686
|
+
val leftBmp = renderPageBitmap(pdf, i, halfWidth)
|
|
687
|
+
val rightBmp = if (i + 1 < minOf(endPage, pageCount))
|
|
688
|
+
renderPageBitmap(pdf, i + 1, halfWidth) else null
|
|
689
|
+
val li = i; val ri = i + 1
|
|
690
|
+
withContext(Dispatchers.Main) {
|
|
691
|
+
// twoUp row-ын өндрийг зүүн хуудасны bitmap-аас авна (хоёр хуудас ижил scale-тай)
|
|
692
|
+
val rowH = leftBmp.height
|
|
693
|
+
val row = LinearLayout(context).apply {
|
|
694
|
+
orientation = LinearLayout.HORIZONTAL
|
|
695
|
+
layoutParams = LinearLayout.LayoutParams(viewWidth, rowH)
|
|
696
|
+
.apply { setMargins(0, 6, 0, 6) }
|
|
697
|
+
}
|
|
698
|
+
row.addView(buildPageFrame(leftBmp, li,
|
|
699
|
+
pagePdfW.getOrElse(li) { 1 }.toFloat(), pagePdfH.getOrElse(li) { 1 }.toFloat(), 1f))
|
|
700
|
+
if (rightBmp != null) {
|
|
701
|
+
row.addView(buildPageFrame(rightBmp, ri,
|
|
702
|
+
pagePdfW.getOrElse(ri) { 1 }.toFloat(), pagePdfH.getOrElse(ri) { 1 }.toFloat(), 1f))
|
|
703
|
+
} else {
|
|
704
|
+
row.addView(View(context).apply { layoutParams = LinearLayout.LayoutParams(0, rowH, 1f) })
|
|
705
|
+
}
|
|
706
|
+
container.addView(row)
|
|
707
|
+
}
|
|
708
|
+
i += 2
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// ── Lazy chunk helpers ────────────────────────────────────────────────────
|
|
713
|
+
|
|
714
|
+
private fun maybeTriggerChunkLoad(scrollY: Int) {
|
|
715
|
+
if (chunkLoading) return
|
|
716
|
+
if (displayMode !in listOf("continuous", "twoupcontinuous")) return
|
|
717
|
+
|
|
718
|
+
val visibleBottom = scrollY + scrollView.height
|
|
719
|
+
|
|
720
|
+
// Доош scroll: дараагийн chunk ачаална
|
|
721
|
+
if (renderedUpTo < totalPages - 1) {
|
|
722
|
+
val forwardTrigger = container.height - scrollView.height * 2
|
|
723
|
+
if (visibleBottom >= forwardTrigger) {
|
|
724
|
+
chunkLoading = true
|
|
725
|
+
scope.launch {
|
|
726
|
+
try { loadNextChunk() }
|
|
727
|
+
catch (e: CancellationException) { throw e }
|
|
728
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "Chunk load error", e) }
|
|
729
|
+
finally { chunkLoading = false }
|
|
730
|
+
}
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Дээш scroll: өмнөх chunk ачаална
|
|
736
|
+
if (windowStart > 0 && scrollY <= scrollView.height * 2) {
|
|
737
|
+
chunkLoading = true
|
|
738
|
+
scope.launch {
|
|
739
|
+
try { loadPrevChunk() }
|
|
740
|
+
catch (e: CancellationException) { throw e }
|
|
741
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "Prev chunk load error", e) }
|
|
742
|
+
finally { chunkLoading = false }
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
private suspend fun loadNextChunk() {
|
|
748
|
+
val pdf = renderer ?: return
|
|
749
|
+
val start = renderedUpTo + 1
|
|
750
|
+
if (start >= totalPages) return
|
|
751
|
+
val end = minOf(start + PAGE_CHUNK, totalPages)
|
|
752
|
+
val viewWidth = currentViewWidth
|
|
753
|
+
|
|
754
|
+
withContext(Dispatchers.Main) { removeLoadingFooter() }
|
|
755
|
+
|
|
756
|
+
if (displayMode == "twoupcontinuous") {
|
|
757
|
+
renderTwoUpRange(pdf, totalPages, viewWidth, start, end)
|
|
758
|
+
withContext(Dispatchers.Main) {
|
|
759
|
+
renderedUpTo = if (end % 2 == 0) end - 1 else end - 2
|
|
760
|
+
if (renderedUpTo < totalPages - 1) showLoadingFooter()
|
|
761
|
+
forceLayoutScrollView()
|
|
762
|
+
}
|
|
763
|
+
} else {
|
|
764
|
+
for (i in start until end) {
|
|
765
|
+
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
766
|
+
val idx = i
|
|
767
|
+
withContext(Dispatchers.Main) {
|
|
768
|
+
addPageRow(bmp, idx, pagePdfW.getOrElse(idx) { 1 }.toFloat(),
|
|
769
|
+
pagePdfH.getOrElse(idx) { 1 }.toFloat())
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
withContext(Dispatchers.Main) {
|
|
773
|
+
renderedUpTo = end - 1
|
|
774
|
+
// Sliding window: MAX_RENDERED-с илүү болвол дээд хэсгийг устга
|
|
775
|
+
val excess = pageEntries.size - MAX_RENDERED
|
|
776
|
+
if (excess > 0) {
|
|
777
|
+
val removed = pruneTopPages(excess)
|
|
778
|
+
scrollView.scrollBy(0, -removed)
|
|
779
|
+
}
|
|
780
|
+
if (end < totalPages) showLoadingFooter()
|
|
781
|
+
forceLayoutScrollView()
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Дээд хэсгийн [count] хуудсыг устгаж, bitmap санах ойг чөлөөлнэ.
|
|
788
|
+
* @return устгагдсан хуудсуудын нийт өндөр (px) — scroll тохируулахад ашиглана
|
|
789
|
+
*/
|
|
790
|
+
private fun pruneTopPages(count: Int): Int {
|
|
791
|
+
var removedHeight = 0
|
|
792
|
+
repeat(count) {
|
|
793
|
+
val entry = pageEntries.removeFirstOrNull() ?: return@repeat
|
|
794
|
+
removedHeight += entry.bmpHeight + 12 // +12 нь margin (дээш 6 + доош 6)
|
|
795
|
+
// Bitmap-г чөлөөлнэ
|
|
796
|
+
(entry.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
|
|
797
|
+
container.removeView(entry.frame)
|
|
798
|
+
}
|
|
799
|
+
windowStart += count
|
|
800
|
+
return removedHeight
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Өмнөх [PAGE_CHUNK] хуудсыг дээр нэмж, доод хэсгийг цэвэрлэнэ.
|
|
805
|
+
*/
|
|
806
|
+
private suspend fun loadPrevChunk() {
|
|
807
|
+
val pdf = renderer ?: return
|
|
808
|
+
val prevStart = maxOf(0, windowStart - PAGE_CHUNK)
|
|
809
|
+
val prevEnd = windowStart
|
|
810
|
+
if (prevStart >= prevEnd) return
|
|
811
|
+
val viewWidth = currentViewWidth
|
|
812
|
+
|
|
813
|
+
data class PageBmp(val bmp: Bitmap, val idx: Int, val pdfW: Float, val pdfH: Float)
|
|
814
|
+
val pages = mutableListOf<PageBmp>()
|
|
815
|
+
for (i in prevStart until prevEnd) {
|
|
816
|
+
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
817
|
+
pages.add(PageBmp(bmp, i,
|
|
818
|
+
pagePdfW.getOrElse(i) { 1 }.toFloat(),
|
|
819
|
+
pagePdfH.getOrElse(i) { 1 }.toFloat()))
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
withContext(Dispatchers.Main) {
|
|
823
|
+
var insertedHeight = 0
|
|
824
|
+
// Reverse order-оор insert хийж эцэст нь зөв дараалал гарна
|
|
825
|
+
// (бүгд index 0-д insert хийгддэг тул)
|
|
826
|
+
for (page in pages.reversed()) {
|
|
827
|
+
prependPageFrame(page.bmp, page.idx, page.pdfW, page.pdfH)
|
|
828
|
+
insertedHeight += page.bmp.height + 12
|
|
829
|
+
}
|
|
830
|
+
windowStart = prevStart
|
|
831
|
+
renderedUpTo = maxOf(renderedUpTo, windowStart + pageEntries.size - 1)
|
|
832
|
+
|
|
833
|
+
// Доод хэсгийг цэвэрлэнэ
|
|
834
|
+
val excess = pageEntries.size - MAX_RENDERED
|
|
835
|
+
if (excess > 0) pruneBottomPages(excess)
|
|
836
|
+
|
|
837
|
+
// Дээр нэмсэн өндрийг scroll-д нэмж харагдах хэсгийг хэвээр үлдээнэ
|
|
838
|
+
scrollView.scrollBy(0, insertedHeight)
|
|
839
|
+
forceLayoutScrollView()
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Дээр (index 0) шинэ хуудас оруулна. windowStart-г дуудагч тал шинэчилнэ.
|
|
845
|
+
*/
|
|
846
|
+
private fun prependPageFrame(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float) {
|
|
847
|
+
val bmpW = bmp.width
|
|
848
|
+
val bmpH = bmp.height
|
|
849
|
+
val frame = FrameLayout(context).apply {
|
|
850
|
+
layoutParams = LinearLayout.LayoutParams(bmpW, bmpH).apply { setMargins(0, 6, 0, 6) }
|
|
851
|
+
}
|
|
852
|
+
val iv = ImageView(context).apply {
|
|
853
|
+
layoutParams = FrameLayout.LayoutParams(bmpW, bmpH)
|
|
854
|
+
scaleType = ImageView.ScaleType.FIT_XY
|
|
855
|
+
setImageBitmap(bmp)
|
|
856
|
+
}
|
|
857
|
+
val canvasView = AnnotationCanvasView(context, pageIdx, pdfW, pdfH).apply {
|
|
858
|
+
layoutParams = FrameLayout.LayoutParams(bmpW, bmpH)
|
|
859
|
+
translationZ = 1f
|
|
860
|
+
}
|
|
861
|
+
frame.addView(iv)
|
|
862
|
+
frame.addView(canvasView)
|
|
863
|
+
container.addView(frame, 0)
|
|
864
|
+
pageEntries.add(0, PageEntry(frame, canvasView, bmpH))
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Доод хэсгийн [count] хуудсыг устгаж bitmap санах ойг чөлөөлнэ.
|
|
869
|
+
* renderedUpTo-г буулгаж дараагийн доош scroll-д re-render хийгдэхийг зөвшөөрнэ.
|
|
870
|
+
*/
|
|
871
|
+
private fun pruneBottomPages(count: Int) {
|
|
872
|
+
repeat(count) {
|
|
873
|
+
val entry = pageEntries.removeLastOrNull() ?: return@repeat
|
|
874
|
+
(entry.frame.getChildAt(0) as? ImageView)?.setImageBitmap(null)
|
|
875
|
+
container.removeView(entry.frame)
|
|
876
|
+
}
|
|
877
|
+
renderedUpTo = windowStart + pageEntries.size - 1
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/**
|
|
881
|
+
* Render хийгдээгүй хуудас руу үсрэх.
|
|
882
|
+
* Бүх одоогийн view-г цэвэрлэж, targetPage-ийн эргэн тойрны хуудсуудыг render хийнэ.
|
|
883
|
+
*/
|
|
884
|
+
private fun jumpToPage(targetPage: Int) {
|
|
885
|
+
val pdf = renderer ?: return
|
|
886
|
+
val viewWidth = currentViewWidth.takeIf { it > 0 } ?: width.takeIf { it > 0 } ?: return
|
|
887
|
+
renderJob?.cancel()
|
|
888
|
+
renderJob = scope.launch {
|
|
889
|
+
try {
|
|
890
|
+
// Шинэ window: targetPage-ийн хагас chunk өмнөөс эхэлнэ
|
|
891
|
+
val newStart = maxOf(0, targetPage - PAGE_CHUNK / 2)
|
|
892
|
+
val newEnd = minOf(totalPages, newStart + MAX_RENDERED)
|
|
893
|
+
|
|
894
|
+
withContext(Dispatchers.Main) {
|
|
895
|
+
container.removeAllViews()
|
|
896
|
+
pageEntries.clear()
|
|
897
|
+
loadingFooter = null
|
|
898
|
+
windowStart = newStart
|
|
899
|
+
renderedUpTo = newStart - 1
|
|
900
|
+
chunkLoading = false
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
for (i in newStart until newEnd) {
|
|
904
|
+
val bmp = renderPageBitmap(pdf, i, viewWidth)
|
|
905
|
+
val idx = i
|
|
906
|
+
withContext(Dispatchers.Main) {
|
|
907
|
+
addPageRow(bmp, idx, pagePdfW.getOrElse(idx) { 1 }.toFloat(),
|
|
908
|
+
pagePdfH.getOrElse(idx) { 1 }.toFloat())
|
|
909
|
+
}
|
|
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
|
+
}
|
|
919
|
+
} catch (e: CancellationException) { throw e }
|
|
920
|
+
catch (e: Exception) { Log.e("ExpoPdfReader", "JumpToPage error", e) }
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
/**
|
|
925
|
+
* React Native Fabric нь child view-н requestLayout()-г таслах тул
|
|
926
|
+
* addView() дараа layout pass автоматаар ажиллахгүй.
|
|
927
|
+
* Энэ функц scrollView-г шууд measure + layout хийж pages-г харагдуулна.
|
|
928
|
+
*/
|
|
929
|
+
private fun forceLayoutScrollView() {
|
|
930
|
+
val w = width.takeIf { it > 0 } ?: return
|
|
931
|
+
val h = height.takeIf { it > 0 } ?: return
|
|
932
|
+
scrollView.measure(
|
|
933
|
+
MeasureSpec.makeMeasureSpec(w, MeasureSpec.EXACTLY),
|
|
934
|
+
MeasureSpec.makeMeasureSpec(h, MeasureSpec.EXACTLY)
|
|
935
|
+
)
|
|
936
|
+
scrollView.layout(0, 0, w, h)
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
private fun showLoadingFooter() {
|
|
940
|
+
if (loadingFooter != null) return
|
|
941
|
+
val footer = ProgressBar(context, null, android.R.attr.progressBarStyleSmall).apply {
|
|
942
|
+
layoutParams = LinearLayout.LayoutParams(
|
|
943
|
+
LinearLayout.LayoutParams.MATCH_PARENT, 120
|
|
944
|
+
).apply { setMargins(0, 16, 0, 16) }
|
|
945
|
+
isIndeterminate = true
|
|
946
|
+
}
|
|
947
|
+
container.addView(footer)
|
|
948
|
+
loadingFooter = footer
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
private fun removeLoadingFooter() {
|
|
952
|
+
loadingFooter?.let { container.removeView(it) }
|
|
953
|
+
loadingFooter = null
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
private fun addPageRow(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float) =
|
|
957
|
+
container.addView(buildPageFrame(bmp, pageIdx, pdfW, pdfH))
|
|
958
|
+
|
|
959
|
+
private fun buildPageFrame(bmp: Bitmap, pageIdx: Int, pdfW: Float, pdfH: Float, weight: Float = 0f): FrameLayout {
|
|
960
|
+
// MATCH_PARENT нь React Native managed view дотор layout pass болохоос өмнө
|
|
961
|
+
// width = 0 авдаг тул explicit pixel dimension ашиглана → шууд харагдана
|
|
962
|
+
val bmpW = bmp.width
|
|
963
|
+
val bmpH = bmp.height
|
|
964
|
+
val frame = FrameLayout(context).apply {
|
|
965
|
+
layoutParams = if (weight > 0f)
|
|
966
|
+
LinearLayout.LayoutParams(0, bmpH, weight)
|
|
967
|
+
else
|
|
968
|
+
LinearLayout.LayoutParams(bmpW, bmpH)
|
|
969
|
+
.apply { setMargins(0, 6, 0, 6) }
|
|
970
|
+
}
|
|
971
|
+
val iv = ImageView(context).apply {
|
|
972
|
+
layoutParams = FrameLayout.LayoutParams(bmpW, bmpH)
|
|
973
|
+
scaleType = ImageView.ScaleType.FIT_XY
|
|
974
|
+
setImageBitmap(bmp)
|
|
975
|
+
}
|
|
976
|
+
val canvasView = AnnotationCanvasView(context, pageIdx, pdfW, pdfH).apply {
|
|
977
|
+
layoutParams = FrameLayout.LayoutParams(bmpW, bmpH)
|
|
978
|
+
translationZ = 1f
|
|
979
|
+
}
|
|
980
|
+
frame.addView(iv)
|
|
981
|
+
frame.addView(canvasView)
|
|
982
|
+
pageEntries.add(PageEntry(frame, canvasView, bmpH))
|
|
983
|
+
return frame
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
private suspend fun renderPageBitmap(pdf: PdfRenderer, index: Int, targetWidth: Int): Bitmap =
|
|
987
|
+
withContext(pdfDispatcher) {
|
|
988
|
+
pdf.openPage(index).use { page ->
|
|
989
|
+
val s = targetWidth.toFloat() / page.width.toFloat()
|
|
990
|
+
val h = (page.height * s).toInt().coerceAtLeast(1)
|
|
991
|
+
val bmp = Bitmap.createBitmap(targetWidth, h, Bitmap.Config.ARGB_8888)
|
|
992
|
+
bmp.eraseColor(Color.WHITE)
|
|
993
|
+
page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
994
|
+
bmp
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
private fun safeCloseRenderer() {
|
|
999
|
+
try { renderer?.close(); fileDescriptor?.close() }
|
|
1000
|
+
catch (_: Exception) { }
|
|
1001
|
+
finally { renderer = null; fileDescriptor = null }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
private suspend fun resolveFile(url: String): File = withContext(pdfDispatcher) {
|
|
1005
|
+
when {
|
|
1006
|
+
url.startsWith("file://") -> File(url.removePrefix("file://"))
|
|
1007
|
+
url.startsWith("/") -> File(url)
|
|
1008
|
+
else -> {
|
|
1009
|
+
val f = File.createTempFile("pdf_", ".pdf", context.cacheDir)
|
|
1010
|
+
URL(url).openStream().use { inp -> FileOutputStream(f).use { inp.copyTo(it) } }
|
|
1011
|
+
f
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
private fun buildFingerprint(data: List<Map<String, Any?>>): String =
|
|
1017
|
+
data.mapNotNull { item ->
|
|
1018
|
+
val p = (item["page"] as? Number)?.toInt() ?: return@mapNotNull null
|
|
1019
|
+
val t = item["type"] as? String ?: return@mapNotNull null
|
|
1020
|
+
val b = item["bounds"] as? Map<*, *> ?: return@mapNotNull null
|
|
1021
|
+
"$p|$t|${b["x"]},${b["y"]},${b["width"]},${b["height"]}"
|
|
1022
|
+
}.sorted().joinToString("||")
|
|
1023
|
+
|
|
1024
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1025
|
+
// SinglePageAdapter — RecyclerView adapter for "single" display mode
|
|
1026
|
+
// • vertical PagerSnapHelper — босоо scroll-оор хуудас солих
|
|
1027
|
+
// • ZoomRecyclerView zoom-г удирдана (adapter энгийн FrameLayout ашиглана)
|
|
1028
|
+
// • lazy bitmap loading
|
|
1029
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1030
|
+
|
|
1031
|
+
inner class SinglePageAdapter : RecyclerView.Adapter<SinglePageAdapter.Holder>() {
|
|
1032
|
+
|
|
1033
|
+
inner class Holder(val frame: FrameLayout, val imageView: ImageView) :
|
|
1034
|
+
RecyclerView.ViewHolder(frame) {
|
|
1035
|
+
var boundPage = -1
|
|
1036
|
+
var loadJob: Job? = null
|
|
1037
|
+
var currentCanvas: AnnotationCanvasView? = null
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
override fun getItemCount(): Int = totalPages
|
|
1041
|
+
|
|
1042
|
+
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
|
1043
|
+
val frame = FrameLayout(context).apply {
|
|
1044
|
+
layoutParams = RecyclerView.LayoutParams(
|
|
1045
|
+
RecyclerView.LayoutParams.MATCH_PARENT,
|
|
1046
|
+
RecyclerView.LayoutParams.MATCH_PARENT
|
|
1047
|
+
)
|
|
1048
|
+
setBackgroundColor(Color.parseColor("#E8E8E8"))
|
|
1049
|
+
}
|
|
1050
|
+
val iv = ImageView(context).apply {
|
|
1051
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
1052
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
1053
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
1054
|
+
android.view.Gravity.CENTER
|
|
1055
|
+
)
|
|
1056
|
+
scaleType = ImageView.ScaleType.FIT_XY
|
|
1057
|
+
}
|
|
1058
|
+
frame.addView(iv)
|
|
1059
|
+
return Holder(frame, iv)
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
override fun onBindViewHolder(holder: Holder, position: Int) {
|
|
1063
|
+
holder.boundPage = position
|
|
1064
|
+
holder.loadJob?.cancel()
|
|
1065
|
+
holder.imageView.setImageBitmap(null)
|
|
1066
|
+
// Recycle хийгдсэн view-н zoom transform-г цэвэрлэнэ
|
|
1067
|
+
holder.frame.scaleX = 1f; holder.frame.scaleY = 1f
|
|
1068
|
+
holder.frame.translationX = 0f; holder.frame.translationY = 0f
|
|
1069
|
+
|
|
1070
|
+
holder.currentCanvas?.let {
|
|
1071
|
+
singlePageCanvases.remove(it.pageIndex)
|
|
1072
|
+
holder.frame.removeView(it)
|
|
1073
|
+
}
|
|
1074
|
+
val pdfW = pagePdfW.getOrElse(position) { 1 }.toFloat()
|
|
1075
|
+
val pdfH = pagePdfH.getOrElse(position) { 1 }.toFloat()
|
|
1076
|
+
val canvas = AnnotationCanvasView(context, position, pdfW, pdfH).apply {
|
|
1077
|
+
layoutParams = FrameLayout.LayoutParams(
|
|
1078
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
1079
|
+
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
1080
|
+
android.view.Gravity.CENTER
|
|
1081
|
+
)
|
|
1082
|
+
translationZ = 1f
|
|
1083
|
+
}
|
|
1084
|
+
holder.frame.addView(canvas)
|
|
1085
|
+
holder.currentCanvas = canvas
|
|
1086
|
+
singlePageCanvases[position] = canvas
|
|
1087
|
+
|
|
1088
|
+
holder.loadJob = scope.launch {
|
|
1089
|
+
val pdf = renderer ?: return@launch
|
|
1090
|
+
val bmp = renderPageBitmap(pdf, position, this@ExpoPdfReaderView.width)
|
|
1091
|
+
withContext(Dispatchers.Main) {
|
|
1092
|
+
if (holder.boundPage == position) {
|
|
1093
|
+
val lp = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
|
|
1094
|
+
holder.imageView.layoutParams = lp
|
|
1095
|
+
holder.imageView.setImageBitmap(bmp)
|
|
1096
|
+
holder.currentCanvas?.layoutParams = FrameLayout.LayoutParams(bmp.width, bmp.height, android.view.Gravity.CENTER)
|
|
1097
|
+
val fw = this@ExpoPdfReaderView.width.takeIf { it > 0 } ?: return@withContext
|
|
1098
|
+
val fh = this@ExpoPdfReaderView.height.takeIf { it > 0 } ?: return@withContext
|
|
1099
|
+
holder.frame.measure(
|
|
1100
|
+
MeasureSpec.makeMeasureSpec(fw, MeasureSpec.EXACTLY),
|
|
1101
|
+
MeasureSpec.makeMeasureSpec(fh, MeasureSpec.EXACTLY)
|
|
1102
|
+
)
|
|
1103
|
+
holder.frame.layout(0, 0, fw, fh)
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
override fun onViewRecycled(holder: Holder) {
|
|
1110
|
+
holder.loadJob?.cancel()
|
|
1111
|
+
holder.currentCanvas?.let {
|
|
1112
|
+
singlePageCanvases.remove(it.pageIndex)
|
|
1113
|
+
holder.frame.removeView(it)
|
|
1114
|
+
}
|
|
1115
|
+
holder.currentCanvas = null
|
|
1116
|
+
holder.imageView.setImageBitmap(null)
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1121
|
+
// ZoomRecyclerView — single mode-д pinch-to-zoom + pan дэмждэг RecyclerView
|
|
1122
|
+
//
|
|
1123
|
+
// Яагаад RecyclerView subclass хийх шаардлагатай вэ:
|
|
1124
|
+
// AnnotationCanvasView ACTION_DOWN-д false буцаана → RecyclerView
|
|
1125
|
+
// gesture-г эзэмшиж авна → дараагийн ACTION_POINTER_DOWN (2-р хуруу)
|
|
1126
|
+
// шууд RecyclerView.onTouchEvent-д очдог.
|
|
1127
|
+
// RecyclerView subclass хийснээр onTouchEvent-д multi-touch-г зохицуулна.
|
|
1128
|
+
//
|
|
1129
|
+
// Zoom тооцоо (pivot=0,0 + translation):
|
|
1130
|
+
// Item view point (px,py) → дэлгэц: (px*scale + tx, py*scale + ty)
|
|
1131
|
+
// Focus (fx,fy) тогтвортой байхад: new_tx = fx - (fx - tx) * scaleFactor
|
|
1132
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1133
|
+
|
|
1134
|
+
inner class ZoomRecyclerView(context: Context) : RecyclerView(context) {
|
|
1135
|
+
private var currentScale = 1f
|
|
1136
|
+
private var tx = 0f
|
|
1137
|
+
private var ty = 0f
|
|
1138
|
+
private var panX = 0f
|
|
1139
|
+
private var panY = 0f
|
|
1140
|
+
|
|
1141
|
+
private val zoomDetector = ScaleGestureDetector(context,
|
|
1142
|
+
object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
1143
|
+
override fun onScale(d: ScaleGestureDetector): Boolean {
|
|
1144
|
+
val newScale = (currentScale * d.scaleFactor).coerceIn(minZoom, maxZoom)
|
|
1145
|
+
val af = newScale / currentScale
|
|
1146
|
+
// Focus point тогтвортой байхын zoom-to-point тооцоо
|
|
1147
|
+
tx = d.focusX - (d.focusX - tx) * af
|
|
1148
|
+
ty = d.focusY - (d.focusY - ty) * af
|
|
1149
|
+
currentScale = newScale
|
|
1150
|
+
clampAndApply()
|
|
1151
|
+
return true
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
|
|
1157
|
+
// Zoom хийгдсэн + tool идэвхгүй → pan хийхийн тулд ACTION_DOWN-г intercept
|
|
1158
|
+
if (currentScale > 1f && activeTool == null && ev.action == MotionEvent.ACTION_DOWN) {
|
|
1159
|
+
return true
|
|
1160
|
+
}
|
|
1161
|
+
return super.onInterceptTouchEvent(ev)
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
|
1165
|
+
zoomDetector.onTouchEvent(ev)
|
|
1166
|
+
|
|
1167
|
+
// 2 хуруу байвал RecyclerView scroll хийхгүйгээр zoom-г л хийнэ
|
|
1168
|
+
if (ev.pointerCount >= 2 || zoomDetector.isInProgress) {
|
|
1169
|
+
return true
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
// Zoom хийгдсэн үед нэг хуруугаар pan (tool идэвхгүй)
|
|
1173
|
+
if (currentScale > 1f && activeTool == null) {
|
|
1174
|
+
when (ev.actionMasked) {
|
|
1175
|
+
MotionEvent.ACTION_DOWN -> { panX = ev.x; panY = ev.y }
|
|
1176
|
+
MotionEvent.ACTION_MOVE -> {
|
|
1177
|
+
tx += ev.x - panX
|
|
1178
|
+
ty += ev.y - panY
|
|
1179
|
+
panX = ev.x; panY = ev.y
|
|
1180
|
+
clampAndApply()
|
|
1181
|
+
}
|
|
1182
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { }
|
|
1183
|
+
}
|
|
1184
|
+
return true
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
return super.onTouchEvent(ev)
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
fun resetZoom() {
|
|
1191
|
+
currentScale = 1f; tx = 0f; ty = 0f
|
|
1192
|
+
applyToCurrentItem()
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private fun clampAndApply() {
|
|
1196
|
+
val w = width.toFloat(); val h = height.toFloat()
|
|
1197
|
+
if (w > 0f && h > 0f) {
|
|
1198
|
+
val sw = w * currentScale; val sh = h * currentScale
|
|
1199
|
+
tx = if (sw > w) tx.coerceIn(w - sw, 0f) else 0f
|
|
1200
|
+
ty = if (sh > h) ty.coerceIn(h - sh, 0f) else 0f
|
|
1201
|
+
}
|
|
1202
|
+
applyToCurrentItem()
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
private fun applyToCurrentItem() {
|
|
1206
|
+
val lm = layoutManager as? LinearLayoutManager ?: return
|
|
1207
|
+
val pos = lm.findFirstVisibleItemPosition().takeIf { it >= 0 } ?: return
|
|
1208
|
+
val itemView = lm.findViewByPosition(pos) ?: return
|
|
1209
|
+
itemView.pivotX = 0f; itemView.pivotY = 0f
|
|
1210
|
+
itemView.scaleX = currentScale; itemView.scaleY = currentScale
|
|
1211
|
+
itemView.translationX = tx; itemView.translationY = ty
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1216
|
+
// AnnotationCanvasView (inner class — full access to outer view state)
|
|
1217
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
1218
|
+
|
|
1219
|
+
inner class AnnotationCanvasView(
|
|
1220
|
+
context: Context,
|
|
1221
|
+
val pageIndex: Int,
|
|
1222
|
+
private val pdfWidth: Float,
|
|
1223
|
+
private val pdfHeight: Float
|
|
1224
|
+
) : View(context) {
|
|
1225
|
+
|
|
1226
|
+
private var drawTool: String? = null
|
|
1227
|
+
private val drawPoints = mutableListOf<PointF>() // PDF coords
|
|
1228
|
+
private var drawStart: PointF? = null
|
|
1229
|
+
|
|
1230
|
+
// ── No-tool touch state (tap → event, long-press → drag) ─────────────
|
|
1231
|
+
private var noToolDown = false
|
|
1232
|
+
private var noToolDownScreen = PointF()
|
|
1233
|
+
private var isDragging = false
|
|
1234
|
+
private var dragAnn: PdfAnnotation? = null
|
|
1235
|
+
private var dragOrigBounds = RectF()
|
|
1236
|
+
private val LP_TIMEOUT = android.view.ViewConfiguration.getLongPressTimeout().toLong()
|
|
1237
|
+
private val TOUCH_SLOP by lazy { android.view.ViewConfiguration.get(context).scaledTouchSlop.toFloat() }
|
|
1238
|
+
private val lpHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
1239
|
+
private val lpRunnable = Runnable {
|
|
1240
|
+
if (dragAnn?.type != "text" && dragAnn?.type != "note") return@Runnable
|
|
1241
|
+
isDragging = true
|
|
1242
|
+
pager.requestDisallowInterceptTouchEvent(true)
|
|
1243
|
+
scrollView.requestDisallowInterceptTouchEvent(true)
|
|
1244
|
+
invalidate()
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Reusable objects — no allocation in onDraw
|
|
1248
|
+
private val inkPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
1249
|
+
style = Paint.Style.STROKE; strokeCap = Paint.Cap.ROUND; strokeJoin = Paint.Join.ROUND
|
|
1250
|
+
}
|
|
1251
|
+
private val bgFill = Paint().apply { style = Paint.Style.FILL }
|
|
1252
|
+
private val bgStroke = Paint().apply {
|
|
1253
|
+
style = Paint.Style.STROKE; strokeWidth = 1.5f; color = Color.DKGRAY
|
|
1254
|
+
}
|
|
1255
|
+
private val textPaint = TextPaint(Paint.ANTI_ALIAS_FLAG)
|
|
1256
|
+
private val drawPath = Path()
|
|
1257
|
+
|
|
1258
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
1259
|
+
|
|
1260
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
1261
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
1262
|
+
// Single-mode canvases start with MATCH_PARENT (width=0) and receive their real
|
|
1263
|
+
// size after RecyclerView lays out. Re-draw so initialAnnotations become visible.
|
|
1264
|
+
if (w > 0 && oldw == 0) invalidate()
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
// ── Coordinate helpers ────────────────────────────────────────────────
|
|
1268
|
+
|
|
1269
|
+
private fun scale() = if (width > 0 && pdfWidth > 0) width.toFloat() / pdfWidth else 1f
|
|
1270
|
+
|
|
1271
|
+
/** View-local screen → PDF. */
|
|
1272
|
+
private fun toPdf(sx: Float, sy: Float): PointF {
|
|
1273
|
+
val s = scale(); return PointF(sx / s, pdfHeight - sy / s)
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/** PDF → view-local screen. */
|
|
1277
|
+
private fun toScreen(px: Float, py: Float): PointF {
|
|
1278
|
+
val s = scale(); return PointF(px * s, (pdfHeight - py) * s)
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/**
|
|
1282
|
+
* PDF RectF → screen RectF.
|
|
1283
|
+
* bounds.top = lower PDF y → screen bottom
|
|
1284
|
+
* bounds.bottom = upper PDF y → screen top
|
|
1285
|
+
*/
|
|
1286
|
+
private fun toScreenRect(b: RectF): RectF {
|
|
1287
|
+
val s = scale()
|
|
1288
|
+
return RectF(b.left * s, (pdfHeight - b.bottom) * s, b.right * s, (pdfHeight - b.top) * s)
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// ── Touch ─────────────────────────────────────────────────────────────
|
|
1292
|
+
|
|
1293
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
1294
|
+
val tool = activeTool
|
|
1295
|
+
if (tool != null) {
|
|
1296
|
+
// Drawing tool active — consume all events
|
|
1297
|
+
scrollView.requestDisallowInterceptTouchEvent(true)
|
|
1298
|
+
pager.requestDisallowInterceptTouchEvent(true)
|
|
1299
|
+
val pdfPt = toPdf(event.x, event.y)
|
|
1300
|
+
when (tool) {
|
|
1301
|
+
"eraser" -> {
|
|
1302
|
+
if (event.action == MotionEvent.ACTION_DOWN || event.action == MotionEvent.ACTION_MOVE)
|
|
1303
|
+
eraseAt(pdfPt)
|
|
1304
|
+
}
|
|
1305
|
+
"text", "note" -> { if (event.action == MotionEvent.ACTION_DOWN) addInstantAnnotation(pdfPt, tool) }
|
|
1306
|
+
else -> handleDraw(event, pdfPt, tool)
|
|
1307
|
+
}
|
|
1308
|
+
return true
|
|
1309
|
+
}
|
|
1310
|
+
return handleNoToolTouch(event)
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
/**
|
|
1314
|
+
* Touch handling when no drawing tool is active.
|
|
1315
|
+
* - Single tap on text/note → fire onTextPress / onNotePress
|
|
1316
|
+
* - Long press on text/note → drag the annotation
|
|
1317
|
+
* The key issue: returning false on ACTION_DOWN means we never receive ACTION_UP.
|
|
1318
|
+
* So we return true only when we detect a tap/drag target on ACTION_DOWN.
|
|
1319
|
+
*/
|
|
1320
|
+
private fun handleNoToolTouch(event: MotionEvent): Boolean {
|
|
1321
|
+
when (event.action) {
|
|
1322
|
+
MotionEvent.ACTION_DOWN -> {
|
|
1323
|
+
val pdfPt = toPdf(event.x, event.y)
|
|
1324
|
+
val ann = tapTargetAt(pdfPt) ?: return false
|
|
1325
|
+
dragAnn = ann
|
|
1326
|
+
dragOrigBounds = RectF(ann.bounds)
|
|
1327
|
+
noToolDownScreen = PointF(event.x, event.y)
|
|
1328
|
+
noToolDown = true
|
|
1329
|
+
lpHandler.postDelayed(lpRunnable, LP_TIMEOUT)
|
|
1330
|
+
return true
|
|
1331
|
+
}
|
|
1332
|
+
MotionEvent.ACTION_MOVE -> {
|
|
1333
|
+
if (!noToolDown) return false
|
|
1334
|
+
val dx = event.x - noToolDownScreen.x
|
|
1335
|
+
val dy = event.y - noToolDownScreen.y
|
|
1336
|
+
if (isDragging) {
|
|
1337
|
+
val ann = dragAnn ?: return true
|
|
1338
|
+
val s = scale()
|
|
1339
|
+
val annW = dragOrigBounds.width()
|
|
1340
|
+
val annH = dragOrigBounds.height()
|
|
1341
|
+
// PDF y is inverted: screen down (dy>0) → PDF y decreases
|
|
1342
|
+
val newLeft = (dragOrigBounds.left + dx / s).coerceIn(0f, pdfWidth - annW)
|
|
1343
|
+
val newBottom = (dragOrigBounds.bottom - dy / s).coerceIn(annH, pdfHeight)
|
|
1344
|
+
ann.bounds = RectF(newLeft, newBottom - annH, newLeft + annW, newBottom)
|
|
1345
|
+
invalidate()
|
|
1346
|
+
return true
|
|
1347
|
+
}
|
|
1348
|
+
// Cancel long-press if finger moved beyond touch slop
|
|
1349
|
+
if (Math.hypot(dx.toDouble(), dy.toDouble()) > TOUCH_SLOP) {
|
|
1350
|
+
lpHandler.removeCallbacks(lpRunnable)
|
|
1351
|
+
noToolDown = false
|
|
1352
|
+
dragAnn = null
|
|
1353
|
+
return false
|
|
1354
|
+
}
|
|
1355
|
+
return true
|
|
1356
|
+
}
|
|
1357
|
+
MotionEvent.ACTION_UP -> {
|
|
1358
|
+
lpHandler.removeCallbacks(lpRunnable)
|
|
1359
|
+
val wasDown = noToolDown
|
|
1360
|
+
val wasDragging = isDragging
|
|
1361
|
+
noToolDown = false; isDragging = false
|
|
1362
|
+
pager.requestDisallowInterceptTouchEvent(false)
|
|
1363
|
+
scrollView.requestDisallowInterceptTouchEvent(false)
|
|
1364
|
+
if (!wasDown) { dragAnn = null; return false }
|
|
1365
|
+
if (wasDragging) {
|
|
1366
|
+
dragAnn = null
|
|
1367
|
+
notifyAnnotationChange()
|
|
1368
|
+
} else {
|
|
1369
|
+
dragAnn = null
|
|
1370
|
+
handleTap(event.x, event.y)
|
|
1371
|
+
}
|
|
1372
|
+
return true
|
|
1373
|
+
}
|
|
1374
|
+
MotionEvent.ACTION_CANCEL -> {
|
|
1375
|
+
lpHandler.removeCallbacks(lpRunnable)
|
|
1376
|
+
if (isDragging) { dragAnn?.bounds = RectF(dragOrigBounds); invalidate() }
|
|
1377
|
+
noToolDown = false; isDragging = false; dragAnn = null
|
|
1378
|
+
pager.requestDisallowInterceptTouchEvent(false)
|
|
1379
|
+
scrollView.requestDisallowInterceptTouchEvent(false)
|
|
1380
|
+
return false
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
return noToolDown
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
/** Returns the first text or note annotation under [pdfPt], or null. */
|
|
1387
|
+
private fun tapTargetAt(pdfPt: PointF): PdfAnnotation? {
|
|
1388
|
+
val pageAnns = annotationMap[pageIndex] ?: return null
|
|
1389
|
+
return pageAnns.firstOrNull { (it.type == "text" || it.type == "note") && it.bounds.contains(pdfPt.x, pdfPt.y) }
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
private fun handleDraw(event: MotionEvent, pdfPt: PointF, tool: String) {
|
|
1393
|
+
when (event.action) {
|
|
1394
|
+
MotionEvent.ACTION_DOWN -> {
|
|
1395
|
+
drawTool = tool; drawPoints.clear()
|
|
1396
|
+
drawStart = PointF(pdfPt.x, pdfPt.y); drawPoints.add(PointF(pdfPt.x, pdfPt.y))
|
|
1397
|
+
invalidate()
|
|
1398
|
+
}
|
|
1399
|
+
MotionEvent.ACTION_MOVE -> {
|
|
1400
|
+
if (tool == "line") {
|
|
1401
|
+
drawPoints.clear(); drawStart?.let { drawPoints.add(PointF(it.x, it.y)) }
|
|
1402
|
+
}
|
|
1403
|
+
drawPoints.add(PointF(pdfPt.x, pdfPt.y)); invalidate()
|
|
1404
|
+
}
|
|
1405
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
1406
|
+
if (drawPoints.size >= 2) commitStroke(tool)
|
|
1407
|
+
drawTool = null; drawPoints.clear(); drawStart = null; invalidate()
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
private fun commitStroke(tool: String) {
|
|
1413
|
+
val pad = strokeWidthVal / 2f
|
|
1414
|
+
val ann = PdfAnnotation(
|
|
1415
|
+
type = tool, pageIndex = pageIndex,
|
|
1416
|
+
bounds = RectF(
|
|
1417
|
+
drawPoints.minOf { it.x } - pad, drawPoints.minOf { it.y } - pad,
|
|
1418
|
+
drawPoints.maxOf { it.x } + pad, drawPoints.maxOf { it.y } + pad
|
|
1419
|
+
),
|
|
1420
|
+
color = strokeColorVal, strokeWidth = strokeWidthVal
|
|
1421
|
+
)
|
|
1422
|
+
ann.paths.add(AnnPath(drawPoints.map { PointF(it.x, it.y) }.toMutableList()))
|
|
1423
|
+
addAnnotationAndCommit(ann)
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
private fun eraseAt(pdfPt: PointF) {
|
|
1427
|
+
val pageAnns = annotationMap[pageIndex] ?: return
|
|
1428
|
+
val r = 10f / scale()
|
|
1429
|
+
val hit = RectF(pdfPt.x - r, pdfPt.y - r, pdfPt.x + r, pdfPt.y + r)
|
|
1430
|
+
val toRemove = pageAnns.filter { RectF.intersects(it.bounds, hit) }
|
|
1431
|
+
if (toRemove.isNotEmpty()) {
|
|
1432
|
+
pageAnns.removeAll(toRemove.toSet())
|
|
1433
|
+
toRemove.forEach { ann -> undoStack.removeAll { it.annotation === ann } }
|
|
1434
|
+
invalidate(); notifyAnnotationChange(); notifyUndoRedoState()
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
private fun addInstantAnnotation(pdfPt: PointF, tool: String) {
|
|
1439
|
+
val s = scale()
|
|
1440
|
+
val ann = if (tool == "note") {
|
|
1441
|
+
PdfAnnotation("note", pageIndex,
|
|
1442
|
+
RectF(pdfPt.x, pdfPt.y, pdfPt.x + 32f / s, pdfPt.y + 32f / s),
|
|
1443
|
+
color = noteColorVal, contents = textContentVal.ifBlank { " " })
|
|
1444
|
+
} else {
|
|
1445
|
+
val w = 180f / s; val h = 40f / s
|
|
1446
|
+
PdfAnnotation("text", pageIndex,
|
|
1447
|
+
RectF(pdfPt.x, pdfPt.y - h, pdfPt.x + w, pdfPt.y),
|
|
1448
|
+
color = textColorVal, contents = textContentVal.ifBlank { " " },
|
|
1449
|
+
fontSize = textFontSizeVal, bold = textBoldVal, italic = textItalicVal)
|
|
1450
|
+
}
|
|
1451
|
+
addAnnotationAndCommit(ann)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
private fun handleTap(screenX: Float, screenY: Float) {
|
|
1455
|
+
val pdfPt = toPdf(screenX, screenY)
|
|
1456
|
+
val pageAnns = annotationMap[pageIndex] ?: return
|
|
1457
|
+
|
|
1458
|
+
pageAnns.filter { it.type == "text" }.forEachIndexed { idx, ann ->
|
|
1459
|
+
if (ann.bounds.contains(pdfPt.x, pdfPt.y)) {
|
|
1460
|
+
onTextPress(mapOf("page" to pageIndex, "index" to idx,
|
|
1461
|
+
"bounds" to boundsMap(ann), "contents" to ann.contents)); return
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
pageAnns.filter { it.type == "note" }.forEachIndexed { idx, ann ->
|
|
1465
|
+
if (ann.bounds.contains(pdfPt.x, pdfPt.y)) {
|
|
1466
|
+
onNotePress(mapOf("page" to pageIndex, "index" to idx,
|
|
1467
|
+
"bounds" to boundsMap(ann), "contents" to ann.contents)); return
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
private fun boundsMap(ann: PdfAnnotation) = mapOf(
|
|
1473
|
+
"x" to ann.bounds.left.toDouble(), "y" to ann.bounds.top.toDouble(),
|
|
1474
|
+
"width" to ann.bounds.width().toDouble(), "height" to ann.bounds.height().toDouble()
|
|
1475
|
+
)
|
|
1476
|
+
|
|
1477
|
+
// ── Draw ──────────────────────────────────────────────────────────────
|
|
1478
|
+
|
|
1479
|
+
override fun onDraw(canvas: Canvas) {
|
|
1480
|
+
for (ann in annotationMap[pageIndex] ?: emptyList()) drawAnn(canvas, ann)
|
|
1481
|
+
drawTool?.let { drawActiveStroke(canvas, it) }
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
private fun drawAnn(canvas: Canvas, ann: PdfAnnotation) = when (ann.type) {
|
|
1485
|
+
"pen", "highlighter", "line" -> drawInk(canvas, ann)
|
|
1486
|
+
"text" -> drawTextAnn(canvas, ann)
|
|
1487
|
+
"note" -> drawNoteAnn(canvas, ann)
|
|
1488
|
+
else -> Unit
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
private fun drawInk(canvas: Canvas, ann: PdfAnnotation) {
|
|
1492
|
+
val s = scale(); val base = safeColor(ann.color)
|
|
1493
|
+
inkPaint.strokeWidth = ann.strokeWidth * s
|
|
1494
|
+
inkPaint.color = if (ann.type == "highlighter")
|
|
1495
|
+
Color.argb(102, Color.red(base), Color.green(base), Color.blue(base)) else base
|
|
1496
|
+
for (ap in ann.paths) {
|
|
1497
|
+
if (ap.points.isEmpty()) continue
|
|
1498
|
+
drawPath.reset()
|
|
1499
|
+
ap.points.forEachIndexed { i, pt ->
|
|
1500
|
+
val sp = toScreen(pt.x, pt.y)
|
|
1501
|
+
if (i == 0) drawPath.moveTo(sp.x, sp.y) else drawPath.lineTo(sp.x, sp.y)
|
|
1502
|
+
}
|
|
1503
|
+
canvas.drawPath(drawPath, inkPaint)
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
private fun drawTextAnn(canvas: Canvas, ann: PdfAnnotation) {
|
|
1508
|
+
val text = ann.contents.trim().ifEmpty { return }
|
|
1509
|
+
val rect = toScreenRect(ann.bounds)
|
|
1510
|
+
val s = scale()
|
|
1511
|
+
textPaint.textSize = ann.fontSize * s
|
|
1512
|
+
textPaint.color = safeColor(ann.color)
|
|
1513
|
+
textPaint.typeface = when {
|
|
1514
|
+
ann.bold && ann.italic -> Typeface.create(Typeface.DEFAULT, Typeface.BOLD_ITALIC)
|
|
1515
|
+
ann.bold -> Typeface.create(Typeface.DEFAULT, Typeface.BOLD)
|
|
1516
|
+
ann.italic -> Typeface.create(Typeface.DEFAULT, Typeface.ITALIC)
|
|
1517
|
+
else -> Typeface.DEFAULT
|
|
1518
|
+
}
|
|
1519
|
+
// No background — fully transparent
|
|
1520
|
+
val maxW = (rect.width() - 8f).toInt().coerceAtLeast(1)
|
|
1521
|
+
canvas.save()
|
|
1522
|
+
canvas.translate(rect.left + 4f, rect.top + 4f)
|
|
1523
|
+
StaticLayout.Builder.obtain(text, 0, text.length, textPaint, maxW)
|
|
1524
|
+
.setAlignment(Layout.Alignment.ALIGN_NORMAL).build().draw(canvas)
|
|
1525
|
+
canvas.restore()
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
private fun drawNoteAnn(canvas: Canvas, ann: PdfAnnotation) {
|
|
1529
|
+
val rect = toScreenRect(ann.bounds)
|
|
1530
|
+
bgFill.color = try { Color.parseColor(ann.color) } catch (_: Exception) { Color.YELLOW }
|
|
1531
|
+
canvas.drawRect(rect, bgFill); canvas.drawRect(rect, bgStroke)
|
|
1532
|
+
textPaint.textSize = rect.height() * 0.55f; textPaint.color = Color.DKGRAY
|
|
1533
|
+
textPaint.textAlign = Paint.Align.CENTER; textPaint.typeface = Typeface.DEFAULT
|
|
1534
|
+
canvas.drawText("✎", rect.centerX(), rect.centerY() + textPaint.textSize * 0.35f, textPaint)
|
|
1535
|
+
textPaint.textAlign = Paint.Align.LEFT
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
private fun drawActiveStroke(canvas: Canvas, tool: String) {
|
|
1539
|
+
if (drawPoints.isEmpty()) return
|
|
1540
|
+
val s = scale(); val base = safeColor(strokeColorVal)
|
|
1541
|
+
inkPaint.strokeWidth = strokeWidthVal * s
|
|
1542
|
+
inkPaint.color = if (tool == "highlighter")
|
|
1543
|
+
Color.argb(102, Color.red(base), Color.green(base), Color.blue(base)) else base
|
|
1544
|
+
drawPath.reset()
|
|
1545
|
+
drawPoints.forEachIndexed { i, pt ->
|
|
1546
|
+
val sp = toScreen(pt.x, pt.y)
|
|
1547
|
+
if (i == 0) drawPath.moveTo(sp.x, sp.y) else drawPath.lineTo(sp.x, sp.y)
|
|
1548
|
+
}
|
|
1549
|
+
canvas.drawPath(drawPath, inkPaint)
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
private fun safeColor(hex: String): Int =
|
|
1553
|
+
try { Color.parseColor(hex) } catch (_: Exception) { Color.RED }
|
|
1554
|
+
}
|
|
1555
|
+
}
|