react-native-photo-video-editor 0.1.0 → 0.1.1

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.
Files changed (203) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/PhotoVideoEditor.podspec +1 -0
  3. package/README.md +10 -6
  4. package/THIRD_PARTY_NOTICES.md +20 -0
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
  8. package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
  9. package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
  10. package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
  11. package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
  12. package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
  13. package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
  14. package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
  15. package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
  16. package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
  17. package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
  18. package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
  19. package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
  20. package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
  21. package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
  22. package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
  23. package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
  24. package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
  25. package/android/src/main/assets/Stickers/LOL_1.png +0 -0
  26. package/android/src/main/assets/Stickers/LOL_10.png +0 -0
  27. package/android/src/main/assets/Stickers/LOL_11.png +0 -0
  28. package/android/src/main/assets/Stickers/LOL_12.png +0 -0
  29. package/android/src/main/assets/Stickers/LOL_13.png +0 -0
  30. package/android/src/main/assets/Stickers/LOL_14.png +0 -0
  31. package/android/src/main/assets/Stickers/LOL_15.png +0 -0
  32. package/android/src/main/assets/Stickers/LOL_16.png +0 -0
  33. package/android/src/main/assets/Stickers/LOL_17.png +0 -0
  34. package/android/src/main/assets/Stickers/LOL_18.png +0 -0
  35. package/android/src/main/assets/Stickers/LOL_19.png +0 -0
  36. package/android/src/main/assets/Stickers/LOL_2.png +0 -0
  37. package/android/src/main/assets/Stickers/LOL_3.png +0 -0
  38. package/android/src/main/assets/Stickers/LOL_4.png +0 -0
  39. package/android/src/main/assets/Stickers/LOL_5.png +0 -0
  40. package/android/src/main/assets/Stickers/LOL_6.png +0 -0
  41. package/android/src/main/assets/Stickers/LOL_7.png +0 -0
  42. package/android/src/main/assets/Stickers/LOL_8.png +0 -0
  43. package/android/src/main/assets/Stickers/LOL_9.png +0 -0
  44. package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
  45. package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
  46. package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
  47. package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
  48. package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
  49. package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
  50. package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
  51. package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
  52. package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
  53. package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
  54. package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
  55. package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
  56. package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
  57. package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
  58. package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
  59. package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
  60. package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
  61. package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
  62. package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
  63. package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
  64. package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
  65. package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
  66. package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
  67. package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
  68. package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
  69. package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
  70. package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
  71. package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
  72. package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
  73. package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
  74. package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
  75. package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
  76. package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
  77. package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
  78. package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
  79. package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
  80. package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
  81. package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
  82. package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
  83. package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
  84. package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
  85. package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
  86. package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
  87. package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
  88. package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
  89. package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
  90. package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +188 -108
  91. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
  92. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +21 -0
  93. package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +34 -2
  94. package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
  95. package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +21 -1
  96. package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
  97. package/android/src/main/res/drawable/ic_draw.xml +6 -0
  98. package/android/src/main/res/drawable/ic_pause.xml +7 -0
  99. package/android/src/main/res/drawable/ic_play.xml +7 -0
  100. package/docs/api.md +1 -1
  101. package/docs/photo-editor.md +5 -6
  102. package/ios/Editor/PhotoVideoEditorViewController.swift +166 -77
  103. package/ios/Photo/Rendering/PhotoLayer.swift +5 -1
  104. package/ios/Photo/Rendering/PhotoLayerRenderer.swift +14 -0
  105. package/ios/Photo/UI/CropOverlayView.swift +38 -1
  106. package/ios/Photo/UI/DrawOverlayView.swift +48 -0
  107. package/ios/Photo/UI/LayerOverlayView.swift +9 -1
  108. package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
  109. package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
  110. package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
  111. package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
  112. package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
  113. package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
  114. package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
  115. package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
  116. package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
  117. package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
  118. package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
  119. package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
  120. package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
  121. package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
  122. package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
  123. package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
  124. package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
  125. package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
  126. package/ios/Resources/Stickers/LOL_1.png +0 -0
  127. package/ios/Resources/Stickers/LOL_10.png +0 -0
  128. package/ios/Resources/Stickers/LOL_11.png +0 -0
  129. package/ios/Resources/Stickers/LOL_12.png +0 -0
  130. package/ios/Resources/Stickers/LOL_13.png +0 -0
  131. package/ios/Resources/Stickers/LOL_14.png +0 -0
  132. package/ios/Resources/Stickers/LOL_15.png +0 -0
  133. package/ios/Resources/Stickers/LOL_16.png +0 -0
  134. package/ios/Resources/Stickers/LOL_17.png +0 -0
  135. package/ios/Resources/Stickers/LOL_18.png +0 -0
  136. package/ios/Resources/Stickers/LOL_19.png +0 -0
  137. package/ios/Resources/Stickers/LOL_2.png +0 -0
  138. package/ios/Resources/Stickers/LOL_3.png +0 -0
  139. package/ios/Resources/Stickers/LOL_4.png +0 -0
  140. package/ios/Resources/Stickers/LOL_5.png +0 -0
  141. package/ios/Resources/Stickers/LOL_6.png +0 -0
  142. package/ios/Resources/Stickers/LOL_7.png +0 -0
  143. package/ios/Resources/Stickers/LOL_8.png +0 -0
  144. package/ios/Resources/Stickers/LOL_9.png +0 -0
  145. package/ios/Resources/Stickers/SPRING_1.png +0 -0
  146. package/ios/Resources/Stickers/SPRING_2.png +0 -0
  147. package/ios/Resources/Stickers/SPRING_3.png +0 -0
  148. package/ios/Resources/Stickers/SPRING_4.png +0 -0
  149. package/ios/Resources/Stickers/SPRING_5.png +0 -0
  150. package/ios/Resources/Stickers/SPRING_6.png +0 -0
  151. package/ios/Resources/Stickers/SUMMER_1.png +0 -0
  152. package/ios/Resources/Stickers/SUMMER_10.png +0 -0
  153. package/ios/Resources/Stickers/SUMMER_11.png +0 -0
  154. package/ios/Resources/Stickers/SUMMER_12.png +0 -0
  155. package/ios/Resources/Stickers/SUMMER_13.png +0 -0
  156. package/ios/Resources/Stickers/SUMMER_14.png +0 -0
  157. package/ios/Resources/Stickers/SUMMER_15.png +0 -0
  158. package/ios/Resources/Stickers/SUMMER_16.png +0 -0
  159. package/ios/Resources/Stickers/SUMMER_17.png +0 -0
  160. package/ios/Resources/Stickers/SUMMER_18.png +0 -0
  161. package/ios/Resources/Stickers/SUMMER_19.png +0 -0
  162. package/ios/Resources/Stickers/SUMMER_2.png +0 -0
  163. package/ios/Resources/Stickers/SUMMER_20.png +0 -0
  164. package/ios/Resources/Stickers/SUMMER_21.png +0 -0
  165. package/ios/Resources/Stickers/SUMMER_3.png +0 -0
  166. package/ios/Resources/Stickers/SUMMER_4.png +0 -0
  167. package/ios/Resources/Stickers/SUMMER_5.png +0 -0
  168. package/ios/Resources/Stickers/SUMMER_6.png +0 -0
  169. package/ios/Resources/Stickers/SUMMER_7.png +0 -0
  170. package/ios/Resources/Stickers/SUMMER_8.png +0 -0
  171. package/ios/Resources/Stickers/SUMMER_9.png +0 -0
  172. package/ios/Resources/Stickers/TEXT_1.png +0 -0
  173. package/ios/Resources/Stickers/TEXT_12.png +0 -0
  174. package/ios/Resources/Stickers/TEXT_13.png +0 -0
  175. package/ios/Resources/Stickers/TEXT_14.png +0 -0
  176. package/ios/Resources/Stickers/TEXT_15.png +0 -0
  177. package/ios/Resources/Stickers/TEXT_16.png +0 -0
  178. package/ios/Resources/Stickers/TEXT_2.png +0 -0
  179. package/ios/Resources/Stickers/TEXT_25.png +0 -0
  180. package/ios/Resources/Stickers/TEXT_26.png +0 -0
  181. package/ios/Resources/Stickers/TEXT_27.png +0 -0
  182. package/ios/Resources/Stickers/TEXT_28.png +0 -0
  183. package/ios/Resources/Stickers/TEXT_29.png +0 -0
  184. package/ios/Resources/Stickers/TEXT_3.png +0 -0
  185. package/ios/Resources/Stickers/TEXT_30.png +0 -0
  186. package/ios/Resources/Stickers/TEXT_31.png +0 -0
  187. package/ios/Resources/Stickers/TEXT_32.png +0 -0
  188. package/ios/Resources/Stickers/TEXT_7.png +0 -0
  189. package/ios/Resources/Stickers/TEXT_8.png +0 -0
  190. package/ios/Resources/Stickers/TEXT_9.png +0 -0
  191. package/ios/Resources/Stickers/a_pety_icon.png +0 -0
  192. package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
  193. package/lib/module/index.js +2 -2
  194. package/lib/module/index.js.map +1 -1
  195. package/lib/typescript/src/types.d.ts +2 -2
  196. package/lib/typescript/src/types.d.ts.map +1 -1
  197. package/package.json +2 -1
  198. package/src/index.ts +1 -1
  199. package/src/types.ts +2 -2
  200. package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
  201. package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
  202. package/ios/Stickers/OpenMojiCatalog.swift +0 -188
  203. package/ios/Stickers/RemoteImageLoader.swift +0 -127
