@june24/expo-pdf-reader 0.1.0
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 -0
- package/android/build.gradle +23 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/pdfreader/ExpoPdfReaderModule.kt +202 -0
- package/android/src/main/java/expo/modules/pdfreader/ExpoPdfReaderView.kt +934 -0
- package/build/ExpoPdfReader.types.d.ts +85 -0
- package/build/ExpoPdfReader.types.d.ts.map +1 -0
- package/build/ExpoPdfReader.types.js +2 -0
- package/build/ExpoPdfReader.types.js.map +1 -0
- package/build/ExpoPdfReaderView.d.ts +28 -0
- package/build/ExpoPdfReaderView.d.ts.map +1 -0
- package/build/ExpoPdfReaderView.js +107 -0
- package/build/ExpoPdfReaderView.js.map +1 -0
- package/build/index.d.ts +4 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +3 -0
- package/build/index.js.map +1 -0
- package/expo-module.config.json +16 -0
- package/ios/ExpoPdfReader.podspec +27 -0
- package/ios/ExpoPdfReaderModule.swift +170 -0
- package/ios/ExpoPdfReaderView.swift +675 -0
- package/package.json +37 -0
- package/src/ExpoPdfReader.types.ts +99 -0
- package/src/ExpoPdfReaderView.tsx +137 -0
- package/src/index.ts +4 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,934 @@
|
|
|
1
|
+
package expo.modules.pdfreader
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.*
|
|
5
|
+
import android.graphics.pdf.PdfDocument
|
|
6
|
+
import android.graphics.pdf.PdfRenderer
|
|
7
|
+
import android.net.Uri
|
|
8
|
+
import android.os.ParcelFileDescriptor
|
|
9
|
+
import android.view.MotionEvent
|
|
10
|
+
import android.view.View
|
|
11
|
+
import android.widget.FrameLayout
|
|
12
|
+
import android.widget.ImageView
|
|
13
|
+
import android.widget.ScrollView
|
|
14
|
+
import android.widget.LinearLayout
|
|
15
|
+
import android.view.ViewTreeObserver
|
|
16
|
+
import android.view.ViewGroup
|
|
17
|
+
import expo.modules.kotlin.AppContext
|
|
18
|
+
import expo.modules.kotlin.views.ExpoView
|
|
19
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
20
|
+
import java.io.File
|
|
21
|
+
import java.io.FileOutputStream
|
|
22
|
+
import java.net.URL
|
|
23
|
+
import kotlinx.coroutines.*
|
|
24
|
+
|
|
25
|
+
class ExpoPdfReaderView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
|
|
26
|
+
private val scrollView = ScrollView(context)
|
|
27
|
+
private val container = FrameLayout(context)
|
|
28
|
+
private val imageView = ImageView(context)
|
|
29
|
+
private val drawingView = DrawingView(context)
|
|
30
|
+
|
|
31
|
+
private var pdfRenderer: PdfRenderer? = null
|
|
32
|
+
private var currentPage: PdfRenderer.Page? = null
|
|
33
|
+
private var fileDescriptor: ParcelFileDescriptor? = null
|
|
34
|
+
private val scope = CoroutineScope(Dispatchers.Main)
|
|
35
|
+
|
|
36
|
+
private var currentTool = "none"
|
|
37
|
+
private var currentColor = Color.BLACK
|
|
38
|
+
private var currentFontSize = 16.0
|
|
39
|
+
private var currentText = "Text"
|
|
40
|
+
private var currentPdfFile: File? = null
|
|
41
|
+
private var currentStrokeWidth: Double = 10.0
|
|
42
|
+
private var displayMode = "continuous" // 'single' | 'continuous' | 'twoUp' | 'twoUpContinuous'
|
|
43
|
+
private var initialPage: Int = 0
|
|
44
|
+
private var minZoom: Float = 1.0f
|
|
45
|
+
private var maxZoom: Float = 4.0f
|
|
46
|
+
private var currentZoom: Float = 1.0f
|
|
47
|
+
|
|
48
|
+
private val onAnnotationChange by EventDispatcher()
|
|
49
|
+
private val onPageChange by EventDispatcher()
|
|
50
|
+
private val onScroll by EventDispatcher()
|
|
51
|
+
|
|
52
|
+
init {
|
|
53
|
+
// Setup ScrollView
|
|
54
|
+
scrollView.layoutParams = FrameLayout.LayoutParams(
|
|
55
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
56
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
57
|
+
)
|
|
58
|
+
scrollView.isFillViewport = true
|
|
59
|
+
|
|
60
|
+
// Setup container
|
|
61
|
+
container.layoutParams = FrameLayout.LayoutParams(
|
|
62
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
63
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
// Setup Image View
|
|
67
|
+
imageView.layoutParams = FrameLayout.LayoutParams(
|
|
68
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
69
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
70
|
+
)
|
|
71
|
+
imageView.adjustViewBounds = true
|
|
72
|
+
imageView.scaleType = ImageView.ScaleType.FIT_START
|
|
73
|
+
|
|
74
|
+
// Setup Drawing View (Overlay)
|
|
75
|
+
drawingView.layoutParams = FrameLayout.LayoutParams(
|
|
76
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
77
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
78
|
+
)
|
|
79
|
+
drawingView.setBackgroundColor(Color.TRANSPARENT)
|
|
80
|
+
drawingView.visibility = View.GONE // Hidden by default
|
|
81
|
+
|
|
82
|
+
// Set callback
|
|
83
|
+
drawingView.onDrawingChanged = {
|
|
84
|
+
onAnnotationChange(mapOf("annotations" to getAnnotations()))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Setup scroll listener
|
|
88
|
+
scrollView.viewTreeObserver.addOnScrollChangedListener {
|
|
89
|
+
val scrollX = scrollView.scrollX
|
|
90
|
+
val scrollY = scrollView.scrollY
|
|
91
|
+
val contentWidth = scrollView.getChildAt(0)?.width ?: 0
|
|
92
|
+
val contentHeight = scrollView.getChildAt(0)?.height ?: 0
|
|
93
|
+
val layoutWidth = scrollView.width
|
|
94
|
+
val layoutHeight = scrollView.height
|
|
95
|
+
|
|
96
|
+
onScroll(mapOf(
|
|
97
|
+
"x" to scrollX.toDouble(),
|
|
98
|
+
"y" to scrollY.toDouble(),
|
|
99
|
+
"contentWidth" to contentWidth.toDouble(),
|
|
100
|
+
"contentHeight" to contentHeight.toDouble(),
|
|
101
|
+
"layoutWidth" to layoutWidth.toDouble(),
|
|
102
|
+
"layoutHeight" to layoutHeight.toDouble()
|
|
103
|
+
))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
container.addView(imageView)
|
|
107
|
+
container.addView(drawingView)
|
|
108
|
+
container.scaleX = currentZoom
|
|
109
|
+
container.scaleY = currentZoom
|
|
110
|
+
scrollView.addView(container)
|
|
111
|
+
addView(scrollView)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
fun setDisplayMode(mode: String) {
|
|
115
|
+
displayMode = mode
|
|
116
|
+
// Re-render PDF with new display mode
|
|
117
|
+
currentPdfFile?.let { file ->
|
|
118
|
+
renderPdf(file)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
fun setInitialPage(page: Int) {
|
|
123
|
+
initialPage = if (page >= 0) page else 0
|
|
124
|
+
// If PDF already loaded, jump immediately
|
|
125
|
+
if (pdfRenderer != null) {
|
|
126
|
+
goToPage(initialPage)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
fun setMinZoom(value: Double) {
|
|
131
|
+
val v = value.toFloat()
|
|
132
|
+
minZoom = if (v > 0f) v else 0.5f
|
|
133
|
+
if (currentZoom < minZoom) {
|
|
134
|
+
applyZoom(minZoom)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
fun setMaxZoom(value: Double) {
|
|
139
|
+
val v = value.toFloat()
|
|
140
|
+
maxZoom = if (v > minZoom) v else minZoom
|
|
141
|
+
if (currentZoom > maxZoom) {
|
|
142
|
+
applyZoom(maxZoom)
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private fun applyZoom(factor: Float) {
|
|
147
|
+
currentZoom = factor
|
|
148
|
+
container.scaleX = factor
|
|
149
|
+
container.scaleY = factor
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
fun zoomIn(): Boolean {
|
|
153
|
+
val newZoom = (currentZoom * 1.25f).coerceAtMost(maxZoom)
|
|
154
|
+
if (newZoom == currentZoom) return false
|
|
155
|
+
applyZoom(newZoom)
|
|
156
|
+
return true
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fun zoomOut(): Boolean {
|
|
160
|
+
val newZoom = (currentZoom / 1.25f).coerceAtLeast(minZoom)
|
|
161
|
+
if (newZoom == currentZoom) return false
|
|
162
|
+
applyZoom(newZoom)
|
|
163
|
+
return true
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fun setUrl(url: String) {
|
|
167
|
+
scope.launch(Dispatchers.IO) {
|
|
168
|
+
try {
|
|
169
|
+
val file = if (url.startsWith("http")) {
|
|
170
|
+
downloadFile(url)
|
|
171
|
+
} else {
|
|
172
|
+
val uri = Uri.parse(url)
|
|
173
|
+
File(uri.path ?: url)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (file.exists()) {
|
|
177
|
+
currentPdfFile = file
|
|
178
|
+
withContext(Dispatchers.Main) {
|
|
179
|
+
renderPdf(file)
|
|
180
|
+
// After render, go to initialPage (or 0)
|
|
181
|
+
if (pdfRenderer != null) {
|
|
182
|
+
val target = if (initialPage >= 0 && initialPage < pdfRenderer!!.pageCount) {
|
|
183
|
+
initialPage
|
|
184
|
+
} else {
|
|
185
|
+
0
|
|
186
|
+
}
|
|
187
|
+
goToPage(target)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} catch (e: Exception) {
|
|
192
|
+
e.printStackTrace()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
fun setAnnotationTool(tool: String) {
|
|
198
|
+
currentTool = tool
|
|
199
|
+
drawingView.setTool(tool)
|
|
200
|
+
|
|
201
|
+
if (tool == "none") {
|
|
202
|
+
if (drawingView.hasAnnotations()) {
|
|
203
|
+
drawingView.visibility = View.VISIBLE
|
|
204
|
+
} else {
|
|
205
|
+
drawingView.visibility = View.GONE
|
|
206
|
+
}
|
|
207
|
+
} else {
|
|
208
|
+
drawingView.visibility = View.VISIBLE
|
|
209
|
+
drawingView.bringToFront()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
fun setAnnotationColor(colorHex: String?) {
|
|
214
|
+
if (colorHex != null) {
|
|
215
|
+
try {
|
|
216
|
+
currentColor = Color.parseColor(colorHex)
|
|
217
|
+
drawingView.setColor(currentColor)
|
|
218
|
+
} catch (e: Exception) {
|
|
219
|
+
// Invalid color
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
fun setAnnotationFontSize(size: Double) {
|
|
225
|
+
currentFontSize = size
|
|
226
|
+
drawingView.setFontSize(size.toFloat())
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
fun setAnnotationText(text: String) {
|
|
230
|
+
currentText = text
|
|
231
|
+
drawingView.setTextToDraw(text)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
fun setAnnotationStrokeWidth(width: Double) {
|
|
235
|
+
currentStrokeWidth = width
|
|
236
|
+
drawingView.setBaseStrokeWidth(width.toFloat())
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fun getAnnotations(): List<AnnotationData> {
|
|
240
|
+
return drawingView.getAnnotations()
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fun setAnnotations(annotations: List<AnnotationData>) {
|
|
244
|
+
drawingView.setAnnotations(annotations)
|
|
245
|
+
if (annotations.isNotEmpty()) {
|
|
246
|
+
drawingView.visibility = View.VISIBLE
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
fun undo(): Boolean {
|
|
251
|
+
return drawingView.undo()
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
fun redo(): Boolean {
|
|
255
|
+
return drawingView.redo()
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
fun renderThumbnail(pageIndex: Int, targetWidth: Int): String {
|
|
259
|
+
if (targetWidth <= 0) {
|
|
260
|
+
throw IllegalArgumentException("targetWidth must be > 0")
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (pdfRenderer == null) {
|
|
264
|
+
if (currentPdfFile == null || !currentPdfFile!!.exists()) {
|
|
265
|
+
throw IllegalStateException("PDF not loaded")
|
|
266
|
+
}
|
|
267
|
+
fileDescriptor = ParcelFileDescriptor.open(currentPdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
268
|
+
pdfRenderer = PdfRenderer(fileDescriptor!!)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (pageIndex < 0 || pageIndex >= pdfRenderer!!.pageCount) {
|
|
272
|
+
throw IllegalArgumentException("Invalid page index")
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
val page = pdfRenderer!!.openPage(pageIndex)
|
|
276
|
+
try {
|
|
277
|
+
val scale = targetWidth.toFloat() / page.width.toFloat()
|
|
278
|
+
val targetHeight = (page.height * scale).toInt().coerceAtLeast(1)
|
|
279
|
+
|
|
280
|
+
val bitmap = Bitmap.createBitmap(
|
|
281
|
+
targetWidth,
|
|
282
|
+
targetHeight,
|
|
283
|
+
Bitmap.Config.ARGB_8888
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
287
|
+
|
|
288
|
+
val fileName = "thumb_${pageIndex}_${System.currentTimeMillis()}.png"
|
|
289
|
+
val file = File(context.cacheDir, fileName)
|
|
290
|
+
val output = FileOutputStream(file)
|
|
291
|
+
try {
|
|
292
|
+
bitmap.compress(Bitmap.CompressFormat.PNG, 90, output)
|
|
293
|
+
} finally {
|
|
294
|
+
output.close()
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return Uri.fromFile(file).toString()
|
|
298
|
+
} finally {
|
|
299
|
+
page.close()
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
fun searchText(text: String): List<SearchResultData> {
|
|
304
|
+
android.util.Log.w("ExpoPdfReader", "Search is not supported on Android with native PdfRenderer.")
|
|
305
|
+
return emptyList()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
fun goToPage(pageIndex: Int) {
|
|
309
|
+
if (pdfRenderer != null && pageIndex >= 0 && pageIndex < pdfRenderer!!.pageCount) {
|
|
310
|
+
try {
|
|
311
|
+
currentPage?.close()
|
|
312
|
+
currentPage = pdfRenderer!!.openPage(pageIndex)
|
|
313
|
+
|
|
314
|
+
val bitmap = Bitmap.createBitmap(
|
|
315
|
+
currentPage!!.width,
|
|
316
|
+
currentPage!!.height,
|
|
317
|
+
Bitmap.Config.ARGB_8888
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
currentPage!!.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
321
|
+
imageView.setImageBitmap(bitmap)
|
|
322
|
+
|
|
323
|
+
// Notify page change
|
|
324
|
+
onPageChange(mapOf(
|
|
325
|
+
"page" to pageIndex,
|
|
326
|
+
"total" to pdfRenderer!!.pageCount
|
|
327
|
+
))
|
|
328
|
+
|
|
329
|
+
} catch (e: Exception) {
|
|
330
|
+
e.printStackTrace()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fun savePdf(): String {
|
|
336
|
+
if (currentPdfFile == null || !currentPdfFile!!.exists()) {
|
|
337
|
+
throw Exception("No PDF loaded")
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
val document = PdfDocument()
|
|
341
|
+
val descriptor = ParcelFileDescriptor.open(currentPdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
342
|
+
val renderer = PdfRenderer(descriptor)
|
|
343
|
+
|
|
344
|
+
if (renderer.pageCount > 0) {
|
|
345
|
+
val page = renderer.openPage(0)
|
|
346
|
+
val pageInfo = PdfDocument.PageInfo.Builder(page.width, page.height, 1).create()
|
|
347
|
+
val newPage = document.startPage(pageInfo)
|
|
348
|
+
val canvas = newPage.canvas
|
|
349
|
+
|
|
350
|
+
val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
|
|
351
|
+
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
352
|
+
canvas.drawBitmap(bitmap, 0f, 0f, null)
|
|
353
|
+
|
|
354
|
+
val viewWidth = imageView.width.toFloat()
|
|
355
|
+
val viewHeight = imageView.height.toFloat()
|
|
356
|
+
val pdfWidth = page.width.toFloat()
|
|
357
|
+
val pdfHeight = page.height.toFloat()
|
|
358
|
+
|
|
359
|
+
val scale: Float
|
|
360
|
+
val dx: Float
|
|
361
|
+
val dy: Float
|
|
362
|
+
|
|
363
|
+
if (pdfWidth / pdfHeight > viewWidth / viewHeight) {
|
|
364
|
+
scale = viewWidth / pdfWidth
|
|
365
|
+
dx = 0f
|
|
366
|
+
dy = (viewHeight - pdfHeight * scale) * 0.5f
|
|
367
|
+
} else {
|
|
368
|
+
scale = viewHeight / pdfHeight
|
|
369
|
+
dx = (viewWidth - pdfWidth * scale) * 0.5f
|
|
370
|
+
dy = 0f
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
canvas.save()
|
|
374
|
+
canvas.scale(1/scale, 1/scale)
|
|
375
|
+
canvas.translate(-dx, -dy)
|
|
376
|
+
drawingView.drawPaths(canvas)
|
|
377
|
+
canvas.restore()
|
|
378
|
+
|
|
379
|
+
document.finishPage(newPage)
|
|
380
|
+
page.close()
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
renderer.close()
|
|
384
|
+
descriptor.close()
|
|
385
|
+
|
|
386
|
+
val fileName = "saved_pdf_${System.currentTimeMillis()}.pdf"
|
|
387
|
+
val file = File(context.cacheDir, fileName)
|
|
388
|
+
val outputStream = FileOutputStream(file)
|
|
389
|
+
document.writeTo(outputStream)
|
|
390
|
+
document.close()
|
|
391
|
+
outputStream.close()
|
|
392
|
+
|
|
393
|
+
return Uri.fromFile(file).toString()
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
private fun downloadFile(urlStr: String): File {
|
|
397
|
+
val url = URL(urlStr)
|
|
398
|
+
val connection = url.openConnection()
|
|
399
|
+
connection.connect()
|
|
400
|
+
val input = connection.getInputStream()
|
|
401
|
+
val file = File(context.cacheDir, "temp_${System.currentTimeMillis()}.pdf")
|
|
402
|
+
val output = FileOutputStream(file)
|
|
403
|
+
input.copyTo(output)
|
|
404
|
+
output.close()
|
|
405
|
+
input.close()
|
|
406
|
+
return file
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private fun renderPdf(file: File) {
|
|
410
|
+
try {
|
|
411
|
+
fileDescriptor?.close()
|
|
412
|
+
fileDescriptor = ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY)
|
|
413
|
+
pdfRenderer?.close()
|
|
414
|
+
pdfRenderer = PdfRenderer(fileDescriptor!!)
|
|
415
|
+
|
|
416
|
+
// Clear existing views
|
|
417
|
+
container.removeAllViews()
|
|
418
|
+
|
|
419
|
+
when (displayMode) {
|
|
420
|
+
"single" -> {
|
|
421
|
+
// Single page mode - show only current page
|
|
422
|
+
if (pdfRenderer!!.pageCount > 0) {
|
|
423
|
+
currentPage?.close()
|
|
424
|
+
currentPage = pdfRenderer!!.openPage(0)
|
|
425
|
+
|
|
426
|
+
val bitmap = Bitmap.createBitmap(
|
|
427
|
+
currentPage!!.width,
|
|
428
|
+
currentPage!!.height,
|
|
429
|
+
Bitmap.Config.ARGB_8888
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
currentPage!!.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
433
|
+
|
|
434
|
+
val singleImageView = ImageView(context)
|
|
435
|
+
singleImageView.layoutParams = FrameLayout.LayoutParams(
|
|
436
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
437
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
438
|
+
)
|
|
439
|
+
singleImageView.adjustViewBounds = true
|
|
440
|
+
singleImageView.scaleType = ImageView.ScaleType.FIT_START
|
|
441
|
+
singleImageView.setImageBitmap(bitmap)
|
|
442
|
+
|
|
443
|
+
container.addView(singleImageView)
|
|
444
|
+
container.addView(drawingView)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
"continuous" -> {
|
|
448
|
+
// Continuous mode - show all pages vertically
|
|
449
|
+
val verticalContainer = LinearLayout(context)
|
|
450
|
+
verticalContainer.orientation = LinearLayout.VERTICAL
|
|
451
|
+
verticalContainer.layoutParams = FrameLayout.LayoutParams(
|
|
452
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
453
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
for (i in 0 until pdfRenderer!!.pageCount) {
|
|
457
|
+
val page = pdfRenderer!!.openPage(i)
|
|
458
|
+
val bitmap = Bitmap.createBitmap(
|
|
459
|
+
page.width,
|
|
460
|
+
page.height,
|
|
461
|
+
Bitmap.Config.ARGB_8888
|
|
462
|
+
)
|
|
463
|
+
page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
464
|
+
|
|
465
|
+
val pageImageView = ImageView(context)
|
|
466
|
+
pageImageView.layoutParams = LinearLayout.LayoutParams(
|
|
467
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
468
|
+
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
469
|
+
)
|
|
470
|
+
pageImageView.adjustViewBounds = true
|
|
471
|
+
pageImageView.scaleType = ImageView.ScaleType.FIT_START
|
|
472
|
+
pageImageView.setImageBitmap(bitmap)
|
|
473
|
+
|
|
474
|
+
verticalContainer.addView(pageImageView)
|
|
475
|
+
page.close()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
container.addView(verticalContainer)
|
|
479
|
+
container.addView(drawingView)
|
|
480
|
+
}
|
|
481
|
+
"twoUp", "twoUpContinuous" -> {
|
|
482
|
+
// Two-up mode - show two pages side by side
|
|
483
|
+
val verticalContainer = LinearLayout(context)
|
|
484
|
+
verticalContainer.orientation = LinearLayout.VERTICAL
|
|
485
|
+
verticalContainer.layoutParams = FrameLayout.LayoutParams(
|
|
486
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
487
|
+
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
var i = 0
|
|
491
|
+
while (i < pdfRenderer!!.pageCount) {
|
|
492
|
+
val rowContainer = LinearLayout(context)
|
|
493
|
+
rowContainer.orientation = LinearLayout.HORIZONTAL
|
|
494
|
+
rowContainer.layoutParams = LinearLayout.LayoutParams(
|
|
495
|
+
LinearLayout.LayoutParams.MATCH_PARENT,
|
|
496
|
+
LinearLayout.LayoutParams.WRAP_CONTENT
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
// First page
|
|
500
|
+
val page1 = pdfRenderer!!.openPage(i)
|
|
501
|
+
val bitmap1 = Bitmap.createBitmap(
|
|
502
|
+
page1.width,
|
|
503
|
+
page1.height,
|
|
504
|
+
Bitmap.Config.ARGB_8888
|
|
505
|
+
)
|
|
506
|
+
page1.render(bitmap1, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
507
|
+
|
|
508
|
+
val pageImageView1 = ImageView(context)
|
|
509
|
+
pageImageView1.layoutParams = LinearLayout.LayoutParams(
|
|
510
|
+
0,
|
|
511
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
512
|
+
1.0f
|
|
513
|
+
)
|
|
514
|
+
pageImageView1.adjustViewBounds = true
|
|
515
|
+
pageImageView1.scaleType = ImageView.ScaleType.FIT_START
|
|
516
|
+
pageImageView1.setImageBitmap(bitmap1)
|
|
517
|
+
rowContainer.addView(pageImageView1)
|
|
518
|
+
page1.close()
|
|
519
|
+
|
|
520
|
+
// Second page (if exists)
|
|
521
|
+
if (i + 1 < pdfRenderer!!.pageCount) {
|
|
522
|
+
val page2 = pdfRenderer!!.openPage(i + 1)
|
|
523
|
+
val bitmap2 = Bitmap.createBitmap(
|
|
524
|
+
page2.width,
|
|
525
|
+
page2.height,
|
|
526
|
+
Bitmap.Config.ARGB_8888
|
|
527
|
+
)
|
|
528
|
+
page2.render(bitmap2, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
|
|
529
|
+
|
|
530
|
+
val pageImageView2 = ImageView(context)
|
|
531
|
+
pageImageView2.layoutParams = LinearLayout.LayoutParams(
|
|
532
|
+
0,
|
|
533
|
+
LinearLayout.LayoutParams.WRAP_CONTENT,
|
|
534
|
+
1.0f
|
|
535
|
+
)
|
|
536
|
+
pageImageView2.adjustViewBounds = true
|
|
537
|
+
pageImageView2.scaleType = ImageView.ScaleType.FIT_START
|
|
538
|
+
pageImageView2.setImageBitmap(bitmap2)
|
|
539
|
+
rowContainer.addView(pageImageView2)
|
|
540
|
+
page2.close()
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
verticalContainer.addView(rowContainer)
|
|
544
|
+
i += 2
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
container.addView(verticalContainer)
|
|
548
|
+
container.addView(drawingView)
|
|
549
|
+
}
|
|
550
|
+
else -> {
|
|
551
|
+
// Default to continuous
|
|
552
|
+
displayMode = "continuous"
|
|
553
|
+
renderPdf(file)
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Re-apply zoom after layout rebuild
|
|
559
|
+
applyZoom(currentZoom)
|
|
560
|
+
|
|
561
|
+
// Set current page for single mode
|
|
562
|
+
if (displayMode == "single" && pdfRenderer!!.pageCount > 0) {
|
|
563
|
+
currentPage?.close()
|
|
564
|
+
currentPage = pdfRenderer!!.openPage(0)
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
} catch (e: Exception) {
|
|
568
|
+
e.printStackTrace()
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
override fun onDetachedFromWindow() {
|
|
573
|
+
super.onDetachedFromWindow()
|
|
574
|
+
currentPage?.close()
|
|
575
|
+
pdfRenderer?.close()
|
|
576
|
+
fileDescriptor?.close()
|
|
577
|
+
scope.cancel()
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Inner class for handling drawing
|
|
581
|
+
inner class DrawingView(context: Context) : View(context) {
|
|
582
|
+
private var drawPath = Path()
|
|
583
|
+
private var drawPaint = Paint()
|
|
584
|
+
private var textPaint = Paint()
|
|
585
|
+
|
|
586
|
+
// Callback
|
|
587
|
+
var onDrawingChanged: (() -> Unit)? = null
|
|
588
|
+
|
|
589
|
+
// Store raw data for serialization
|
|
590
|
+
data class AnnotationItem(
|
|
591
|
+
val path: Path?,
|
|
592
|
+
val paint: Paint,
|
|
593
|
+
val type: String,
|
|
594
|
+
val color: Int,
|
|
595
|
+
val points: MutableList<PointF>,
|
|
596
|
+
val text: String = "",
|
|
597
|
+
val fontSize: Float = 16f,
|
|
598
|
+
val x: Float = 0f,
|
|
599
|
+
val y: Float = 0f
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
private val items = ArrayList<AnnotationItem>()
|
|
603
|
+
private val undoStack = ArrayList<AnnotationItem>() // History for undo
|
|
604
|
+
private val redoStack = ArrayList<AnnotationItem>() // History for redo
|
|
605
|
+
private var currentPoints = ArrayList<PointF>()
|
|
606
|
+
private var currentTool = "none"
|
|
607
|
+
private var currentTextToDraw = "Text"
|
|
608
|
+
private var currentFontSize = 16f
|
|
609
|
+
private var baseStrokeWidth = 10f
|
|
610
|
+
|
|
611
|
+
init {
|
|
612
|
+
setupPaint()
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
fun hasAnnotations(): Boolean {
|
|
616
|
+
return items.isNotEmpty()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private fun setupPaint() {
|
|
620
|
+
drawPaint.color = currentColor
|
|
621
|
+
drawPaint.isAntiAlias = true
|
|
622
|
+
drawPaint.strokeWidth = baseStrokeWidth
|
|
623
|
+
drawPaint.style = Paint.Style.STROKE
|
|
624
|
+
drawPaint.strokeJoin = Paint.Join.ROUND
|
|
625
|
+
drawPaint.strokeCap = Paint.Cap.ROUND
|
|
626
|
+
|
|
627
|
+
textPaint.color = currentColor
|
|
628
|
+
textPaint.isAntiAlias = true
|
|
629
|
+
textPaint.textSize = currentFontSize
|
|
630
|
+
textPaint.style = Paint.Style.FILL
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
fun setTool(tool: String) {
|
|
634
|
+
currentTool = tool
|
|
635
|
+
if (tool == "highlighter") {
|
|
636
|
+
drawPaint.alpha = 80
|
|
637
|
+
drawPaint.strokeWidth = baseStrokeWidth * 3f
|
|
638
|
+
} else if (tool == "eraser") {
|
|
639
|
+
// Eraser doesn't draw, it erases
|
|
640
|
+
} else {
|
|
641
|
+
drawPaint.alpha = 255
|
|
642
|
+
drawPaint.strokeWidth = baseStrokeWidth
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
fun setColor(color: Int) {
|
|
647
|
+
drawPaint.color = color
|
|
648
|
+
textPaint.color = color
|
|
649
|
+
if (currentTool == "highlighter") {
|
|
650
|
+
drawPaint.alpha = 80
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fun setFontSize(size: Float) {
|
|
655
|
+
currentFontSize = size
|
|
656
|
+
textPaint.textSize = size
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
fun setBaseStrokeWidth(width: Float) {
|
|
660
|
+
baseStrokeWidth = if (width > 0f) width else 1f
|
|
661
|
+
// Apply to current tool
|
|
662
|
+
if (currentTool == "highlighter") {
|
|
663
|
+
drawPaint.strokeWidth = baseStrokeWidth * 3f
|
|
664
|
+
} else {
|
|
665
|
+
drawPaint.strokeWidth = baseStrokeWidth
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
fun setTextToDraw(text: String) {
|
|
670
|
+
currentTextToDraw = text
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
fun drawPaths(canvas: Canvas) {
|
|
674
|
+
for (item in items) {
|
|
675
|
+
if (item.type == "text") {
|
|
676
|
+
canvas.drawText(item.text, item.x, item.y, item.paint)
|
|
677
|
+
} else if (item.path != null) {
|
|
678
|
+
canvas.drawPath(item.path, item.paint)
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
fun getAnnotations(): List<AnnotationData> {
|
|
684
|
+
return items.map { item ->
|
|
685
|
+
val annotation = AnnotationData()
|
|
686
|
+
annotation.type = item.type
|
|
687
|
+
annotation.color = String.format("#%06X", (0xFFFFFF and item.color))
|
|
688
|
+
|
|
689
|
+
if (item.type == "text") {
|
|
690
|
+
annotation.text = item.text
|
|
691
|
+
annotation.fontSize = item.fontSize.toDouble()
|
|
692
|
+
annotation.x = item.x.toDouble()
|
|
693
|
+
annotation.y = item.y.toDouble()
|
|
694
|
+
} else {
|
|
695
|
+
annotation.width = if (item.type == "highlighter") (baseStrokeWidth * 3f).toDouble() else baseStrokeWidth.toDouble()
|
|
696
|
+
val pointsList = ArrayList<Map<String, Double>>()
|
|
697
|
+
for (pt in item.points) {
|
|
698
|
+
pointsList.add(mapOf("x" to pt.x.toDouble(), "y" to pt.y.toDouble()))
|
|
699
|
+
}
|
|
700
|
+
annotation.points = listOf(pointsList)
|
|
701
|
+
}
|
|
702
|
+
annotation
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
fun undo(): Boolean {
|
|
707
|
+
if (items.isEmpty()) return false
|
|
708
|
+
|
|
709
|
+
val lastItem = items.removeAt(items.size - 1)
|
|
710
|
+
undoStack.add(lastItem)
|
|
711
|
+
invalidate()
|
|
712
|
+
onDrawingChanged?.invoke()
|
|
713
|
+
return true
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
fun redo(): Boolean {
|
|
717
|
+
if (undoStack.isEmpty()) return false
|
|
718
|
+
|
|
719
|
+
val item = undoStack.removeAt(undoStack.size - 1)
|
|
720
|
+
items.add(item)
|
|
721
|
+
invalidate()
|
|
722
|
+
onDrawingChanged?.invoke()
|
|
723
|
+
return true
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
fun setAnnotations(annotations: List<AnnotationData>) {
|
|
727
|
+
items.clear()
|
|
728
|
+
undoStack.clear()
|
|
729
|
+
redoStack.clear()
|
|
730
|
+
for (ann in annotations) {
|
|
731
|
+
if (ann.type == "text") {
|
|
732
|
+
val paint = Paint(textPaint)
|
|
733
|
+
try {
|
|
734
|
+
paint.color = Color.parseColor(ann.color)
|
|
735
|
+
} catch (e: Exception) {
|
|
736
|
+
paint.color = Color.BLACK
|
|
737
|
+
}
|
|
738
|
+
paint.textSize = ann.fontSize.toFloat()
|
|
739
|
+
|
|
740
|
+
items.add(AnnotationItem(
|
|
741
|
+
null,
|
|
742
|
+
paint,
|
|
743
|
+
"text",
|
|
744
|
+
paint.color,
|
|
745
|
+
ArrayList(),
|
|
746
|
+
ann.text,
|
|
747
|
+
ann.fontSize.toFloat(),
|
|
748
|
+
ann.x.toFloat(),
|
|
749
|
+
ann.y.toFloat()
|
|
750
|
+
))
|
|
751
|
+
} else {
|
|
752
|
+
val path = Path()
|
|
753
|
+
val points = ArrayList<PointF>()
|
|
754
|
+
|
|
755
|
+
if (ann.points.isNotEmpty()) {
|
|
756
|
+
val stroke = ann.points[0]
|
|
757
|
+
if (stroke.isNotEmpty()) {
|
|
758
|
+
val start = stroke[0]
|
|
759
|
+
path.moveTo(start["x"]!!.toFloat(), start["y"]!!.toFloat())
|
|
760
|
+
points.add(PointF(start["x"]!!.toFloat(), start["y"]!!.toFloat()))
|
|
761
|
+
|
|
762
|
+
for (i in 1 until stroke.size) {
|
|
763
|
+
val pt = stroke[i]
|
|
764
|
+
path.lineTo(pt["x"]!!.toFloat(), pt["y"]!!.toFloat())
|
|
765
|
+
points.add(PointF(pt["x"]!!.toFloat(), pt["y"]!!.toFloat()))
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
val paint = Paint()
|
|
771
|
+
paint.isAntiAlias = true
|
|
772
|
+
paint.style = Paint.Style.STROKE
|
|
773
|
+
paint.strokeJoin = Paint.Join.ROUND
|
|
774
|
+
paint.strokeCap = Paint.Cap.ROUND
|
|
775
|
+
|
|
776
|
+
try {
|
|
777
|
+
paint.color = Color.parseColor(ann.color)
|
|
778
|
+
} catch (e: Exception) {
|
|
779
|
+
paint.color = Color.BLACK
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
if (ann.type == "highlighter") {
|
|
783
|
+
paint.alpha = 80
|
|
784
|
+
paint.strokeWidth = 30f
|
|
785
|
+
} else {
|
|
786
|
+
paint.alpha = 255
|
|
787
|
+
paint.strokeWidth = 10f
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
val colorInt = paint.color
|
|
791
|
+
items.add(AnnotationItem(path, paint, ann.type, colorInt, points))
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
invalidate()
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
override fun onDraw(canvas: Canvas) {
|
|
798
|
+
super.onDraw(canvas)
|
|
799
|
+
for (item in items) {
|
|
800
|
+
if (item.type == "text") {
|
|
801
|
+
canvas.drawText(item.text, item.x, item.y, item.paint)
|
|
802
|
+
} else if (item.path != null) {
|
|
803
|
+
canvas.drawPath(item.path, item.paint)
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
if (currentTool != "none" && currentTool != "text") {
|
|
807
|
+
canvas.drawPath(drawPath, drawPaint)
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
private fun findAnnotationAt(x: Float, y: Float, tolerance: Float = 20f): AnnotationItem? {
|
|
812
|
+
// Check items in reverse order (most recent first)
|
|
813
|
+
for (i in items.size - 1 downTo 0) {
|
|
814
|
+
val item = items[i]
|
|
815
|
+
|
|
816
|
+
if (item.type == "text") {
|
|
817
|
+
// Check if point is near text
|
|
818
|
+
val textBounds = Rect()
|
|
819
|
+
item.paint.getTextBounds(item.text, 0, item.text.length, textBounds)
|
|
820
|
+
val textWidth = item.paint.measureText(item.text)
|
|
821
|
+
val textHeight = textBounds.height().toFloat()
|
|
822
|
+
|
|
823
|
+
if (x >= item.x - tolerance && x <= item.x + textWidth + tolerance &&
|
|
824
|
+
y >= item.y - textHeight - tolerance && y <= item.y + tolerance) {
|
|
825
|
+
return item
|
|
826
|
+
}
|
|
827
|
+
} else if (item.path != null) {
|
|
828
|
+
// Check if point is on or near the path
|
|
829
|
+
val region = Region()
|
|
830
|
+
val rect = RectF()
|
|
831
|
+
item.path.computeBounds(rect, true)
|
|
832
|
+
rect.inset(-tolerance, -tolerance)
|
|
833
|
+
|
|
834
|
+
region.setPath(item.path, Region(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt()))
|
|
835
|
+
if (region.contains(x.toInt(), y.toInt())) {
|
|
836
|
+
return item
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// Also check if point is near any point in the path
|
|
840
|
+
for (pt in item.points) {
|
|
841
|
+
val dx = pt.x - x
|
|
842
|
+
val dy = pt.y - y
|
|
843
|
+
if (dx * dx + dy * dy <= tolerance * tolerance) {
|
|
844
|
+
return item
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
return null
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
853
|
+
if (currentTool == "none") return false
|
|
854
|
+
|
|
855
|
+
val touchX = event.x
|
|
856
|
+
val touchY = event.y
|
|
857
|
+
|
|
858
|
+
if (currentTool == "eraser") {
|
|
859
|
+
when (event.action) {
|
|
860
|
+
MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> {
|
|
861
|
+
val item = findAnnotationAt(touchX, touchY)
|
|
862
|
+
if (item != null) {
|
|
863
|
+
items.remove(item)
|
|
864
|
+
undoStack.add(item)
|
|
865
|
+
redoStack.clear()
|
|
866
|
+
invalidate()
|
|
867
|
+
onDrawingChanged?.invoke()
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
return true
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
if (currentTool == "text") {
|
|
875
|
+
if (event.action == MotionEvent.ACTION_UP) {
|
|
876
|
+
val paint = Paint(textPaint)
|
|
877
|
+
val newItem = AnnotationItem(
|
|
878
|
+
null,
|
|
879
|
+
paint,
|
|
880
|
+
"text",
|
|
881
|
+
paint.color,
|
|
882
|
+
ArrayList(),
|
|
883
|
+
currentTextToDraw,
|
|
884
|
+
currentFontSize,
|
|
885
|
+
touchX,
|
|
886
|
+
touchY
|
|
887
|
+
)
|
|
888
|
+
items.add(newItem)
|
|
889
|
+
|
|
890
|
+
// Clear redo stack when new action is performed
|
|
891
|
+
redoStack.clear()
|
|
892
|
+
|
|
893
|
+
invalidate()
|
|
894
|
+
onDrawingChanged?.invoke()
|
|
895
|
+
}
|
|
896
|
+
return true
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
when (event.action) {
|
|
900
|
+
MotionEvent.ACTION_DOWN -> {
|
|
901
|
+
drawPath.moveTo(touchX, touchY)
|
|
902
|
+
currentPoints.clear()
|
|
903
|
+
currentPoints.add(PointF(touchX, touchY))
|
|
904
|
+
}
|
|
905
|
+
MotionEvent.ACTION_MOVE -> {
|
|
906
|
+
drawPath.lineTo(touchX, touchY)
|
|
907
|
+
currentPoints.add(PointF(touchX, touchY))
|
|
908
|
+
}
|
|
909
|
+
MotionEvent.ACTION_UP -> {
|
|
910
|
+
drawPath.lineTo(touchX, touchY)
|
|
911
|
+
currentPoints.add(PointF(touchX, touchY))
|
|
912
|
+
|
|
913
|
+
val newPaint = Paint(drawPaint)
|
|
914
|
+
val newPath = Path(drawPath)
|
|
915
|
+
val newPoints = ArrayList(currentPoints)
|
|
916
|
+
|
|
917
|
+
val newItem = AnnotationItem(newPath, newPaint, currentTool, drawPaint.color, newPoints)
|
|
918
|
+
items.add(newItem)
|
|
919
|
+
|
|
920
|
+
// Clear redo stack when new action is performed
|
|
921
|
+
redoStack.clear()
|
|
922
|
+
|
|
923
|
+
drawPath.reset()
|
|
924
|
+
|
|
925
|
+
// Trigger callback
|
|
926
|
+
onDrawingChanged?.invoke()
|
|
927
|
+
}
|
|
928
|
+
else -> return false
|
|
929
|
+
}
|
|
930
|
+
invalidate()
|
|
931
|
+
return true
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|