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.
- package/CHANGELOG.md +9 -0
- package/PhotoVideoEditor.podspec +1 -0
- package/README.md +10 -6
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
- package/android/src/main/assets/Stickers/LOL_1.png +0 -0
- package/android/src/main/assets/Stickers/LOL_10.png +0 -0
- package/android/src/main/assets/Stickers/LOL_11.png +0 -0
- package/android/src/main/assets/Stickers/LOL_12.png +0 -0
- package/android/src/main/assets/Stickers/LOL_13.png +0 -0
- package/android/src/main/assets/Stickers/LOL_14.png +0 -0
- package/android/src/main/assets/Stickers/LOL_15.png +0 -0
- package/android/src/main/assets/Stickers/LOL_16.png +0 -0
- package/android/src/main/assets/Stickers/LOL_17.png +0 -0
- package/android/src/main/assets/Stickers/LOL_18.png +0 -0
- package/android/src/main/assets/Stickers/LOL_19.png +0 -0
- package/android/src/main/assets/Stickers/LOL_2.png +0 -0
- package/android/src/main/assets/Stickers/LOL_3.png +0 -0
- package/android/src/main/assets/Stickers/LOL_4.png +0 -0
- package/android/src/main/assets/Stickers/LOL_5.png +0 -0
- package/android/src/main/assets/Stickers/LOL_6.png +0 -0
- package/android/src/main/assets/Stickers/LOL_7.png +0 -0
- package/android/src/main/assets/Stickers/LOL_8.png +0 -0
- package/android/src/main/assets/Stickers/LOL_9.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +188 -108
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +21 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +34 -2
- package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +21 -1
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
- package/android/src/main/res/drawable/ic_draw.xml +6 -0
- package/android/src/main/res/drawable/ic_pause.xml +7 -0
- package/android/src/main/res/drawable/ic_play.xml +7 -0
- package/docs/api.md +1 -1
- package/docs/photo-editor.md +5 -6
- package/ios/Editor/PhotoVideoEditorViewController.swift +166 -77
- package/ios/Photo/Rendering/PhotoLayer.swift +5 -1
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +14 -0
- package/ios/Photo/UI/CropOverlayView.swift +38 -1
- package/ios/Photo/UI/DrawOverlayView.swift +48 -0
- package/ios/Photo/UI/LayerOverlayView.swift +9 -1
- package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
- package/ios/Resources/Stickers/LOL_1.png +0 -0
- package/ios/Resources/Stickers/LOL_10.png +0 -0
- package/ios/Resources/Stickers/LOL_11.png +0 -0
- package/ios/Resources/Stickers/LOL_12.png +0 -0
- package/ios/Resources/Stickers/LOL_13.png +0 -0
- package/ios/Resources/Stickers/LOL_14.png +0 -0
- package/ios/Resources/Stickers/LOL_15.png +0 -0
- package/ios/Resources/Stickers/LOL_16.png +0 -0
- package/ios/Resources/Stickers/LOL_17.png +0 -0
- package/ios/Resources/Stickers/LOL_18.png +0 -0
- package/ios/Resources/Stickers/LOL_19.png +0 -0
- package/ios/Resources/Stickers/LOL_2.png +0 -0
- package/ios/Resources/Stickers/LOL_3.png +0 -0
- package/ios/Resources/Stickers/LOL_4.png +0 -0
- package/ios/Resources/Stickers/LOL_5.png +0 -0
- package/ios/Resources/Stickers/LOL_6.png +0 -0
- package/ios/Resources/Stickers/LOL_7.png +0 -0
- package/ios/Resources/Stickers/LOL_8.png +0 -0
- package/ios/Resources/Stickers/LOL_9.png +0 -0
- package/ios/Resources/Stickers/SPRING_1.png +0 -0
- package/ios/Resources/Stickers/SPRING_2.png +0 -0
- package/ios/Resources/Stickers/SPRING_3.png +0 -0
- package/ios/Resources/Stickers/SPRING_4.png +0 -0
- package/ios/Resources/Stickers/SPRING_5.png +0 -0
- package/ios/Resources/Stickers/SPRING_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_1.png +0 -0
- package/ios/Resources/Stickers/SUMMER_10.png +0 -0
- package/ios/Resources/Stickers/SUMMER_11.png +0 -0
- package/ios/Resources/Stickers/SUMMER_12.png +0 -0
- package/ios/Resources/Stickers/SUMMER_13.png +0 -0
- package/ios/Resources/Stickers/SUMMER_14.png +0 -0
- package/ios/Resources/Stickers/SUMMER_15.png +0 -0
- package/ios/Resources/Stickers/SUMMER_16.png +0 -0
- package/ios/Resources/Stickers/SUMMER_17.png +0 -0
- package/ios/Resources/Stickers/SUMMER_18.png +0 -0
- package/ios/Resources/Stickers/SUMMER_19.png +0 -0
- package/ios/Resources/Stickers/SUMMER_2.png +0 -0
- package/ios/Resources/Stickers/SUMMER_20.png +0 -0
- package/ios/Resources/Stickers/SUMMER_21.png +0 -0
- package/ios/Resources/Stickers/SUMMER_3.png +0 -0
- package/ios/Resources/Stickers/SUMMER_4.png +0 -0
- package/ios/Resources/Stickers/SUMMER_5.png +0 -0
- package/ios/Resources/Stickers/SUMMER_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_7.png +0 -0
- package/ios/Resources/Stickers/SUMMER_8.png +0 -0
- package/ios/Resources/Stickers/SUMMER_9.png +0 -0
- package/ios/Resources/Stickers/TEXT_1.png +0 -0
- package/ios/Resources/Stickers/TEXT_12.png +0 -0
- package/ios/Resources/Stickers/TEXT_13.png +0 -0
- package/ios/Resources/Stickers/TEXT_14.png +0 -0
- package/ios/Resources/Stickers/TEXT_15.png +0 -0
- package/ios/Resources/Stickers/TEXT_16.png +0 -0
- package/ios/Resources/Stickers/TEXT_2.png +0 -0
- package/ios/Resources/Stickers/TEXT_25.png +0 -0
- package/ios/Resources/Stickers/TEXT_26.png +0 -0
- package/ios/Resources/Stickers/TEXT_27.png +0 -0
- package/ios/Resources/Stickers/TEXT_28.png +0 -0
- package/ios/Resources/Stickers/TEXT_29.png +0 -0
- package/ios/Resources/Stickers/TEXT_3.png +0 -0
- package/ios/Resources/Stickers/TEXT_30.png +0 -0
- package/ios/Resources/Stickers/TEXT_31.png +0 -0
- package/ios/Resources/Stickers/TEXT_32.png +0 -0
- package/ios/Resources/Stickers/TEXT_7.png +0 -0
- package/ios/Resources/Stickers/TEXT_8.png +0 -0
- package/ios/Resources/Stickers/TEXT_9.png +0 -0
- package/ios/Resources/Stickers/a_pety_icon.png +0 -0
- package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/types.ts +2 -2
- package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
- package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
- package/ios/Stickers/OpenMojiCatalog.swift +0 -188
- 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("
|
|
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" ->
|
|
469
|
-
"
|
|
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(
|
|
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
|
|
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,
|
|
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("
|
|
516
|
+
.setPositiveButton("Done") { _, _ ->
|
|
504
517
|
val text = input.text?.toString().orEmpty()
|
|
505
518
|
if (text.isNotBlank()) {
|
|
506
|
-
val layer =
|
|
507
|
-
|
|
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
|
-
.
|
|
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 = "
|
|
1383
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
.
|
|
1826
|
-
.
|
|
1827
|
-
|
|
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.
|
|
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
|
}
|