@@ -39,7 +39,9 @@ import com.photovideoeditor.photo.render.PhotoEditSession
39
39
  import com.photovideoeditor.photo.render.PhotoFilterPresets
40
40
  import com.photovideoeditor.photo.render.PhotoLayer
41
41
  import com.photovideoeditor.photo.render.PhotoLayerRenderer
42
+ import com.photovideoeditor.photo.render.PhotoTransformState
42
43
  import com.photovideoeditor.photo.ui.CropOverlayView
44
+ import com.photovideoeditor.photo.ui.DrawOverlayView
43
45
  import com.photovideoeditor.photo.ui.FilterThumbnailLoader
44
46
  import com.photovideoeditor.video.export.VideoExportException
45
47
  import com.photovideoeditor.video.export.VideoExporter
@@ -51,6 +53,7 @@ import com.photovideoeditor.photo.ui.LayerOverlayView
51
53
  import com.photovideoeditor.photo.ui.PhotoPreviewRenderCoordinator
52
54
  import com.photovideoeditor.photo.ui.ZoomableImageView
53
55
  import com.photovideoeditor.stickers.OnlineStickerSheet
56
+ import com.photovideoeditor.stickers.RuntimeSticker
54
57
  import com.photovideoeditor.ui.DesignTokens
55
58
  import com.photovideoeditor.ui.components.applyPressScale
56
59
  import com.photovideoeditor.ui.components.editorChip
@@ -67,8 +70,10 @@ class PhotoVideoEditorActivity : Activity() {
67
70
 
68
71
  private var photoSession: PhotoEditSession? = null
69
72
  private var cropMode = false
73
+ private var cropEntryState: PhotoTransformState? = null
70
74
  private var adjustMode = false
71
75
  private var filtersMode = false
76
+ private var drawMode = false
72
77
  private var stickersMode = false
73
78
  private lateinit var stickersSubBar: View
74
79
  private var activeAdjustmentKey = "brightness"
@@ -78,11 +83,13 @@ class PhotoVideoEditorActivity : Activity() {
78
83
  private lateinit var photoImageView: ZoomableImageView
79
84
  private lateinit var cropOverlay: CropOverlayView
80
85
  private lateinit var layerOverlay: LayerOverlayView
86
+ private lateinit var drawOverlay: DrawOverlayView
81
87
  private lateinit var straightenSeekBar: SeekBar
82
88
  private lateinit var cropSubBar: View
83
89
  private lateinit var adjustmentSeekBar: SeekBar
84
90
  private lateinit var filtersSubBar: View
85
91
  private lateinit var adjustSubBar: View
92
+ private lateinit var drawSubBar: View
86
93
  private lateinit var layerToolBar: View
87
94
  private lateinit var layerPropertySeekBar: SeekBar
88
95
  private lateinit var layerColorSwatchRow: View
@@ -234,18 +241,6 @@ class PhotoVideoEditorActivity : Activity() {
234
241
  )
235
242
  previewContainer.addView(imageView, FrameLayout.LayoutParams(MATCH, MATCH))
236
243
 
237
- previewContainer.addView(TextView(this).apply {
238
- text = "● ORIGINAL • 100%"
239
- textSize = 10f
240
- letterSpacing = 0.08f
241
- setTextColor(DesignTokens.onSurfaceVariant)
242
- gravity = Gravity.CENTER
243
- background = roundedDrawable(DesignTokens.withAlphaPercent(DesignTokens.surfaceContainerHigh, 92), DesignTokens.radiusLg)
244
- setPadding(dp(DesignTokens.spaceMd), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceMd), dp(DesignTokens.spaceXs))
245
- }, FrameLayout.LayoutParams(WRAP, WRAP, Gravity.TOP or Gravity.CENTER_HORIZONTAL).apply {
246
- topMargin = dp(DesignTokens.spaceSm)
247
- })
248
-
249
244
  val overlay = CropOverlayView(this).apply { visibility = View.GONE }
250
245
  cropOverlay = overlay
251
246
  previewContainer.addView(overlay, FrameLayout.LayoutParams(MATCH, MATCH))
@@ -254,12 +249,20 @@ class PhotoVideoEditorActivity : Activity() {
254
249
  layerOverlay = layers
255
250
  previewContainer.addView(layers, FrameLayout.LayoutParams(MATCH, MATCH))
256
251
 
252
+ val drawing = DrawOverlayView(this).apply { visibility = View.GONE }
253
+ drawOverlay = drawing
254
+ previewContainer.addView(drawing, FrameLayout.LayoutParams(MATCH, MATCH))
255
+
257
256
  imageView.onBoundsChanged = { bounds ->
258
257
  if (cropMode) overlay.setImageBounds(bounds, resetCrop = false)
259
258
  layers.setImageBounds(bounds)
259
+ drawing.setImageBounds(bounds)
260
260
  }
261
261
  overlay.onCropChanged = { left, top, right, bottom -> session.update { it.withCrop(left, top, right, bottom) } }
262
262
  layers.onLayerTapped = { id -> selectLayer(id) }
263
+ layers.onLayerDoubleTapped = { id ->
264
+ session.layerStack.layers.firstOrNull { it.id == id && it.type == LayerType.TEXT }?.let { showTextInputDialog(session, it) }
265
+ }
263
266
  layers.onLayerTransformChanged = { id, x, y, scale, rotation ->
264
267
  if (dragStartSnapshot == null) dragStartSnapshot = session.layerStack.layers
265
268
  session.layerStack.updateLive { list ->
@@ -342,6 +345,11 @@ class PhotoVideoEditorActivity : Activity() {
342
345
  stickersBar.visibility = View.GONE
343
346
  root.addView(stickersBar, LinearLayout.LayoutParams(MATCH, dp(ASSET_GRID_BAR_HEIGHT_DP)))
344
347
 
348
+ val drawBar = createDrawSubBar(session)
349
+ drawSubBar = drawBar
350
+ drawBar.visibility = View.GONE
351
+ root.addView(drawBar, LinearLayout.LayoutParams(MATCH, dp(56)))
352
+
345
353
  val layerPropertySeek = SeekBar(this).apply {
346
354
  visibility = View.GONE
347
355
  setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
@@ -381,6 +389,7 @@ class PhotoVideoEditorActivity : Activity() {
381
389
  resizeBar to dp(56),
382
390
  filtersBar to dp(FILTERS_SUB_BAR_HEIGHT_DP),
383
391
  stickersBar to dp(ASSET_GRID_BAR_HEIGHT_DP),
392
+ drawBar to dp(56),
384
393
  layerBar to dp(56)
385
394
  )
386
395
  )
@@ -394,9 +403,7 @@ class PhotoVideoEditorActivity : Activity() {
394
403
  Triple("filters", "Filters", R.drawable.ic_photo_filter),
395
404
  Triple("text", "Text", R.drawable.ic_title),
396
405
  Triple("stickers", "Stickers", R.drawable.ic_sentiment_satisfied),
397
- Triple("overlay", "Overlay", R.drawable.ic_wallpaper),
398
- Triple("retouch", "Retouch", R.drawable.ic_face),
399
- Triple("layers", "Layers", R.drawable.ic_layers),
406
+ Triple("draw", "Draw", R.drawable.ic_draw),
400
407
  Triple("resize", "Resize", R.drawable.ic_aspect_ratio)
401
408
  )
402
409
 
@@ -438,6 +445,7 @@ class PhotoVideoEditorActivity : Activity() {
438
445
  "adjust" -> adjustMode
439
446
  "filters" -> filtersMode
440
447
  "stickers" -> stickersMode
448
+ "draw" -> drawMode
441
449
  "resize" -> resizeMode
442
450
  else -> false
443
451
  }
@@ -465,51 +473,59 @@ class PhotoVideoEditorActivity : Activity() {
465
473
  "adjust" -> setAdjustMode(true, session)
466
474
  "filters" -> setFiltersMode(true, session)
467
475
  "text" -> showTextInputDialog(session)
468
- "stickers" -> setStickersMode(true, session)
469
- "overlay" -> launchOverlayPicker(REQUEST_CODE_OVERLAY_UPLOAD)
476
+ "stickers" -> showStickerBottomSheet(session)
477
+ "draw" -> setDrawMode(true, session)
470
478
  "resize" -> setResizeMode(true, session, imageView, overlay)
471
- "retouch" -> setAdjustMode(true, session)
472
- "layers" -> openLayerManager(session)
473
479
  else -> Toast.makeText(this, "$key is coming in a later milestone.", Toast.LENGTH_SHORT).show()
474
480
  }
475
481
  }
476
482
 
477
- private fun openLayerManager(session: PhotoEditSession) {
478
- val top = session.layerStack.layers.lastOrNull()
479
- if (top == null) Toast.makeText(this, "Add content to start a layer stack.", Toast.LENGTH_LONG).show()
480
- else selectLayer(top.id)
481
- }
482
-
483
483
  // ---------------------------------------------------------------------
484
484
  // Layers: text, stickers, shapes, freehand drawing, selection, and
485
485
  // transform/duplicate/reorder/lock/hide/delete with undo/redo (Milestone 4).
486
486
  // ---------------------------------------------------------------------
487
487
 
488
- private fun showTextInputDialog(session: PhotoEditSession) {
488
+ private fun showTextInputDialog(session: PhotoEditSession, editingLayer: PhotoLayer? = null) {
489
+ var selectedColor = editingLayer?.textColor ?: Color.WHITE
489
490
  val input = EditText(this).apply {
490
491
  inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
491
- setTextColor(DesignTokens.onSurface)
492
+ setTextColor(selectedColor)
492
493
  setHintTextColor(DesignTokens.outline)
493
494
  hint = "Type something..."
495
+ setText(editingLayer?.text.orEmpty())
496
+ gravity = Gravity.CENTER
494
497
  setPadding(dp(DesignTokens.spaceLg), dp(DesignTokens.spaceLg), dp(DesignTokens.spaceLg), dp(DesignTokens.spaceLg))
495
498
  }
496
- val container = FrameLayout(this).apply {
499
+ val colors = listOf(Color.WHITE, Color.BLACK, Color.RED, Color.YELLOW, Color.GREEN, Color.CYAN, Color.BLUE, Color.MAGENTA)
500
+ val colorRow = LinearLayout(this).apply {
501
+ orientation = LinearLayout.HORIZONTAL
502
+ gravity = Gravity.CENTER
503
+ colors.forEach { color ->
504
+ addView(createColorSwatch(color) { selectedColor = color; input.setTextColor(color) }, LinearLayout.LayoutParams(dp(40), dp(40)))
505
+ }
506
+ }
507
+ val container = LinearLayout(this).apply {
508
+ orientation = LinearLayout.VERTICAL
497
509
  background = roundedDrawable(DesignTokens.surfaceContainerHigh, DesignTokens.radiusLg)
498
- addView(input, FrameLayout.LayoutParams(MATCH, WRAP))
510
+ addView(input, LinearLayout.LayoutParams(MATCH, dp(220)))
511
+ addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply { addView(colorRow) }, LinearLayout.LayoutParams(MATCH, dp(56)))
499
512
  }
500
- AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert)
501
- .setTitle("Add text")
513
+ val dialog = AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert)
514
+ .setTitle(if (editingLayer == null) "Add text" else "Edit text")
502
515
  .setView(container)
503
- .setPositiveButton("Add") { _, _ ->
516
+ .setPositiveButton("Done") { _, _ ->
504
517
  val text = input.text?.toString().orEmpty()
505
518
  if (text.isNotBlank()) {
506
- val layer = PhotoLayer(type = LayerType.TEXT, text = text)
507
- session.layerStack.commit { it + layer }
519
+ val layer = editingLayer?.copy(text = text, textColor = selectedColor)
520
+ ?: PhotoLayer(type = LayerType.TEXT, text = text, textColor = selectedColor)
521
+ session.layerStack.commit { list -> if (editingLayer == null) list + layer else list.map { if (it.id == layer.id) layer else it } }
508
522
  selectLayer(layer.id)
509
523
  }
510
524
  }
511
525
  .setNegativeButton("Cancel", null)
512
- .show()
526
+ .create()
527
+ dialog.setOnShowListener { dialog.window?.setLayout(MATCH, MATCH); input.requestFocus() }
528
+ dialog.show()
513
529
  }
514
530
 
515
531
  private fun setStickersMode(enabled: Boolean, session: PhotoEditSession) {
@@ -519,6 +535,21 @@ class PhotoVideoEditorActivity : Activity() {
519
535
  refreshPhotoToolSelection()
520
536
  }
521
537
 
538
+ private fun showStickerBottomSheet(session: PhotoEditSession) {
539
+ stickersMode = true
540
+ refreshPhotoToolSelection()
541
+ OnlineStickerSheet(this, runtimeStickerAssets()) { path ->
542
+ val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = Uri.fromFile(File(path)).toString())
543
+ session.layerStack.commit { it + layer }
544
+ selectLayer(layer.id)
545
+ }.apply {
546
+ setOnDismissListener {
547
+ stickersMode = false
548
+ refreshPhotoToolSelection()
549
+ }
550
+ }.show()
551
+ }
552
+
522
553
  /** One asset card in the Stickers library grid (see stickers_shapes_library mockup). */
523
554
  private fun createAssetCard(
524
555
  faceView: View,
@@ -605,24 +636,6 @@ class PhotoVideoEditorActivity : Activity() {
605
636
  },
606
637
  LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) }
607
638
  )
608
- val features = request.optJSONObject("features")
609
- if (features?.optBoolean("onlineStickers", true) != false) {
610
- val browseIcon = ImageView(this@PhotoVideoEditorActivity).apply {
611
- setImageResource(R.drawable.ic_photo_filter)
612
- setColorFilter(DesignTokens.tertiary)
613
- }
614
- addView(
615
- createAssetCard(browseIcon, "Browse") {
616
- OnlineStickerSheet(this@PhotoVideoEditorActivity) { path ->
617
- val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = Uri.fromFile(File(path)).toString())
618
- session.layerStack.commit { it + layer }
619
- selectLayer(layer.id)
620
- setStickersMode(false, session)
621
- }.show()
622
- },
623
- LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) }
624
- )
625
- }
626
639
  })
627
640
  }, LinearLayout.LayoutParams(0, MATCH, 1f))
628
641
  addView(
@@ -632,6 +645,69 @@ class PhotoVideoEditorActivity : Activity() {
632
645
  }
633
646
  }
634
647
 
648
+ private fun setDrawMode(enabled: Boolean, session: PhotoEditSession) {
649
+ drawMode = enabled
650
+ drawOverlay.visibility = if (enabled) View.VISIBLE else View.GONE
651
+ drawSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
652
+ mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
653
+ layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
654
+ photoImageView.panZoomEnabled = !enabled
655
+ if (enabled) {
656
+ drawOverlay.strokeColor = Color.RED
657
+ drawOverlay.strokeWidthPx = dp(4).toFloat()
658
+ drawOverlay.setImageBounds(photoImageView.currentImageBounds())
659
+ } else {
660
+ commitDrawStrokes(session)
661
+ drawOverlay.clearStrokes()
662
+ }
663
+ refreshPhotoToolSelection()
664
+ }
665
+
666
+ private fun commitDrawStrokes(session: PhotoEditSession) {
667
+ if (!drawOverlay.hasStrokes) return
668
+ val bounds = photoImageView.currentImageBounds()
669
+ val shortSide = minOf(bounds.width(), bounds.height())
670
+ if (shortSide <= 0f) return
671
+ val newLayers = drawOverlay.normalizedStrokes().mapNotNull { (points, stroke) ->
672
+ if (points.size < 2) return@mapNotNull null
673
+ val centerX = points.sumOf { it.first.toDouble() }.toFloat() / points.size
674
+ val centerY = points.sumOf { it.second.toDouble() }.toFloat() / points.size
675
+ PhotoLayer(
676
+ type = LayerType.DRAWING,
677
+ x = centerX,
678
+ y = centerY,
679
+ drawColor = stroke.color,
680
+ drawStrokeWidth = stroke.widthPx / shortSide,
681
+ drawPoints = points.map { (x, y) -> (x - centerX) to (y - centerY) }
682
+ )
683
+ }
684
+ if (newLayers.isNotEmpty()) session.layerStack.commit { it + newLayers }
685
+ onLayerStackChanged()
686
+ }
687
+
688
+ private fun createDrawSubBar(session: PhotoEditSession): LinearLayout {
689
+ val textColor = themeColor("textColor", DesignTokens.onSurface)
690
+ val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
691
+ val colors = listOf(Color.RED, Color.rgb(255, 214, 51), Color.rgb(76, 217, 100), Color.rgb(10, 132, 255), Color.WHITE, Color.BLACK)
692
+ return LinearLayout(this).apply {
693
+ gravity = Gravity.CENTER_VERTICAL
694
+ setPadding(dp(12), 0, dp(12), 0)
695
+ setBackgroundColor(themeColor("toolbarColor", DesignTokens.surfaceContainerLow))
696
+ addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
697
+ isHorizontalScrollBarEnabled = false
698
+ addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
699
+ orientation = LinearLayout.HORIZONTAL; gravity = Gravity.CENTER_VERTICAL
700
+ colors.forEach { color -> addView(createColorSwatch(color) { drawOverlay.strokeColor = color }, LinearLayout.LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(6) }) }
701
+ addView(actionButton("Thin") { drawOverlay.strokeWidthPx = dp(2).toFloat() }.apply { setTextColor(textColor) })
702
+ addView(actionButton("Thick") { drawOverlay.strokeWidthPx = dp(10).toFloat() }.apply { setTextColor(textColor) })
703
+ addView(actionButton("Undo") { drawOverlay.undoLastStroke() }.apply { setTextColor(textColor) })
704
+ addView(actionButton("Clear") { drawOverlay.clearStrokes() }.apply { setTextColor(textColor) })
705
+ })
706
+ }, LinearLayout.LayoutParams(0, MATCH, 1f))
707
+ addView(actionButton("Done") { setDrawMode(false, session) }.apply { setTextColor(primaryColor) }, LinearLayout.LayoutParams(dp(72), MATCH))
708
+ }
709
+ }
710
+
635
711
  /** Launches the SAF image picker for either the sticker-upload or overlay-upload flow, dispatched in [onActivityResult]. */
636
712
  private fun launchOverlayPicker(requestCode: Int) {
637
713
  val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).setType("image/*").addCategory(Intent.CATEGORY_OPENABLE)
@@ -1105,6 +1181,7 @@ class PhotoVideoEditorActivity : Activity() {
1105
1181
  }
1106
1182
 
1107
1183
  private fun setCropMode(enabled: Boolean, session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
1184
+ if (enabled && !cropMode) cropEntryState = session.state
1108
1185
  cropMode = enabled
1109
1186
  imageView.panZoomEnabled = !enabled
1110
1187
  overlay.visibility = if (enabled) View.VISIBLE else View.GONE
@@ -1117,11 +1194,23 @@ class PhotoVideoEditorActivity : Activity() {
1117
1194
  val state = session.state
1118
1195
  val isIdentityCrop = state.cropLeft == 0f && state.cropTop == 0f && state.cropRight == 1f && state.cropBottom == 1f
1119
1196
  overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = isIdentityCrop)
1197
+ overlay.setCrop(state.cropLeft, state.cropTop, state.cropRight, state.cropBottom)
1198
+ overlay.setAspectRatio(state.aspectRatio)
1199
+ } else {
1200
+ cropEntryState = null
1120
1201
  }
1121
1202
  refreshPhotoToolSelection()
1122
1203
  refreshSelection(cropAspectButtons, session.state.aspectRatio, themeColor("textColor", DesignTokens.onSurface), themeColor("primaryColor", DesignTokens.primaryContainer))
1123
1204
  }
1124
1205
 
1206
+ private fun cancelCropMode(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
1207
+ cropEntryState?.let { snapshot -> session.update { snapshot } }
1208
+ straightenSeekBar.progress = ((cropEntryState?.straightenDegrees ?: 0f) + 45f).roundToInt()
1209
+ cropEntryState = null
1210
+ schedulePhotoPreviewRender()
1211
+ setCropMode(false, session, imageView, overlay)
1212
+ }
1213
+
1125
1214
  private fun createCropSubBar(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView): LinearLayout {
1126
1215
  val toolbarColor = themeColor("toolbarColor", DesignTokens.surfaceContainerLow)
1127
1216
  val textColor = themeColor("textColor", DesignTokens.onSurface)
@@ -1168,6 +1257,10 @@ class PhotoVideoEditorActivity : Activity() {
1168
1257
  addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
1169
1258
  gravity = Gravity.CENTER_VERTICAL
1170
1259
  setPadding(dp(12), 0, dp(12), 0)
1260
+ addView(
1261
+ actionButton("Cancel") { cancelCropMode(session, imageView, overlay) }.apply { setTextColor(textColor) },
1262
+ LinearLayout.LayoutParams(dp(72), MATCH)
1263
+ )
1171
1264
  addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
1172
1265
  isHorizontalScrollBarEnabled = false
1173
1266
  addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
@@ -1266,6 +1359,7 @@ class PhotoVideoEditorActivity : Activity() {
1266
1359
  finishWithError("Nothing to export.", "E_INTERNAL")
1267
1360
  return
1268
1361
  }
1362
+ if (drawMode) setDrawMode(false, session)
1269
1363
  exportProgressView.visibility = View.VISIBLE
1270
1364
  val state = session.state
1271
1365
  val adjustments = session.adjustments
@@ -1379,11 +1473,20 @@ class PhotoVideoEditorActivity : Activity() {
1379
1473
  }
1380
1474
 
1381
1475
  val playPause = Button(this).apply {
1382
- text = "Play"
1383
- setTextColor(Color.WHITE)
1476
+ text = ""
1477
+ contentDescription = "Play video"
1478
+ setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_play, 0, 0, 0)
1479
+ gravity = Gravity.CENTER
1384
1480
  setBackgroundColor(Color.TRANSPARENT)
1385
1481
  setOnClickListener {
1386
- if (session.player.isPlaying) session.player.pause() else session.player.play()
1482
+ if (session.player.isPlaying) {
1483
+ session.player.pause()
1484
+ } else {
1485
+ if (session.player.playbackState == Player.STATE_ENDED || session.globalPositionMs() >= session.durationMs - 50L) {
1486
+ session.seekToGlobalMs(0)
1487
+ }
1488
+ session.player.play()
1489
+ }
1387
1490
  }
1388
1491
  }
1389
1492
  playPauseButton = playPause
@@ -1433,14 +1536,16 @@ class PhotoVideoEditorActivity : Activity() {
1433
1536
  }
1434
1537
  }
1435
1538
  trimRangeView = trim
1436
- root.addView(trim, LinearLayout.LayoutParams(MATCH, dp(48)).apply { setMargins(dp(8), dp(4), dp(8), 0) })
1539
+ // Single-video editor: keep the internal range initialized for export,
1540
+ // but do not show the trim/clip timeline UI.
1437
1541
 
1438
1542
  val strip = LinearLayout(this).apply {
1439
1543
  orientation = LinearLayout.HORIZONTAL
1440
1544
  setPadding(dp(8), dp(4), dp(8), dp(4))
1441
1545
  }
1546
+ // Keep the internal container initialized for the single-clip session,
1547
+ // but do not add the multi-clip strip/actions to the video editor UI.
1442
1548
  clipStripBar = strip
1443
- root.addView(strip, LinearLayout.LayoutParams(MATCH, WRAP))
1444
1549
 
1445
1550
  session.onClipsReady = {
1446
1551
  runOnUiThread {
@@ -1523,7 +1628,9 @@ class PhotoVideoEditorActivity : Activity() {
1523
1628
  positionSeekBar.progress = (session.globalPositionMs() * 1000 / session.durationMs).toInt().coerceIn(0, 1000)
1524
1629
  updateTimeLabel()
1525
1630
  }
1526
- playPauseButton.text = if (session.player.isPlaying) "Pause" else "Play"
1631
+ val playing = session.player.isPlaying
1632
+ playPauseButton.setCompoundDrawablesWithIntrinsicBounds(if (playing) R.drawable.ic_pause else R.drawable.ic_play, 0, 0, 0)
1633
+ playPauseButton.contentDescription = if (playing) "Pause video" else "Play video"
1527
1634
  refreshVideoOverlayPreview(session)
1528
1635
  positionPollHandler.postDelayed(this, 250)
1529
1636
  }
@@ -1630,7 +1737,11 @@ class PhotoVideoEditorActivity : Activity() {
1630
1737
  videoLayerToolBar.visibility = if (visible) View.VISIBLE else View.GONE
1631
1738
  videoLayerPropertySeekBar.visibility = if (visible) View.VISIBLE else View.GONE
1632
1739
  videoToolBar.visibility = if (visible) View.GONE else View.VISIBLE
1633
- if (visible) syncVideoLayerPropertySeekBar(session)
1740
+ if (visible) {
1741
+ if (activeVideoLayerPropertyKey !in setOf("rotation", "opacity")) activeVideoLayerPropertyKey = "rotation"
1742
+ syncVideoLayerPropertySeekBar(session)
1743
+ refreshSelection(videoLayerPropertyButtons, activeVideoLayerPropertyKey, themeColor("textColor", DesignTokens.onSurface), themeColor("primaryColor", DesignTokens.primaryContainer))
1744
+ }
1634
1745
  }
1635
1746
 
1636
1747
  private fun currentSelectedVideoLayer(session: VideoEditSession): PhotoLayer? =
@@ -1678,9 +1789,7 @@ class PhotoVideoEditorActivity : Activity() {
1678
1789
  val toolbarColor = themeColor("toolbarColor", DesignTokens.surfaceContainerLow)
1679
1790
  val textColor = themeColor("textColor", DesignTokens.onSurface)
1680
1791
  val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
1681
- val properties = listOf(
1682
- "scale" to "Scale", "rotation" to "Rotate", "opacity" to "Opacity", "start" to "Start", "end" to "End"
1683
- )
1792
+ val properties = listOf("rotation" to "Rotate", "opacity" to "Opacity")
1684
1793
  return LinearLayout(this).apply {
1685
1794
  gravity = Gravity.CENTER_VERTICAL
1686
1795
  setPadding(dp(12), 0, dp(12), 0)
@@ -1748,13 +1857,8 @@ class PhotoVideoEditorActivity : Activity() {
1748
1857
 
1749
1858
  val features = request.optJSONObject("features")
1750
1859
  val tools = listOf(
1751
- Triple("cover", "Cover", R.drawable.ic_movie),
1752
- Triple("speed", "Speed 1x", R.drawable.ic_speed),
1753
- Triple("crop", "Crop", R.drawable.ic_crop),
1754
- Triple("filters", "Filters", R.drawable.ic_photo_filter),
1755
1860
  Triple("text", "Text", R.drawable.ic_title),
1756
- Triple("stickers", "Stickers", R.drawable.ic_sentiment_satisfied),
1757
- Triple("overlay", "Overlay", R.drawable.ic_wallpaper)
1861
+ Triple("stickers", "Stickers", R.drawable.ic_sentiment_satisfied)
1758
1862
  )
1759
1863
  addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
1760
1864
  isHorizontalScrollBarEnabled = false
@@ -1810,45 +1914,21 @@ class PhotoVideoEditorActivity : Activity() {
1810
1914
  }
1811
1915
 
1812
1916
  private fun showVideoStickerPicker(session: VideoEditSession) {
1813
- val consumerAssets = request.optJSONArray("stickerAssets")
1814
- val builtin = PhotoLayerRenderer.builtinStickerIds()
1815
- val consumerCount = consumerAssets?.length() ?: 0
1816
- val features = request.optJSONObject("features")
1817
- val onlineStickersEnabled = features?.optBoolean("onlineStickers", true) != false
1818
- val labels = (
1819
- builtin.map { PhotoLayerRenderer.glyphFor(it) } +
1820
- (0 until consumerCount).map { i -> consumerAssets?.optJSONObject(i)?.optString("id", "asset$i") ?: "asset$i" } +
1821
- listOf("Upload from device") +
1822
- (if (onlineStickersEnabled) listOf("Browse online") else emptyList())
1823
- ).toTypedArray()
1824
- AlertDialog.Builder(this)
1825
- .setTitle("Add sticker")
1826
- .setItems(labels) { _, index ->
1827
- when {
1828
- index < builtin.size -> {
1829
- val layer = PhotoLayer(type = LayerType.STICKER, stickerId = builtin[index])
1830
- session.layerStack.commit { it + layer }
1831
- onVideoLayerStackChanged()
1832
- selectVideoLayer(layer.id, session)
1833
- }
1834
- index < builtin.size + consumerCount -> {
1835
- val asset = consumerAssets?.optJSONObject(index - builtin.size)
1836
- val uri = asset?.optString("uri").takeUnless { it.isNullOrBlank() } ?: return@setItems
1837
- val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = uri)
1838
- session.layerStack.commit { it + layer }
1839
- onVideoLayerStackChanged()
1840
- selectVideoLayer(layer.id, session)
1841
- }
1842
- index == builtin.size + consumerCount -> launchOverlayPicker(REQUEST_CODE_VIDEO_STICKER_UPLOAD)
1843
- else -> OnlineStickerSheet(this) { path ->
1844
- val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = Uri.fromFile(File(path)).toString())
1845
- session.layerStack.commit { it + layer }
1846
- onVideoLayerStackChanged()
1847
- selectVideoLayer(layer.id, session)
1848
- }.show()
1849
- }
1850
- }
1851
- .show()
1917
+ OnlineStickerSheet(this, runtimeStickerAssets()) { path ->
1918
+ val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = Uri.fromFile(File(path)).toString())
1919
+ session.layerStack.commit { it + layer }
1920
+ onVideoLayerStackChanged()
1921
+ selectVideoLayer(layer.id, session)
1922
+ }.show()
1923
+ }
1924
+
1925
+ private fun runtimeStickerAssets(): List<RuntimeSticker> {
1926
+ val assets = request.optJSONArray("stickerAssets") ?: return emptyList()
1927
+ return (0 until assets.length()).mapNotNull { index ->
1928
+ val asset = assets.optJSONObject(index) ?: return@mapNotNull null
1929
+ val uri = asset.optString("uri").takeIf { it.isNotBlank() } ?: return@mapNotNull null
1930
+ RuntimeSticker(asset.optString("id").takeIf { it.isNotBlank() } ?: "sticker-${index + 1}", uri)
1931
+ }
1852
1932
  }
1853
1933
 
1854
1934
  private fun formatSpeedLabel(speed: Float): String {
@@ -3,7 +3,7 @@ package com.photovideoeditor.photo.render
3
3
  import android.graphics.Color
4
4
  import java.util.UUID
5
5
 
6
- enum class LayerType { TEXT, STICKER, OVERLAY }
6
+ enum class LayerType { TEXT, STICKER, OVERLAY, DRAWING }
7
7
 
8
8
  /**
9
9
  * A single non-destructive overlay layer (text, sticker, or uploaded image
@@ -33,6 +33,10 @@ data class PhotoLayer(
33
33
  // Overlay (uploaded image)
34
34
  var overlayUri: String? = null,
35
35
  var overlayAspectRatio: Float? = null,
36
+ // Drawing (points are normalized offsets from x/y)
37
+ var drawColor: Int = Color.RED,
38
+ var drawStrokeWidth: Float = 0.012f,
39
+ var drawPoints: List<Pair<Float, Float>> = emptyList(),
36
40
  // Timing (video overlays only; ignored for photo layers). endMs <= 0 means "to end of video".
37
41
  var startMs: Long = 0L,
38
42
  var endMs: Long = 0L
@@ -3,6 +3,7 @@ package com.photovideoeditor.photo.render
3
3
  import android.graphics.Bitmap
4
4
  import android.graphics.Canvas
5
5
  import android.graphics.Paint
6
+ import android.graphics.Path
6
7
  import android.graphics.RectF
7
8
  import android.text.TextPaint
8
9
 
@@ -41,6 +42,7 @@ object PhotoLayerRenderer {
41
42
  LayerType.TEXT -> drawText(canvas, layer, geometry.shortSide, alpha)
42
43
  LayerType.STICKER -> drawSticker(canvas, layer, geometry.shortSide, alpha, bitmapResolver)
43
44
  LayerType.OVERLAY -> drawOverlay(canvas, layer, geometry.shortSide, alpha, bitmapResolver)
45
+ LayerType.DRAWING -> drawStroke(canvas, layer, result.width.toFloat(), result.height.toFloat(), geometry.shortSide, alpha)
44
46
  }
45
47
  canvas.restore()
46
48
  }
@@ -105,4 +107,23 @@ object PhotoLayerRenderer {
105
107
  val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { this.alpha = alpha }
106
108
  canvas.drawBitmap(bitmap, null, RectF(-width / 2, -height / 2, width / 2, height / 2), paint)
107
109
  }
110
+
111
+ private fun drawStroke(canvas: Canvas, layer: PhotoLayer, width: Float, height: Float, shortSide: Float, alpha: Int) {
112
+ if (layer.drawPoints.size < 2) return
113
+ val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
114
+ color = layer.drawColor
115
+ this.alpha = alpha
116
+ style = Paint.Style.STROKE
117
+ strokeWidth = layer.drawStrokeWidth * shortSide
118
+ strokeCap = Paint.Cap.ROUND
119
+ strokeJoin = Paint.Join.ROUND
120
+ }
121
+ val path = Path()
122
+ layer.drawPoints.forEachIndexed { index, (dx, dy) ->
123
+ val px = dx * width
124
+ val py = dy * height
125
+ if (index == 0) path.moveTo(px, py) else path.lineTo(px, py)
126
+ }
127
+ canvas.drawPath(path, paint)
128
+ }
108
129
  }
@@ -31,7 +31,7 @@ class CropOverlayView(context: Context) : View(context) {
31
31
  private var lastX = 0f
32
32
  private var lastY = 0f
33
33
 
34
- private enum class Handle { NONE, MOVE, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
34
+ private enum class Handle { NONE, MOVE, TOP, RIGHT, BOTTOM, LEFT, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
35
35
 
36
36
  /** Sets the on-screen image rect this overlay crops against. Resets the crop to full-frame when [resetCrop] is true. */
37
37
  fun setImageBounds(bounds: RectF, resetCrop: Boolean) {
@@ -43,6 +43,18 @@ class CropOverlayView(context: Context) : View(context) {
43
43
  invalidate()
44
44
  }
45
45
 
46
+ /** Restores a normalized crop rectangle, used when reopening or cancelling crop mode. */
47
+ fun setCrop(left: Float, top: Float, right: Float, bottom: Float) {
48
+ if (imageBounds.width() <= 0 || imageBounds.height() <= 0) return
49
+ cropRect = RectF(
50
+ imageBounds.left + left * imageBounds.width(),
51
+ imageBounds.top + top * imageBounds.height(),
52
+ imageBounds.left + right * imageBounds.width(),
53
+ imageBounds.top + bottom * imageBounds.height()
54
+ )
55
+ clampToImageBounds(); invalidate()
56
+ }
57
+
46
58
  fun setAspectRatio(ratio: Float?) {
47
59
  aspectRatio = ratio
48
60
  if (ratio != null && imageBounds.width() > 0 && imageBounds.height() > 0) {
@@ -77,9 +89,13 @@ class CropOverlayView(context: Context) : View(context) {
77
89
  }
78
90
  listOf(
79
91
  cropRect.left to cropRect.top,
92
+ cropRect.centerX() to cropRect.top,
80
93
  cropRect.right to cropRect.top,
94
+ cropRect.right to cropRect.centerY(),
95
+ cropRect.right to cropRect.bottom,
96
+ cropRect.centerX() to cropRect.bottom,
81
97
  cropRect.left to cropRect.bottom,
82
- cropRect.right to cropRect.bottom
98
+ cropRect.left to cropRect.centerY()
83
99
  ).forEach { (x, y) -> canvas.drawCircle(x, y, 8f, handlePaint) }
84
100
  }
85
101
 
@@ -118,6 +134,10 @@ class CropOverlayView(context: Context) : View(context) {
118
134
  near(cropRect.right, cropRect.top) -> Handle.TOP_RIGHT
119
135
  near(cropRect.left, cropRect.bottom) -> Handle.BOTTOM_LEFT
120
136
  near(cropRect.right, cropRect.bottom) -> Handle.BOTTOM_RIGHT
137
+ near(cropRect.centerX(), cropRect.top) -> Handle.TOP
138
+ near(cropRect.right, cropRect.centerY()) -> Handle.RIGHT
139
+ near(cropRect.centerX(), cropRect.bottom) -> Handle.BOTTOM
140
+ near(cropRect.left, cropRect.centerY()) -> Handle.LEFT
121
141
  cropRect.contains(x, y) -> Handle.MOVE
122
142
  else -> Handle.NONE
123
143
  }
@@ -131,6 +151,10 @@ class CropOverlayView(context: Context) : View(context) {
131
151
  Handle.TOP_RIGHT -> { proposed.right += dx; proposed.top += dy }
132
152
  Handle.BOTTOM_LEFT -> { proposed.left += dx; proposed.bottom += dy }
133
153
  Handle.BOTTOM_RIGHT -> { proposed.right += dx; proposed.bottom += dy }
154
+ Handle.TOP -> proposed.top += dy
155
+ Handle.RIGHT -> proposed.right += dx
156
+ Handle.BOTTOM -> proposed.bottom += dy
157
+ Handle.LEFT -> proposed.left += dx
134
158
  Handle.NONE -> return
135
159
  }
136
160
  if (proposed.width() < minSizePx || proposed.height() < minSizePx) return
@@ -139,6 +163,14 @@ class CropOverlayView(context: Context) : View(context) {
139
163
  Handle.TOP_LEFT, Handle.BOTTOM_RIGHT -> proposed.bottom = proposed.top + proposed.width() / ratio
140
164
  Handle.TOP_RIGHT -> proposed.top = proposed.bottom - proposed.width() / ratio
141
165
  Handle.BOTTOM_LEFT -> proposed.bottom = proposed.top + proposed.width() / ratio
166
+ Handle.LEFT, Handle.RIGHT -> {
167
+ val centerY = cropRect.centerY(); val height = proposed.width() / ratio
168
+ proposed.top = centerY - height / 2; proposed.bottom = centerY + height / 2
169
+ }
170
+ Handle.TOP, Handle.BOTTOM -> {
171
+ val centerX = cropRect.centerX(); val width = proposed.height() * ratio
172
+ proposed.left = centerX - width / 2; proposed.right = centerX + width / 2
173
+ }
142
174
  else -> {}
143
175
  }
144
176
  }