react-native-photo-video-editor 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +760 -590
- package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +8 -23
- package/android/src/main/java/com/photovideoeditor/photo/render/CropGeometry.kt +71 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +36 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +5 -7
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +71 -41
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +3 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformRenderer.kt +33 -0
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +6 -1
- package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +109 -151
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +42 -13
- package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/CropPanel.kt +122 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorPanel.kt +250 -0
- package/android/src/main/java/com/photovideoeditor/ui/components/EditorToolSlider.kt +103 -0
- package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +38 -11
- package/android/src/main/java/com/photovideoeditor/video/render/VideoCropEffects.kt +35 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +1 -0
- package/docs/editor-ui-validation.md +22 -0
- package/ios/Editor/PhotoVideoEditorViewController.swift +778 -443
- package/ios/Editor/UI/CropPanel.swift +97 -0
- package/ios/Editor/UI/EditorPanel.swift +334 -0
- package/ios/Editor/UI/EditorToolSlider.swift +22 -0
- package/ios/Editor/UI/TextEditorSheet.swift +84 -0
- package/ios/Photo/Export/PhotoExporter.swift +3 -3
- package/ios/Photo/Rendering/CropGeometry.swift +48 -0
- package/ios/Photo/Rendering/PhotoEditSession.swift +21 -10
- package/ios/Photo/Rendering/PhotoFilterPresets.swift +53 -23
- package/ios/Photo/Rendering/PhotoLayer.swift +9 -1
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +2 -1
- package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
- package/ios/Photo/UI/CropOverlayView.swift +96 -169
- package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
- package/ios/Photo/UI/LayerOverlayView.swift +27 -10
- package/ios/Photo/UI/ZoomableImageView.swift +19 -1
- package/ios/Video/Export/VideoExporter.swift +10 -1
- package/ios/Video/Render/VideoCropGeometry.swift +48 -0
- package/ios/Video/Render/VideoTransformState.swift +1 -0
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ import android.content.Intent
|
|
|
7
7
|
import android.graphics.Bitmap
|
|
8
8
|
import android.graphics.BitmapFactory
|
|
9
9
|
import android.graphics.Color
|
|
10
|
+
import android.graphics.Typeface
|
|
10
11
|
import android.net.Uri
|
|
11
12
|
import android.os.Bundle
|
|
12
13
|
import android.text.InputType
|
|
@@ -33,6 +34,7 @@ import androidx.media3.ui.PlayerView
|
|
|
33
34
|
import com.photovideoeditor.files.SourceResolver
|
|
34
35
|
import com.photovideoeditor.photo.export.PhotoExportException
|
|
35
36
|
import com.photovideoeditor.photo.export.PhotoExporter
|
|
37
|
+
import com.photovideoeditor.photo.render.OverlayGeometry
|
|
36
38
|
import com.photovideoeditor.photo.render.LayerType
|
|
37
39
|
import com.photovideoeditor.photo.render.PhotoAdjustments
|
|
38
40
|
import com.photovideoeditor.photo.render.PhotoEditSession
|
|
@@ -55,9 +57,18 @@ import com.photovideoeditor.photo.ui.ZoomableImageView
|
|
|
55
57
|
import com.photovideoeditor.stickers.OnlineStickerSheet
|
|
56
58
|
import com.photovideoeditor.stickers.RuntimeSticker
|
|
57
59
|
import com.photovideoeditor.ui.DesignTokens
|
|
60
|
+
import com.photovideoeditor.ui.components.CropPanel
|
|
61
|
+
import com.photovideoeditor.ui.components.EditorFilterThumbnail
|
|
62
|
+
import com.photovideoeditor.ui.components.EditorPanelMetrics
|
|
63
|
+
import com.photovideoeditor.ui.components.EditorStripItem
|
|
64
|
+
import com.photovideoeditor.ui.components.EditorToolSlider
|
|
58
65
|
import com.photovideoeditor.ui.components.applyPressScale
|
|
59
66
|
import com.photovideoeditor.ui.components.editorChip
|
|
67
|
+
import com.photovideoeditor.ui.components.editorFilterThumbnail
|
|
68
|
+
import com.photovideoeditor.ui.components.editorPanelHeader
|
|
60
69
|
import com.photovideoeditor.ui.components.editorPrimaryButton
|
|
70
|
+
import com.photovideoeditor.ui.components.editorStripItem
|
|
71
|
+
import com.photovideoeditor.ui.components.editorToolStrip
|
|
61
72
|
import org.json.JSONObject
|
|
62
73
|
import java.io.File
|
|
63
74
|
import kotlin.math.roundToInt
|
|
@@ -71,6 +82,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
71
82
|
private var photoSession: PhotoEditSession? = null
|
|
72
83
|
private var cropMode = false
|
|
73
84
|
private var cropEntryState: PhotoTransformState? = null
|
|
85
|
+
private var photoCropPanel: CropPanel? = null
|
|
74
86
|
private var adjustMode = false
|
|
75
87
|
private var filtersMode = false
|
|
76
88
|
private var drawMode = false
|
|
@@ -78,7 +90,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
78
90
|
private lateinit var stickersSubBar: View
|
|
79
91
|
private var activeAdjustmentKey = "brightness"
|
|
80
92
|
private var selectedLayerId: String? = null
|
|
81
|
-
private var activeLayerPropertyKey = "
|
|
93
|
+
private var activeLayerPropertyKey = ""
|
|
82
94
|
private var dragStartSnapshot: List<PhotoLayer>? = null
|
|
83
95
|
private lateinit var photoImageView: ZoomableImageView
|
|
84
96
|
private lateinit var cropOverlay: CropOverlayView
|
|
@@ -86,7 +98,10 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
86
98
|
private lateinit var drawOverlay: DrawOverlayView
|
|
87
99
|
private lateinit var straightenSeekBar: SeekBar
|
|
88
100
|
private lateinit var cropSubBar: View
|
|
89
|
-
|
|
101
|
+
/** Drives whichever adjustment is selected in the Adjust panel. */
|
|
102
|
+
private lateinit var adjustmentSeekBar: EditorToolSlider
|
|
103
|
+
/** Filter strength, owned by the Filters panel (a View can only have one parent, so not shared). */
|
|
104
|
+
private lateinit var filterIntensitySeekBar: EditorToolSlider
|
|
90
105
|
private lateinit var filtersSubBar: View
|
|
91
106
|
private lateinit var adjustSubBar: View
|
|
92
107
|
private lateinit var drawSubBar: View
|
|
@@ -104,9 +119,14 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
104
119
|
private var videoSession: VideoEditSession? = null
|
|
105
120
|
private var videoExporter: VideoExporter? = null
|
|
106
121
|
private var videoAspectMode = false
|
|
122
|
+
private var videoCropMode = false
|
|
123
|
+
private var videoCropEntryState: PhotoTransformState? = null
|
|
124
|
+
private var videoCropPanel: CropPanel? = null
|
|
125
|
+
private lateinit var videoCropOverlay: CropOverlayView
|
|
126
|
+
private lateinit var cropPlayerView: PlayerView
|
|
107
127
|
private lateinit var trimRangeView: TrimRangeView
|
|
108
128
|
private lateinit var positionSeekBar: SeekBar
|
|
109
|
-
private lateinit var playPauseButton:
|
|
129
|
+
private lateinit var playPauseButton: ImageButton
|
|
110
130
|
private lateinit var timeLabel: TextView
|
|
111
131
|
private lateinit var videoAspectSubBar: View
|
|
112
132
|
private lateinit var videoToolBar: View
|
|
@@ -118,20 +138,22 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
118
138
|
private val positionPollHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
119
139
|
private var positionPollRunnable: Runnable? = null
|
|
120
140
|
private var seeking = false
|
|
141
|
+
private lateinit var videoControlsOverlay: View
|
|
142
|
+
private var videoControlsVisible = true
|
|
143
|
+
private val hideVideoControlsRunnable = Runnable { setVideoControlsVisible(false) }
|
|
121
144
|
|
|
122
145
|
private val photoToolButtons = mutableMapOf<String, ToolNode>()
|
|
123
146
|
private var photoToolScroll: HorizontalScrollView? = null
|
|
124
147
|
private val cropAspectButtons = mutableMapOf<Float?, Button>()
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
148
|
+
/** Adjustment key (plus "mirror") -> its entry in the Adjust panel's strip. */
|
|
149
|
+
private val adjustmentStripItems = mutableMapOf<String, EditorStripItem>()
|
|
150
|
+
/** Preset id ("" = Original) -> its thumbnail in the Filters carousel. */
|
|
151
|
+
private val filterPresetCards = mutableMapOf<String, EditorFilterThumbnail>()
|
|
128
152
|
private var resizeMode = false
|
|
129
153
|
private lateinit var resizeSubBar: View
|
|
130
154
|
private var resizePresetSize: Pair<Int, Int>? = null
|
|
131
155
|
private val resizePresetButtons = mutableMapOf<Pair<Int, Int>?, Button>()
|
|
132
|
-
private val layerPropertyButtons = mutableMapOf<String,
|
|
133
|
-
/** Non-Delete/Duplicate layer action icons, hidden for TEXT layers (only Opacity/Colors/Duplicate/Delete apply there). */
|
|
134
|
-
private val layerIconButtons = mutableMapOf<String, View>()
|
|
156
|
+
private val layerPropertyButtons = mutableMapOf<String, ToolNode>()
|
|
135
157
|
private val videoToolButtons = mutableMapOf<String, ToolNode>()
|
|
136
158
|
private val videoAspectButtons = mutableMapOf<Float?, Button>()
|
|
137
159
|
private val videoFilterButtons = mutableMapOf<String, Button>()
|
|
@@ -143,9 +165,12 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
143
165
|
private lateinit var videoLayerToolBar: View
|
|
144
166
|
private lateinit var videoLayerPropertySeekBar: SeekBar
|
|
145
167
|
private var selectedVideoLayerId: String? = null
|
|
146
|
-
private var activeVideoLayerPropertyKey = "
|
|
168
|
+
private var activeVideoLayerPropertyKey = ""
|
|
147
169
|
private var videoDragStartSnapshot: List<PhotoLayer>? = null
|
|
148
|
-
private val videoLayerPropertyButtons = mutableMapOf<String,
|
|
170
|
+
private val videoLayerPropertyButtons = mutableMapOf<String, ToolNode>()
|
|
171
|
+
private lateinit var videoColorRow: View
|
|
172
|
+
private val textSwatches = mutableMapOf<Int, View>()
|
|
173
|
+
private val videoTextSwatches = mutableMapOf<Int, View>()
|
|
149
174
|
private var selectedClipIndex = 0
|
|
150
175
|
private lateinit var clipStripBar: LinearLayout
|
|
151
176
|
private var timelineThumbnailRepository: TimelineThumbnailRepository? = null
|
|
@@ -234,9 +259,13 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
234
259
|
}
|
|
235
260
|
photoImageView = imageView
|
|
236
261
|
photoPreviewRenderer = PhotoPreviewRenderCoordinator(
|
|
237
|
-
render = { session.renderPreview() },
|
|
262
|
+
render = { session.renderPreview(cropMode || resizeMode) },
|
|
238
263
|
display = { bitmap ->
|
|
239
|
-
if (!comparingOriginal && !isFinishing && !isDestroyed)
|
|
264
|
+
if (!comparingOriginal && !isFinishing && !isDestroyed) {
|
|
265
|
+
imageView.setImageBitmap(bitmap)
|
|
266
|
+
imageView.resetToFit()
|
|
267
|
+
if (cropMode && ::cropOverlay.isInitialized) cropOverlay.restore(imageView.currentImageBounds(), session.state)
|
|
268
|
+
}
|
|
240
269
|
},
|
|
241
270
|
)
|
|
242
271
|
previewContainer.addView(imageView, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
@@ -254,11 +283,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
254
283
|
previewContainer.addView(drawing, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
255
284
|
|
|
256
285
|
imageView.onBoundsChanged = { bounds ->
|
|
257
|
-
if (cropMode) overlay.
|
|
286
|
+
if (cropMode) overlay.restore(bounds, session.state)
|
|
258
287
|
layers.setImageBounds(bounds)
|
|
259
288
|
drawing.setImageBounds(bounds)
|
|
260
289
|
}
|
|
261
290
|
overlay.onCropChanged = { left, top, right, bottom -> session.update { it.withCrop(left, top, right, bottom) } }
|
|
291
|
+
overlay.onMediaBoundsChanged = { bounds -> imageView.setRenderedBounds(bounds) }
|
|
292
|
+
overlay.onViewportChanged = { zoom, panX, panY -> session.update { it.copy(zoom = zoom, panX = panX, panY = panY) } }
|
|
293
|
+
layers.onLayerDelete = { id ->
|
|
294
|
+
session.layerStack.commit { list -> list.filterNot { it.id == id } }
|
|
295
|
+
selectLayer(null); onLayerStackChanged()
|
|
296
|
+
}
|
|
262
297
|
layers.onLayerTapped = { id -> selectLayer(id) }
|
|
263
298
|
layers.onLayerDoubleTapped = { id ->
|
|
264
299
|
session.layerStack.layers.firstOrNull { it.id == id && it.type == LayerType.TEXT }?.let { showTextInputDialog(session, it) }
|
|
@@ -298,10 +333,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
298
333
|
})
|
|
299
334
|
}
|
|
300
335
|
straightenSeekBar = straighten
|
|
301
|
-
|
|
302
|
-
straighten,
|
|
303
|
-
LinearLayout.LayoutParams(MATCH, WRAP).apply { setMargins(dp(16), dp(4), dp(16), 0) }
|
|
304
|
-
)
|
|
336
|
+
|
|
305
337
|
|
|
306
338
|
val cropBar = createCropSubBar(session, imageView, overlay)
|
|
307
339
|
cropSubBar = cropBar
|
|
@@ -313,22 +345,35 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
313
345
|
resizeBar.visibility = View.GONE
|
|
314
346
|
root.addView(resizeBar, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
315
347
|
|
|
316
|
-
val
|
|
317
|
-
|
|
348
|
+
val accentColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
349
|
+
adjustmentSeekBar = EditorToolSlider(this).apply {
|
|
350
|
+
applyCompactTrack(accentColor)
|
|
318
351
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
319
352
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
320
353
|
if (!fromUser) return
|
|
321
354
|
val (min, _) = PhotoAdjustments.rangeFor(activeAdjustmentKey)
|
|
322
|
-
|
|
323
|
-
|
|
355
|
+
session.updateAdjustments { it.with(activeAdjustmentKey, min + progress) }
|
|
356
|
+
// Coalesced by the preview renderer, so dragging does not queue a render per pixel.
|
|
357
|
+
schedulePhotoPreviewRender()
|
|
358
|
+
}
|
|
359
|
+
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
360
|
+
override fun onStopTrackingTouch(seekBar: SeekBar?) { refreshAdjustSelection(session) }
|
|
361
|
+
})
|
|
362
|
+
}
|
|
363
|
+
filterIntensitySeekBar = EditorToolSlider(this).apply {
|
|
364
|
+
applyCompactTrack(accentColor)
|
|
365
|
+
labelText = "Intensity"
|
|
366
|
+
valueFormatter = { progress -> "$progress" }
|
|
367
|
+
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
368
|
+
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
369
|
+
if (!fromUser) return
|
|
370
|
+
session.updateAdjustments { it.copy(filterStrength = progress.toFloat()) }
|
|
324
371
|
schedulePhotoPreviewRender()
|
|
325
372
|
}
|
|
326
373
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
327
374
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
328
375
|
})
|
|
329
376
|
}
|
|
330
|
-
adjustmentSeekBar = adjustmentSeek
|
|
331
|
-
root.addView(adjustmentSeek, LinearLayout.LayoutParams(MATCH, WRAP).apply { setMargins(dp(16), dp(4), dp(16), 0) })
|
|
332
377
|
|
|
333
378
|
val adjustBar = createAdjustSubBar(session, imageView)
|
|
334
379
|
adjustSubBar = adjustBar
|
|
@@ -350,15 +395,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
350
395
|
drawBar.visibility = View.GONE
|
|
351
396
|
root.addView(drawBar, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
352
397
|
|
|
353
|
-
val layerPropertySeek =
|
|
398
|
+
val layerPropertySeek = EditorToolSlider(this).apply {
|
|
399
|
+
progressTintList = android.content.res.ColorStateList.valueOf(themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
400
|
+
thumbTintList = progressTintList
|
|
354
401
|
visibility = View.GONE
|
|
355
402
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
356
403
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
357
404
|
if (!fromUser) return
|
|
358
405
|
onLayerPropertyChanged(progress)
|
|
359
406
|
}
|
|
360
|
-
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
361
|
-
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
407
|
+
override fun onStartTrackingTouch(seekBar: SeekBar?) { dragStartSnapshot = session.layerStack.layers.map { it.copy() } }
|
|
408
|
+
override fun onStopTrackingTouch(seekBar: SeekBar?) { dragStartSnapshot?.let { session.layerStack.commitSnapshot(it) }; dragStartSnapshot = null; onLayerStackChanged() }
|
|
362
409
|
})
|
|
363
410
|
}
|
|
364
411
|
layerPropertySeekBar = layerPropertySeek
|
|
@@ -372,7 +419,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
372
419
|
val layerBar = createLayerToolBar(session)
|
|
373
420
|
layerToolBar = layerBar
|
|
374
421
|
layerBar.visibility = View.GONE
|
|
375
|
-
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(
|
|
422
|
+
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(76)))
|
|
376
423
|
|
|
377
424
|
val toolbarHeight = dp(76)
|
|
378
425
|
val toolbar = createPhotoToolBar(session, imageView, overlay)
|
|
@@ -390,7 +437,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
390
437
|
filtersBar to dp(FILTERS_SUB_BAR_HEIGHT_DP),
|
|
391
438
|
stickersBar to dp(ASSET_GRID_BAR_HEIGHT_DP),
|
|
392
439
|
drawBar to dp(56),
|
|
393
|
-
layerBar to dp(
|
|
440
|
+
layerBar to dp(76)
|
|
394
441
|
)
|
|
395
442
|
)
|
|
396
443
|
return outer
|
|
@@ -404,39 +451,57 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
404
451
|
Triple("text", "Text", R.drawable.ic_title),
|
|
405
452
|
Triple("stickers", "Stickers", R.drawable.ic_sentiment_satisfied),
|
|
406
453
|
Triple("draw", "Draw", R.drawable.ic_draw),
|
|
407
|
-
Triple("resize", "Resize", R.drawable.ic_aspect_ratio)
|
|
454
|
+
Triple("resize", "Resize", R.drawable.ic_aspect_ratio),
|
|
455
|
+
Triple("compare", "Original", R.drawable.ic_compare)
|
|
408
456
|
)
|
|
409
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Main tool dock. All eight tools stay available; rather than compressing them onto one screen
|
|
460
|
+
* (tiny icons, wrapped labels), items are sized so roughly five fill the width and the rest are a
|
|
461
|
+
* scroll away.
|
|
462
|
+
*/
|
|
410
463
|
private fun createPhotoToolBar(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView): LinearLayout {
|
|
411
464
|
val backgroundColor = themeColor("toolbarColor", DesignTokens.withAlphaPercent(DesignTokens.surfaceContainerLowest, 95))
|
|
465
|
+
val itemWidth = photoToolItemWidth()
|
|
412
466
|
return LinearLayout(this).apply {
|
|
413
467
|
orientation = LinearLayout.VERTICAL
|
|
414
468
|
setBackgroundColor(backgroundColor)
|
|
415
|
-
setPadding(dp(DesignTokens.spaceXs), dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceLg))
|
|
416
|
-
|
|
417
|
-
addView(
|
|
418
|
-
View(this@PhotoVideoEditorActivity).apply { background = roundedDrawable(DesignTokens.surfaceVariant, DesignTokens.radiusSm) },
|
|
419
|
-
LinearLayout.LayoutParams(dp(36), dp(4)).apply { gravity = Gravity.CENTER_HORIZONTAL; bottomMargin = dp(DesignTokens.spaceXs) }
|
|
420
|
-
)
|
|
421
469
|
|
|
422
470
|
val features = request.optJSONObject("features")
|
|
423
471
|
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
424
472
|
isHorizontalScrollBarEnabled = false
|
|
473
|
+
clipToPadding = false
|
|
425
474
|
photoToolScroll = this
|
|
426
475
|
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
427
476
|
orientation = LinearLayout.HORIZONTAL
|
|
428
477
|
gravity = Gravity.CENTER_VERTICAL
|
|
429
|
-
setPadding(dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs))
|
|
430
478
|
photoToolDefinitions.filter { features?.optBoolean(it.first, true) != false }.forEach { (key, label, iconRes) ->
|
|
431
479
|
val node = createToolNode(iconRes, label) { onPhotoToolTapped(key, session, imageView, overlay) }
|
|
480
|
+
if (key == "compare") {
|
|
481
|
+
node.root.contentDescription = "Hold to compare with original"
|
|
482
|
+
node.root.setOnTouchListener { _, event ->
|
|
483
|
+
when (event.actionMasked) {
|
|
484
|
+
MotionEvent.ACTION_DOWN -> showOriginalPreview(true)
|
|
485
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> showOriginalPreview(false)
|
|
486
|
+
}
|
|
487
|
+
true
|
|
488
|
+
}
|
|
489
|
+
}
|
|
432
490
|
photoToolButtons[key] = node
|
|
433
|
-
addView(node.root, LinearLayout.LayoutParams(
|
|
491
|
+
addView(node.root, LinearLayout.LayoutParams(itemWidth, WRAP))
|
|
434
492
|
}
|
|
435
493
|
})
|
|
436
494
|
}, LinearLayout.LayoutParams(MATCH, WRAP))
|
|
437
495
|
}
|
|
438
496
|
}
|
|
439
497
|
|
|
498
|
+
/**
|
|
499
|
+
* Width that shows about five tools at once, so the dock fills the screen evenly on a small phone
|
|
500
|
+
* and does not leave 40dp gaps on a large one. Clamped so labels stay on one line either way.
|
|
501
|
+
*/
|
|
502
|
+
private fun photoToolItemWidth(): Int =
|
|
503
|
+
(resources.displayMetrics.widthPixels / 5).coerceIn(dp(64), dp(88))
|
|
504
|
+
|
|
440
505
|
private fun refreshPhotoToolSelection() {
|
|
441
506
|
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
442
507
|
photoToolButtons.forEach { (key, node) ->
|
|
@@ -468,7 +533,9 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
468
533
|
}
|
|
469
534
|
|
|
470
535
|
private fun onPhotoToolTapped(key: String, session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
536
|
+
if (key != "compare" && comparingOriginal) showOriginalPreview(false)
|
|
471
537
|
when (key) {
|
|
538
|
+
"compare" -> showOriginalPreview(!comparingOriginal)
|
|
472
539
|
"crop" -> setCropMode(true, session, imageView, overlay)
|
|
473
540
|
"adjust" -> setAdjustMode(true, session)
|
|
474
541
|
"filters" -> setFiltersMode(true, session)
|
|
@@ -486,60 +553,92 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
486
553
|
// ---------------------------------------------------------------------
|
|
487
554
|
|
|
488
555
|
private fun showTextInputDialog(session: PhotoEditSession, editingLayer: PhotoLayer? = null) {
|
|
489
|
-
|
|
556
|
+
showTextEditor(editingLayer) { text ->
|
|
557
|
+
val layer = editingLayer?.copy(text = text) ?: PhotoLayer(type = LayerType.TEXT, text = text)
|
|
558
|
+
session.layerStack.commit { list -> if (editingLayer == null) list + layer else list.map { if (it.id == layer.id) layer else it } }
|
|
559
|
+
selectLayer(layer.id)
|
|
560
|
+
onLayerStackChanged()
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
private fun showTextEditor(editingLayer: PhotoLayer?, onSave: (String) -> Unit) {
|
|
565
|
+
val dialog = android.app.Dialog(this)
|
|
566
|
+
dialog.requestWindowFeature(android.view.Window.FEATURE_NO_TITLE)
|
|
567
|
+
val panel = LinearLayout(this).apply {
|
|
568
|
+
orientation = LinearLayout.VERTICAL
|
|
569
|
+
setPadding(dp(20), dp(16), dp(20), dp(20))
|
|
570
|
+
background = roundedDrawable(DesignTokens.surfaceContainerHigh, 16)
|
|
571
|
+
}
|
|
572
|
+
panel.addView(TextView(this).apply {
|
|
573
|
+
text = if (editingLayer == null) "Add Text" else "Edit Text"
|
|
574
|
+
textSize = 20f
|
|
575
|
+
setTextColor(Color.WHITE)
|
|
576
|
+
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
|
577
|
+
})
|
|
490
578
|
val input = EditText(this).apply {
|
|
491
|
-
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
492
|
-
setTextColor(selectedColor)
|
|
493
|
-
setHintTextColor(DesignTokens.outline)
|
|
579
|
+
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
494
580
|
hint = "Type something..."
|
|
581
|
+
setHintTextColor(DesignTokens.outline)
|
|
582
|
+
setTextColor(Color.WHITE)
|
|
583
|
+
textSize = 22f
|
|
584
|
+
gravity = Gravity.TOP or Gravity.START
|
|
585
|
+
minLines = 2; maxLines = 6
|
|
586
|
+
setPadding(dp(12), dp(12), dp(12), dp(12))
|
|
587
|
+
background = roundedDrawable(DesignTokens.surfaceContainerLow, 12)
|
|
495
588
|
setText(editingLayer?.text.orEmpty())
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
}
|
|
499
|
-
val
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
589
|
+
setSelection(text.length)
|
|
590
|
+
}
|
|
591
|
+
panel.addView(input, LinearLayout.LayoutParams(MATCH, WRAP).apply { topMargin = dp(16); bottomMargin = dp(12) })
|
|
592
|
+
val actions = LinearLayout(this).apply { gravity = Gravity.END or Gravity.CENTER_VERTICAL }
|
|
593
|
+
actions.addView(actionButton("Cancel") { dialog.dismiss() }, LinearLayout.LayoutParams(dp(88), dp(48)))
|
|
594
|
+
actions.addView(editorPrimaryButton(this, if (editingLayer == null) "Add" else "Done", fillColor = themeColor("primaryColor", DesignTokens.primaryContainer)) {
|
|
595
|
+
val text = input.text.toString()
|
|
596
|
+
if (text.isNotBlank()) { onSave(text); dialog.dismiss() }
|
|
597
|
+
}, LinearLayout.LayoutParams(dp(88), dp(48)))
|
|
598
|
+
panel.addView(actions, LinearLayout.LayoutParams(MATCH, WRAP))
|
|
599
|
+
val scroll = android.widget.ScrollView(this).apply { isFillViewport = true; addView(panel) }
|
|
600
|
+
dialog.setContentView(scroll)
|
|
601
|
+
dialog.window?.apply {
|
|
602
|
+
setBackgroundDrawableResource(android.R.color.transparent)
|
|
603
|
+
addFlags(android.view.WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
604
|
+
setDimAmount(0.7f)
|
|
605
|
+
setSoftInputMode(android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE)
|
|
606
|
+
}
|
|
607
|
+
dialog.setOnShowListener {
|
|
608
|
+
dialog.window?.setLayout(MATCH, WRAP)
|
|
609
|
+
dialog.window?.setGravity(Gravity.BOTTOM)
|
|
610
|
+
input.requestFocus()
|
|
611
|
+
input.post { (getSystemService(INPUT_METHOD_SERVICE) as android.view.inputmethod.InputMethodManager).showSoftInput(input, android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT) }
|
|
506
612
|
}
|
|
507
|
-
val container = LinearLayout(this).apply {
|
|
508
|
-
orientation = LinearLayout.VERTICAL
|
|
509
|
-
background = roundedDrawable(DesignTokens.surfaceContainerHigh, DesignTokens.radiusLg)
|
|
510
|
-
addView(input, LinearLayout.LayoutParams(MATCH, dp(220)))
|
|
511
|
-
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply { addView(colorRow) }, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
512
|
-
}
|
|
513
|
-
val dialog = AlertDialog.Builder(this, android.R.style.Theme_Material_Dialog_Alert)
|
|
514
|
-
.setTitle(if (editingLayer == null) "Add text" else "Edit text")
|
|
515
|
-
.setView(container)
|
|
516
|
-
.setPositiveButton("Done") { _, _ ->
|
|
517
|
-
val text = input.text?.toString().orEmpty()
|
|
518
|
-
if (text.isNotBlank()) {
|
|
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 } }
|
|
522
|
-
selectLayer(layer.id)
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
.setNegativeButton("Cancel", null)
|
|
526
|
-
.create()
|
|
527
|
-
dialog.setOnShowListener { dialog.window?.setLayout(MATCH, MATCH); input.requestFocus() }
|
|
528
613
|
dialog.show()
|
|
529
614
|
}
|
|
530
615
|
|
|
531
616
|
private fun setStickersMode(enabled: Boolean, session: PhotoEditSession) {
|
|
532
617
|
stickersMode = enabled
|
|
533
618
|
stickersSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
534
|
-
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
619
|
+
mainToolBar.visibility = if (enabled || selectedLayerId != null) View.GONE else View.VISIBLE
|
|
535
620
|
refreshPhotoToolSelection()
|
|
536
621
|
}
|
|
537
622
|
|
|
623
|
+
private fun newSticker(stickerId: String? = null, stickerUri: String? = null): PhotoLayer {
|
|
624
|
+
val bounds = if (mediaType == "photo") photoImageView.currentImageBounds()
|
|
625
|
+
else videoSession?.let { computeVideoLetterboxBounds(it, videoOverlayImageView) }
|
|
626
|
+
val scale = bounds?.takeIf { it.width() > 0 && it.height() > 0 }?.let {
|
|
627
|
+
OverlayGeometry.initialStickerScale(it.width(), it.height())
|
|
628
|
+
} ?: (0.3f / 0.18f)
|
|
629
|
+
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
|
630
|
+
stickerUri?.let { uri ->
|
|
631
|
+
SourceResolver.resolvePath(this, uri, "pve_sticker_bounds")?.let { BitmapFactory.decodeFile(it, options) }
|
|
632
|
+
}
|
|
633
|
+
val aspect = if (options.outWidth > 0 && options.outHeight > 0) options.outWidth.toFloat() / options.outHeight else 1f
|
|
634
|
+
return PhotoLayer(type = LayerType.STICKER, stickerId = stickerId, stickerUri = stickerUri, scale = scale, overlayAspectRatio = aspect)
|
|
635
|
+
}
|
|
636
|
+
|
|
538
637
|
private fun showStickerBottomSheet(session: PhotoEditSession) {
|
|
539
638
|
stickersMode = true
|
|
540
639
|
refreshPhotoToolSelection()
|
|
541
640
|
OnlineStickerSheet(this, runtimeStickerAssets()) { path ->
|
|
542
|
-
val layer =
|
|
641
|
+
val layer = newSticker(stickerUri = Uri.fromFile(File(path)).toString())
|
|
543
642
|
session.layerStack.commit { it + layer }
|
|
544
643
|
selectLayer(layer.id)
|
|
545
644
|
}.apply {
|
|
@@ -598,7 +697,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
598
697
|
val emoji = TextView(this@PhotoVideoEditorActivity).apply { text = PhotoLayerRenderer.glyphFor(id); textSize = 26f }
|
|
599
698
|
addView(
|
|
600
699
|
createAssetCard(emoji, null) {
|
|
601
|
-
val layer =
|
|
700
|
+
val layer = newSticker(stickerId = id)
|
|
602
701
|
session.layerStack.commit { it + layer }
|
|
603
702
|
selectLayer(layer.id)
|
|
604
703
|
setStickersMode(false, session)
|
|
@@ -617,7 +716,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
617
716
|
}
|
|
618
717
|
addView(
|
|
619
718
|
createAssetCard(icon, id) {
|
|
620
|
-
val layer =
|
|
719
|
+
val layer = newSticker(stickerUri = uri)
|
|
621
720
|
session.layerStack.commit { it + layer }
|
|
622
721
|
selectLayer(layer.id)
|
|
623
722
|
setStickersMode(false, session)
|
|
@@ -735,7 +834,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
735
834
|
when (requestCode) {
|
|
736
835
|
REQUEST_CODE_STICKER_UPLOAD -> {
|
|
737
836
|
val session = photoSession ?: return
|
|
738
|
-
val layer =
|
|
837
|
+
val layer = newSticker(stickerUri = resolvedUri)
|
|
739
838
|
session.layerStack.commit { it + layer }
|
|
740
839
|
selectLayer(layer.id)
|
|
741
840
|
setStickersMode(false, session)
|
|
@@ -755,7 +854,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
755
854
|
}
|
|
756
855
|
REQUEST_CODE_VIDEO_STICKER_UPLOAD -> {
|
|
757
856
|
val session = videoSession ?: return
|
|
758
|
-
val layer =
|
|
857
|
+
val layer = newSticker(stickerUri = resolvedUri)
|
|
759
858
|
session.layerStack.commit { it + layer }
|
|
760
859
|
onVideoLayerStackChanged()
|
|
761
860
|
selectVideoLayer(layer.id, session)
|
|
@@ -779,52 +878,37 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
779
878
|
|
|
780
879
|
private fun selectLayer(id: String?) {
|
|
781
880
|
val session = photoSession
|
|
881
|
+
if (selectedLayerId != id) activeLayerPropertyKey = ""
|
|
782
882
|
selectedLayerId = id
|
|
783
883
|
layerOverlay.selectedLayerId = id
|
|
784
884
|
if (session != null) {
|
|
785
885
|
layerOverlay.layers = session.layerStack.layers
|
|
786
886
|
schedulePhotoPreviewRender()
|
|
787
887
|
}
|
|
888
|
+
if (id != null) {
|
|
889
|
+
if (drawMode && session != null) setDrawMode(false, session)
|
|
890
|
+
if (cropMode && session != null) cancelCropMode(session, photoImageView, cropOverlay)
|
|
891
|
+
adjustMode = false; filtersMode = false; stickersMode = false; resizeMode = false
|
|
892
|
+
listOf(adjustSubBar, filtersSubBar, stickersSubBar, resizeSubBar, adjustmentSeekBar).forEach { it.visibility = View.GONE }
|
|
893
|
+
}
|
|
788
894
|
setLayerToolBarVisible(id != null)
|
|
789
895
|
}
|
|
790
896
|
|
|
791
897
|
private fun setLayerToolBarVisible(visible: Boolean) {
|
|
792
898
|
layerToolBar.visibility = if (visible) View.VISIBLE else View.GONE
|
|
793
899
|
mainToolBar.visibility = if (visible) View.GONE else View.VISIBLE
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
layerPropertyButtons["fontSize"]?.visibility = View.GONE
|
|
805
|
-
layerPropertyButtons["color"]?.visibility = if (isText) View.VISIBLE else View.GONE
|
|
806
|
-
listOf("lock", "visibility", "front", "back").forEach { key ->
|
|
807
|
-
layerIconButtons[key]?.visibility = if (reducedControls) View.GONE else View.VISIBLE
|
|
808
|
-
}
|
|
809
|
-
val allowedKeys = when {
|
|
810
|
-
isText -> setOf("opacity", "color")
|
|
811
|
-
isSticker -> setOf("opacity")
|
|
812
|
-
else -> setOf("scale", "rotation", "opacity")
|
|
813
|
-
}
|
|
814
|
-
if (activeLayerPropertyKey !in allowedKeys) {
|
|
815
|
-
activeLayerPropertyKey = "opacity"
|
|
816
|
-
}
|
|
817
|
-
layerPropertySeekBar.visibility = if (activeLayerPropertyKey == "color") View.GONE else View.VISIBLE
|
|
818
|
-
layerColorSwatchRow.visibility = if (activeLayerPropertyKey == "color") View.VISIBLE else View.GONE
|
|
819
|
-
syncLayerPropertySeekBar()
|
|
820
|
-
refreshSelection(layerPropertyButtons, activeLayerPropertyKey, themeColor("textColor", DesignTokens.onSurface), themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
821
|
-
} else {
|
|
822
|
-
layerPropertySeekBar.visibility = View.GONE
|
|
823
|
-
layerColorSwatchRow.visibility = View.GONE
|
|
824
|
-
}
|
|
900
|
+
val isText = currentSelectedLayer()?.type == LayerType.TEXT
|
|
901
|
+
val simple = isText || currentSelectedLayer()?.type == LayerType.STICKER
|
|
902
|
+
listOf("scale", "lock", "visibility", "front", "back").forEach { layerPropertyButtons[it]?.root?.visibility = if (simple) View.GONE else View.VISIBLE }
|
|
903
|
+
listOf("edit", "color", "fontSize").forEach { layerPropertyButtons[it]?.root?.visibility = if (isText) View.VISIBLE else View.GONE }
|
|
904
|
+
if (!isText && activeLayerPropertyKey in setOf("color", "fontSize")) activeLayerPropertyKey = ""
|
|
905
|
+
layerPropertySeekBar.visibility = if (visible && activeLayerPropertyKey in setOf("scale", "rotation", "opacity", "fontSize")) View.VISIBLE else View.GONE
|
|
906
|
+
layerColorSwatchRow.visibility = if (visible && activeLayerPropertyKey == "color") View.VISIBLE else View.GONE
|
|
907
|
+
if (visible) syncLayerPropertySeekBar()
|
|
908
|
+
textSwatches.forEach { (color, view) -> view.background = roundedDrawable(DesignTokens.surfaceContainerHigh, 24).apply { if (color == currentSelectedLayer()?.textColor) setStroke(dp(2), themeColor("primaryColor", DesignTokens.primaryContainer)) } }
|
|
909
|
+
refreshToolNodeSelection(layerPropertyButtons, activeLayerPropertyKey, Color.WHITE)
|
|
825
910
|
}
|
|
826
911
|
|
|
827
|
-
/** Circular color swatch, matching the drawing_brush_studio mockup's palette dots. */
|
|
828
912
|
private fun createColorSwatch(color: Int, onClick: () -> Unit): FrameLayout {
|
|
829
913
|
val outer = FrameLayout(this).apply {
|
|
830
914
|
background = circleDrawable(DesignTokens.surfaceContainerHigh)
|
|
@@ -837,38 +921,29 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
837
921
|
}
|
|
838
922
|
|
|
839
923
|
/** Text color swatch row, shown when a TEXT layer's "Color" property is active (text_editor_typography_studio mockup). */
|
|
840
|
-
private fun createTextColorSwatchRow(session: PhotoEditSession): LinearLayout {
|
|
841
|
-
val
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
)
|
|
864
|
-
}
|
|
865
|
-
})
|
|
866
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
867
|
-
addView(
|
|
868
|
-
actionButton("Done") { selectLayer(null) }.apply { setTextColor(themeColor("primaryColor", DesignTokens.primaryContainer)) },
|
|
869
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
870
|
-
)
|
|
871
|
-
}
|
|
924
|
+
private fun createTextColorSwatchRow(session: PhotoEditSession): LinearLayout = createTextPalette(false) { color ->
|
|
925
|
+
val id = selectedLayerId ?: return@createTextPalette
|
|
926
|
+
session.layerStack.commit { list -> list.map { if (it.id == id) it.copy(textColor = color) else it } }
|
|
927
|
+
onLayerStackChanged()
|
|
928
|
+
setLayerToolBarVisible(true)
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private fun createTextPalette(video: Boolean, onColor: (Int) -> Unit): LinearLayout = LinearLayout(this).apply {
|
|
932
|
+
setBackgroundColor(themeColor("toolbarColor", DesignTokens.surfaceContainerLow))
|
|
933
|
+
val swatches = if (video) videoTextSwatches else textSwatches
|
|
934
|
+
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
935
|
+
isHorizontalScrollBarEnabled = false
|
|
936
|
+
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
937
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
938
|
+
setPadding(dp(12), 0, dp(12), 0)
|
|
939
|
+
listOf(Color.WHITE, Color.BLACK, Color.rgb(255, 77, 94), Color.rgb(255, 155, 61), Color.rgb(255, 214, 51), Color.rgb(76, 217, 100), Color.rgb(0, 145, 255), DesignTokens.primary, Color.rgb(255, 105, 180)).forEach { color ->
|
|
940
|
+
val swatch = createColorSwatch(color) { onColor(color) }
|
|
941
|
+
swatch.contentDescription = "Text color #" + Integer.toHexString(color).takeLast(6)
|
|
942
|
+
swatches[color] = swatch
|
|
943
|
+
addView(swatch, LinearLayout.LayoutParams(dp(48), dp(48)))
|
|
944
|
+
}
|
|
945
|
+
})
|
|
946
|
+
}, LinearLayout.LayoutParams(MATCH, dp(48)))
|
|
872
947
|
}
|
|
873
948
|
|
|
874
949
|
private fun currentSelectedLayer(): PhotoLayer? {
|
|
@@ -879,13 +954,14 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
879
954
|
private fun syncLayerPropertySeekBar() {
|
|
880
955
|
val layer = currentSelectedLayer() ?: return
|
|
881
956
|
val (min, max, value) = when (activeLayerPropertyKey) {
|
|
882
|
-
"rotation" -> Triple(-180f, 180f, layer.rotationDegrees)
|
|
957
|
+
"rotation" -> Triple(-180f, 180f, ((layer.rotationDegrees % 360f + 540f) % 360f) - 180f)
|
|
883
958
|
"opacity" -> Triple(0f, 100f, layer.opacity * 100f)
|
|
884
959
|
"fontSize" -> Triple(12f, 160f, layer.fontSize)
|
|
885
960
|
else -> Triple(20f, 800f, layer.scale * 100f)
|
|
886
961
|
}
|
|
887
962
|
layerPropertySeekBar.max = (max - min).roundToInt()
|
|
888
963
|
layerPropertySeekBar.progress = (value - min).roundToInt()
|
|
964
|
+
(layerPropertySeekBar as? EditorToolSlider)?.propertyKey = activeLayerPropertyKey
|
|
889
965
|
}
|
|
890
966
|
|
|
891
967
|
private fun onLayerPropertyChanged(progress: Int) {
|
|
@@ -913,269 +989,250 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
913
989
|
schedulePhotoPreviewRender()
|
|
914
990
|
}
|
|
915
991
|
|
|
916
|
-
private fun createLayerToolBar(session: PhotoEditSession): LinearLayout {
|
|
917
|
-
val
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
992
|
+
private fun createLayerToolBar(session: PhotoEditSession): LinearLayout = createContextToolbar(false) { key ->
|
|
993
|
+
val layer = currentSelectedLayer() ?: return@createContextToolbar
|
|
994
|
+
when (key) {
|
|
995
|
+
"edit" -> showTextInputDialog(session, layer)
|
|
996
|
+
"done" -> selectLayer(null)
|
|
997
|
+
"delete" -> { session.layerStack.commit { it.filterNot { item -> item.id == layer.id } }; selectLayer(null); onLayerStackChanged() }
|
|
998
|
+
"duplicate" -> {
|
|
999
|
+
val copy = layer.copy(id = java.util.UUID.randomUUID().toString(), x = (layer.x + 0.04f).coerceIn(0f, 1f), y = (layer.y + 0.04f).coerceIn(0f, 1f))
|
|
1000
|
+
session.layerStack.commit { it + copy }; selectLayer(copy.id); onLayerStackChanged()
|
|
1001
|
+
}
|
|
1002
|
+
"lock", "visibility", "front", "back" -> {
|
|
1003
|
+
session.layerStack.commit { list -> when (key) {
|
|
1004
|
+
"front" -> list.filterNot { it.id == layer.id } + layer
|
|
1005
|
+
"back" -> listOf(layer) + list.filterNot { it.id == layer.id }
|
|
1006
|
+
else -> list.map { if (it.id != layer.id) it else if (key == "lock") it.copy(locked = !it.locked) else it.copy(visible = !it.visible) }
|
|
1007
|
+
} }; onLayerStackChanged()
|
|
1008
|
+
}
|
|
1009
|
+
else -> { activeLayerPropertyKey = if (activeLayerPropertyKey == key) "" else key; setLayerToolBarVisible(true) }
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
private fun createContextToolbar(video: Boolean, onAction: (String) -> Unit): LinearLayout {
|
|
1014
|
+
val nodes = if (video) videoLayerPropertyButtons else layerPropertyButtons
|
|
1015
|
+
val actions = mutableListOf(
|
|
1016
|
+
Triple("edit", "Edit", R.drawable.ic_draw), Triple("color", "Color", R.drawable.ic_palette),
|
|
1017
|
+
Triple("fontSize", "Size", R.drawable.ic_format_size), Triple("rotation", "Rotate", R.drawable.ic_rotate_right),
|
|
1018
|
+
Triple("opacity", "Opacity", R.drawable.ic_opacity), Triple("scale", "Scale", R.drawable.ic_fullscreen),
|
|
1019
|
+
Triple("duplicate", "Duplicate", R.drawable.ic_content_copy), Triple("delete", "Delete", R.drawable.ic_delete)
|
|
1020
|
+
)
|
|
1021
|
+
if (!video) actions.addAll(listOf(Triple("lock", "Lock", R.drawable.ic_lock), Triple("visibility", "Hide", R.drawable.ic_visibility_off), Triple("front", "Front", R.drawable.ic_chevron_right), Triple("back", "Back", R.drawable.ic_chevron_left)))
|
|
922
1022
|
return LinearLayout(this).apply {
|
|
923
1023
|
gravity = Gravity.CENTER_VERTICAL
|
|
924
|
-
|
|
925
|
-
setBackgroundColor(toolbarColor)
|
|
1024
|
+
setBackgroundColor(themeColor("toolbarColor", DesignTokens.surfaceContainerLow))
|
|
926
1025
|
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
927
1026
|
isHorizontalScrollBarEnabled = false
|
|
928
1027
|
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
929
|
-
orientation = LinearLayout.HORIZONTAL
|
|
930
1028
|
gravity = Gravity.CENTER_VERTICAL
|
|
931
|
-
|
|
932
|
-
val
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
if (key != "color") syncLayerPropertySeekBar()
|
|
937
|
-
refreshSelection(layerPropertyButtons, activeLayerPropertyKey, textColor, primaryColor)
|
|
938
|
-
}.apply { setTextColor(textColor) }
|
|
939
|
-
layerPropertyButtons[key] = chip
|
|
940
|
-
addView(chip, LinearLayout.LayoutParams(dp(80), MATCH))
|
|
1029
|
+
actions.forEach { (key, label, icon) ->
|
|
1030
|
+
val node = createToolNode(icon, label) { onAction(key) }
|
|
1031
|
+
nodes[key] = node
|
|
1032
|
+
if (key == "delete") { node.icon.setColorFilter(Color.rgb(255, 100, 115)); node.label.setTextColor(Color.rgb(255, 100, 115)) }
|
|
1033
|
+
addView(node.root, LinearLayout.LayoutParams(dp(72), WRAP))
|
|
941
1034
|
}
|
|
942
|
-
addView(
|
|
943
|
-
createIconButton(R.drawable.ic_content_copy, "Duplicate layer", textColor) {
|
|
944
|
-
val original = currentSelectedLayer() ?: return@createIconButton
|
|
945
|
-
val copy = original.copy(id = java.util.UUID.randomUUID().toString(), x = (original.x + 0.04f).coerceIn(0f, 1f), y = (original.y + 0.04f).coerceIn(0f, 1f))
|
|
946
|
-
session.layerStack.commit { it + copy }
|
|
947
|
-
selectLayer(copy.id)
|
|
948
|
-
},
|
|
949
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
950
|
-
)
|
|
951
|
-
addView(
|
|
952
|
-
createIconButton(R.drawable.ic_lock, "Toggle lock", textColor) {
|
|
953
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
954
|
-
session.layerStack.commit { list -> list.map { if (it.id == id) it.copy(locked = !it.locked) else it } }
|
|
955
|
-
onLayerStackChanged()
|
|
956
|
-
}.also { layerIconButtons["lock"] = it },
|
|
957
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
958
|
-
)
|
|
959
|
-
addView(
|
|
960
|
-
createIconButton(R.drawable.ic_visibility_off, "Toggle visibility", textColor) {
|
|
961
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
962
|
-
session.layerStack.commit { list -> list.map { if (it.id == id) it.copy(visible = !it.visible) else it } }
|
|
963
|
-
onLayerStackChanged()
|
|
964
|
-
}.also { layerIconButtons["visibility"] = it },
|
|
965
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
966
|
-
)
|
|
967
|
-
addView(
|
|
968
|
-
createIconButton(R.drawable.ic_chevron_right, "Bring to front", textColor) {
|
|
969
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
970
|
-
session.layerStack.commit { list -> (list.filter { it.id != id } + list.first { it.id == id }) }
|
|
971
|
-
onLayerStackChanged()
|
|
972
|
-
}.also { layerIconButtons["front"] = it },
|
|
973
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
974
|
-
)
|
|
975
|
-
addView(
|
|
976
|
-
createIconButton(R.drawable.ic_chevron_left, "Send to back", textColor) {
|
|
977
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
978
|
-
session.layerStack.commit { list -> (listOf(list.first { it.id == id }) + list.filter { it.id != id }) }
|
|
979
|
-
onLayerStackChanged()
|
|
980
|
-
}.also { layerIconButtons["back"] = it },
|
|
981
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
982
|
-
)
|
|
983
|
-
addView(
|
|
984
|
-
createIconButton(R.drawable.ic_delete, "Delete layer", textColor) {
|
|
985
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
986
|
-
session.layerStack.commit { list -> list.filterNot { it.id == id } }
|
|
987
|
-
selectLayer(null)
|
|
988
|
-
onLayerStackChanged()
|
|
989
|
-
},
|
|
990
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
991
|
-
)
|
|
992
1035
|
})
|
|
993
1036
|
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
)
|
|
1037
|
+
val done = createToolNode(R.drawable.ic_check, "Done") { onAction("done") }
|
|
1038
|
+
setToolNodeSelected(done, true, Color.WHITE)
|
|
1039
|
+
addView(done.root, LinearLayout.LayoutParams(dp(64), WRAP))
|
|
998
1040
|
}
|
|
999
1041
|
}
|
|
1000
1042
|
|
|
1001
|
-
/** Adjust (image_adjustments_workspace mockup) — Brightness/Contrast/Saturation/etc sliders + Mirror. */
|
|
1002
1043
|
private fun setAdjustMode(enabled: Boolean, session: PhotoEditSession) {
|
|
1003
1044
|
adjustMode = enabled
|
|
1045
|
+
// The slider now lives inside the panel, so showing the panel shows it — one less thing to
|
|
1046
|
+
// keep in sync, and no stray full-width slider row left behind above the main dock.
|
|
1004
1047
|
adjustSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1005
|
-
adjustmentSeekBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1006
1048
|
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1007
1049
|
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1008
|
-
|
|
1050
|
+
// Entering Adjust selects a control to scrub but never resets the values themselves, so
|
|
1051
|
+
// Adjust -> Filters -> Adjust round-trips keep whatever the user already dialled in.
|
|
1052
|
+
if (enabled) { activeAdjustmentKey = "brightness"; syncAdjustmentSeekBar(session); fadeInPanel(adjustSubBar) }
|
|
1009
1053
|
refreshPhotoToolSelection()
|
|
1010
|
-
refreshAdjustSelection()
|
|
1054
|
+
refreshAdjustSelection(session)
|
|
1011
1055
|
}
|
|
1012
1056
|
|
|
1013
|
-
/** Filters
|
|
1057
|
+
/** Filters — preset thumbnail carousel with an intensity slider above it. */
|
|
1014
1058
|
private fun setFiltersMode(enabled: Boolean, session: PhotoEditSession) {
|
|
1015
1059
|
filtersMode = enabled
|
|
1016
1060
|
filtersSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1017
|
-
adjustmentSeekBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1018
1061
|
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1019
1062
|
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1020
|
-
if (enabled) {
|
|
1063
|
+
if (enabled) { syncAdjustmentSeekBar(session); fadeInPanel(filtersSubBar) }
|
|
1021
1064
|
refreshPhotoToolSelection()
|
|
1022
1065
|
refreshFiltersSelection(session)
|
|
1023
1066
|
}
|
|
1024
1067
|
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1068
|
+
/** Short cross-fade when a panel opens, per the "subtle transition" spec (no slide/scale drama). */
|
|
1069
|
+
private fun fadeInPanel(panel: View) {
|
|
1070
|
+
panel.animate().cancel()
|
|
1071
|
+
panel.alpha = 0f
|
|
1072
|
+
panel.animate().alpha(1f).setDuration(180).start()
|
|
1029
1073
|
}
|
|
1030
1074
|
|
|
1031
|
-
private fun
|
|
1032
|
-
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
1075
|
+
private fun refreshAdjustSelection(session: PhotoEditSession) {
|
|
1033
1076
|
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1034
|
-
val
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
android.graphics.drawable.GradientDrawable().apply {
|
|
1041
|
-
cornerRadius = dp(DesignTokens.radiusLg).toFloat()
|
|
1042
|
-
setStroke(dp(2), DesignTokens.primaryContainer)
|
|
1043
|
-
}
|
|
1077
|
+
val defaults = PhotoAdjustments()
|
|
1078
|
+
adjustmentStripItems.forEach { (key, item) ->
|
|
1079
|
+
if (key == "mirror") {
|
|
1080
|
+
// A toggle, so "selected" means "on" rather than "being scrubbed".
|
|
1081
|
+
item.setSelected(session.adjustments.mirror, primaryColor)
|
|
1082
|
+
item.setModified(false, primaryColor)
|
|
1044
1083
|
} else {
|
|
1045
|
-
|
|
1084
|
+
item.setSelected(key == activeAdjustmentKey, primaryColor)
|
|
1085
|
+
item.setModified(session.adjustments.value(key) != defaults.value(key), primaryColor)
|
|
1046
1086
|
}
|
|
1047
|
-
label.setTextColor(if (selected) primaryColor else DesignTokens.outline)
|
|
1048
|
-
label.setTypeface(label.typeface, if (selected) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)
|
|
1049
1087
|
}
|
|
1050
1088
|
}
|
|
1051
1089
|
|
|
1090
|
+
private fun refreshFiltersSelection(session: PhotoEditSession) {
|
|
1091
|
+
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1092
|
+
val activePreset = session.adjustments.filterPreset ?: ""
|
|
1093
|
+
filterPresetCards.forEach { (id, thumbnail) -> thumbnail.setSelected(id == activePreset, primaryColor) }
|
|
1094
|
+
// Intensity only means something once a preset is applied.
|
|
1095
|
+
filterIntensitySeekBar.visibility = if (activePreset.isEmpty()) View.INVISIBLE else View.VISIBLE
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/** Points whichever slider is on screen at the currently selected value. */
|
|
1052
1099
|
private fun syncAdjustmentSeekBar(session: PhotoEditSession) {
|
|
1053
1100
|
val (min, max) = PhotoAdjustments.rangeFor(activeAdjustmentKey)
|
|
1054
1101
|
adjustmentSeekBar.max = (max - min).roundToInt()
|
|
1055
1102
|
adjustmentSeekBar.progress = (session.adjustments.value(activeAdjustmentKey) - min).roundToInt()
|
|
1103
|
+
adjustmentSeekBar.labelText = photoAdjustmentDefinitions.firstOrNull { it.first == activeAdjustmentKey }?.second
|
|
1104
|
+
adjustmentSeekBar.valueFormatter = { progress -> formatAdjustmentValue(activeAdjustmentKey, min + progress) }
|
|
1105
|
+
// Bipolar adjustments get a centre tick at their neutral value; unipolar ones (blur) do not.
|
|
1106
|
+
adjustmentSeekBar.neutralProgress = if (min < 0f) (-min).roundToInt() else null
|
|
1107
|
+
|
|
1108
|
+
val (strengthMin, strengthMax) = PhotoAdjustments.rangeFor("filterStrength")
|
|
1109
|
+
filterIntensitySeekBar.max = (strengthMax - strengthMin).roundToInt()
|
|
1110
|
+
filterIntensitySeekBar.progress = (session.adjustments.filterStrength - strengthMin).roundToInt()
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
private fun formatAdjustmentValue(key: String, value: Float): String {
|
|
1114
|
+
val rounded = value.roundToInt()
|
|
1115
|
+
return if (key == "blurRadius") "$rounded" else if (rounded > 0) "+$rounded" else "$rounded"
|
|
1056
1116
|
}
|
|
1057
1117
|
|
|
1118
|
+
/**
|
|
1119
|
+
* Adjustments the processing engine actually implements, in carousel order. Deliberately not a
|
|
1120
|
+
* wishlist: every entry maps to a real [PhotoAdjustments] field, so there are no controls that
|
|
1121
|
+
* move a slider without changing the photo. (Highlights/shadows/tint/sharpness were removed from
|
|
1122
|
+
* the engine in an earlier pass and would need that processing restored before they could return.)
|
|
1123
|
+
*/
|
|
1124
|
+
private val photoAdjustmentDefinitions = listOf(
|
|
1125
|
+
Triple("exposure", "Exposure", R.drawable.ic_flare),
|
|
1126
|
+
Triple("brightness", "Brightness", R.drawable.ic_light_mode),
|
|
1127
|
+
Triple("contrast", "Contrast", R.drawable.ic_opacity),
|
|
1128
|
+
Triple("saturation", "Saturation", R.drawable.ic_palette),
|
|
1129
|
+
Triple("temperature", "Warmth", R.drawable.ic_compare),
|
|
1130
|
+
Triple("blurRadius", "Blur", R.drawable.ic_filter_center_focus)
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
/**
|
|
1134
|
+
* Adjust panel: `Adjust / Reset / Done` header, one shared slider bound to whichever adjustment is
|
|
1135
|
+
* selected, then a compact scrollable strip. Replaces the previous row of full-height rectangular
|
|
1136
|
+
* buttons, which wrapped "Brightness" onto two lines and gave `Done` a quarter of the screen.
|
|
1137
|
+
*/
|
|
1058
1138
|
private fun createAdjustSubBar(session: PhotoEditSession, imageView: ZoomableImageView): LinearLayout {
|
|
1059
|
-
val toolbarColor = themeColor("toolbarColor", DesignTokens.surfaceContainerLow)
|
|
1060
|
-
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
1061
1139
|
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1062
|
-
val adjustmentChips = listOf(
|
|
1063
|
-
"brightness" to "Brightness", "contrast" to "Contrast", "saturation" to "Saturation",
|
|
1064
|
-
"exposure" to "Exposure", "temperature" to "Temp", "blurRadius" to "Blur"
|
|
1065
|
-
)
|
|
1066
1140
|
return LinearLayout(this).apply {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1141
|
+
orientation = LinearLayout.VERTICAL
|
|
1142
|
+
setBackgroundColor(DesignTokens.editorSurface)
|
|
1143
|
+
addView(
|
|
1144
|
+
editorPanelHeader(
|
|
1145
|
+
context = this@PhotoVideoEditorActivity,
|
|
1146
|
+
title = "Adjust",
|
|
1147
|
+
accentColor = primaryColor,
|
|
1148
|
+
secondaryLabel = "Reset",
|
|
1149
|
+
onSecondary = {
|
|
1150
|
+
session.updateAdjustments {
|
|
1151
|
+
it.copy(brightness = 0f, contrast = 0f, saturation = 0f, exposure = 0f, temperature = 0f, blurRadius = 0f, mirror = false)
|
|
1152
|
+
}
|
|
1153
|
+
syncAdjustmentSeekBar(session)
|
|
1154
|
+
schedulePhotoPreviewRender()
|
|
1155
|
+
refreshAdjustSelection(session)
|
|
1156
|
+
},
|
|
1157
|
+
onDone = { setAdjustMode(false, session) }
|
|
1158
|
+
),
|
|
1159
|
+
LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.HEADER_HEIGHT_DP))
|
|
1160
|
+
)
|
|
1161
|
+
addView(adjustmentSeekBar, LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.SLIDER_HEIGHT_DP)))
|
|
1162
|
+
addView(
|
|
1163
|
+
editorToolStrip(this@PhotoVideoEditorActivity) {
|
|
1164
|
+
photoAdjustmentDefinitions.forEach { (key, label, iconRes) ->
|
|
1165
|
+
val item = editorStripItem(this@PhotoVideoEditorActivity, iconRes, label) {
|
|
1076
1166
|
activeAdjustmentKey = key
|
|
1077
1167
|
syncAdjustmentSeekBar(session)
|
|
1078
|
-
refreshAdjustSelection()
|
|
1079
|
-
}
|
|
1080
|
-
|
|
1081
|
-
addView(
|
|
1168
|
+
refreshAdjustSelection(session)
|
|
1169
|
+
}
|
|
1170
|
+
adjustmentStripItems[key] = item
|
|
1171
|
+
addView(item.root, LinearLayout.LayoutParams(dp(EditorPanelMetrics.STRIP_ITEM_WIDTH_DP), WRAP))
|
|
1082
1172
|
}
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
schedulePhotoPreviewRender()
|
|
1095
|
-
refreshAdjustSelection()
|
|
1096
|
-
}.apply { setTextColor(textColor) },
|
|
1097
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1098
|
-
)
|
|
1099
|
-
})
|
|
1100
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
1101
|
-
addView(
|
|
1102
|
-
actionButton("Done") { setAdjustMode(false, session) }.apply { setTextColor(primaryColor) },
|
|
1103
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1173
|
+
// Mirror is a toggle, not a slider target, but it lives in the same strip so the panel has
|
|
1174
|
+
// one visual language rather than a stray button.
|
|
1175
|
+
val mirror = editorStripItem(this@PhotoVideoEditorActivity, R.drawable.ic_aspect_ratio, "Mirror") {
|
|
1176
|
+
session.updateAdjustments { it.copy(mirror = !it.mirror) }
|
|
1177
|
+
schedulePhotoPreviewRender()
|
|
1178
|
+
refreshAdjustSelection(session)
|
|
1179
|
+
}
|
|
1180
|
+
adjustmentStripItems["mirror"] = mirror
|
|
1181
|
+
addView(mirror.root, LinearLayout.LayoutParams(dp(EditorPanelMetrics.STRIP_ITEM_WIDTH_DP), WRAP))
|
|
1182
|
+
},
|
|
1183
|
+
LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.STRIP_HEIGHT_DP))
|
|
1104
1184
|
)
|
|
1105
1185
|
}
|
|
1106
1186
|
}
|
|
1107
1187
|
|
|
1108
|
-
/**
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
1114
|
-
}
|
|
1115
|
-
val frame = FrameLayout(this).apply {
|
|
1116
|
-
background = roundedDrawable(DesignTokens.surfaceContainerHighest, DesignTokens.radiusLg)
|
|
1117
|
-
clipToOutline = true
|
|
1118
|
-
addView(image, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1119
|
-
}
|
|
1120
|
-
val label = TextView(this).apply {
|
|
1121
|
-
text = labelText
|
|
1122
|
-
textSize = 11f
|
|
1123
|
-
gravity = Gravity.CENTER
|
|
1124
|
-
setTextColor(DesignTokens.outline)
|
|
1125
|
-
setPadding(0, dp(DesignTokens.spaceXs), 0, 0)
|
|
1126
|
-
maxLines = 1
|
|
1127
|
-
}
|
|
1128
|
-
val root = LinearLayout(this).apply {
|
|
1129
|
-
orientation = LinearLayout.VERTICAL
|
|
1130
|
-
gravity = Gravity.CENTER_HORIZONTAL
|
|
1131
|
-
isClickable = true
|
|
1132
|
-
setPadding(dp(4), 0, dp(4), 0)
|
|
1133
|
-
addView(frame, LinearLayout.LayoutParams(thumbSize, thumbSize))
|
|
1134
|
-
addView(label, LinearLayout.LayoutParams(dp(72), WRAP))
|
|
1135
|
-
setOnClickListener { onClick() }
|
|
1136
|
-
}
|
|
1137
|
-
return Triple(root, frame, label)
|
|
1138
|
-
}
|
|
1139
|
-
|
|
1188
|
+
/**
|
|
1189
|
+
* Filters panel: `Filters / Done` header, an intensity slider that appears once a preset is
|
|
1190
|
+
* applied, then the preset carousel. Same header/slider/strip skeleton as Adjust so the two read
|
|
1191
|
+
* as one system.
|
|
1192
|
+
*/
|
|
1140
1193
|
private fun createFiltersSubBar(session: PhotoEditSession, imageView: ZoomableImageView): LinearLayout {
|
|
1141
|
-
val toolbarColor = themeColor("toolbarColor", DesignTokens.surfaceContainerLow)
|
|
1142
|
-
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
1143
1194
|
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1144
1195
|
return LinearLayout(this).apply {
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1196
|
+
orientation = LinearLayout.VERTICAL
|
|
1197
|
+
setBackgroundColor(DesignTokens.editorSurface)
|
|
1198
|
+
addView(
|
|
1199
|
+
editorPanelHeader(
|
|
1200
|
+
context = this@PhotoVideoEditorActivity,
|
|
1201
|
+
title = "Filters",
|
|
1202
|
+
accentColor = primaryColor,
|
|
1203
|
+
onDone = { setFiltersMode(false, session) }
|
|
1204
|
+
),
|
|
1205
|
+
LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.HEADER_HEIGHT_DP))
|
|
1206
|
+
)
|
|
1207
|
+
addView(filterIntensitySeekBar, LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.SLIDER_HEIGHT_DP)))
|
|
1208
|
+
addView(
|
|
1209
|
+
editorToolStrip(this@PhotoVideoEditorActivity) {
|
|
1210
|
+
val original = editorFilterThumbnail(this@PhotoVideoEditorActivity, "Original") {
|
|
1154
1211
|
session.updateAdjustments { it.copy(filterPreset = null, filterStrength = 100f) }
|
|
1155
1212
|
syncAdjustmentSeekBar(session)
|
|
1156
1213
|
schedulePhotoPreviewRender()
|
|
1157
1214
|
refreshFiltersSelection(session)
|
|
1158
1215
|
}
|
|
1159
|
-
|
|
1216
|
+
original.image.setImageBitmap(session.baseBitmap)
|
|
1217
|
+
filterPresetCards[""] = original
|
|
1218
|
+
addView(original.root, LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) })
|
|
1160
1219
|
PhotoFilterPresets.PRESET_IDS.forEach { id ->
|
|
1161
|
-
val
|
|
1220
|
+
val thumbnail = editorFilterThumbnail(this@PhotoVideoEditorActivity, PhotoFilterPresets.labelFor(id)) {
|
|
1162
1221
|
session.updateAdjustments { it.copy(filterPreset = if (it.filterPreset == id) null else id) }
|
|
1163
|
-
activeAdjustmentKey = "filterStrength"
|
|
1164
1222
|
syncAdjustmentSeekBar(session)
|
|
1165
1223
|
schedulePhotoPreviewRender()
|
|
1166
1224
|
refreshFiltersSelection(session)
|
|
1167
1225
|
}
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1226
|
+
// Thumbnails render off a cached downscaled copy of the photo, never the full-resolution
|
|
1227
|
+
// bitmap — 17 presets at full size would stall the panel on open.
|
|
1228
|
+
filterThumbnailLoader.load(session.baseBitmap, id, dp(EditorPanelMetrics.THUMBNAIL_WIDTH_DP)) {
|
|
1229
|
+
thumbnail.image.setImageBitmap(it)
|
|
1230
|
+
}
|
|
1231
|
+
filterPresetCards[id] = thumbnail
|
|
1232
|
+
addView(thumbnail.root, LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) })
|
|
1172
1233
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
1176
|
-
addView(
|
|
1177
|
-
actionButton("Done") { setFiltersMode(false, session) }.apply { setTextColor(primaryColor) },
|
|
1178
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1234
|
+
},
|
|
1235
|
+
LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.STRIP_HEIGHT_DP + 24))
|
|
1179
1236
|
)
|
|
1180
1237
|
}
|
|
1181
1238
|
}
|
|
@@ -1185,118 +1242,39 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1185
1242
|
cropMode = enabled
|
|
1186
1243
|
imageView.panZoomEnabled = !enabled
|
|
1187
1244
|
overlay.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1188
|
-
straightenSeekBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1189
1245
|
cropSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1190
1246
|
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1191
1247
|
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1192
|
-
if (enabled)
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
val isIdentityCrop = state.cropLeft == 0f && state.cropTop == 0f && state.cropRight == 1f && state.cropBottom == 1f
|
|
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
|
|
1201
|
-
}
|
|
1248
|
+
if (!enabled) cropEntryState = null
|
|
1249
|
+
photoCropPanel?.sync(session.state)
|
|
1250
|
+
schedulePhotoPreviewRender()
|
|
1202
1251
|
refreshPhotoToolSelection()
|
|
1203
|
-
refreshSelection(cropAspectButtons, session.state.aspectRatio, themeColor("textColor", DesignTokens.onSurface), themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
1204
1252
|
}
|
|
1205
1253
|
|
|
1206
1254
|
private fun cancelCropMode(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
1207
1255
|
cropEntryState?.let { snapshot -> session.update { snapshot } }
|
|
1208
|
-
straightenSeekBar.progress = ((cropEntryState?.straightenDegrees ?: 0f) + 45f).roundToInt()
|
|
1209
|
-
cropEntryState = null
|
|
1210
|
-
schedulePhotoPreviewRender()
|
|
1211
1256
|
setCropMode(false, session, imageView, overlay)
|
|
1212
1257
|
}
|
|
1213
1258
|
|
|
1214
1259
|
private fun createCropSubBar(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView): LinearLayout {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
gravity = Gravity.CENTER_VERTICAL
|
|
1232
|
-
setPadding(dp(DesignTokens.spaceLg), 0, dp(DesignTokens.spaceLg), 0)
|
|
1233
|
-
}, LinearLayout.LayoutParams(MATCH, dp(36)))
|
|
1234
|
-
|
|
1235
|
-
// Rotate/Flip row — moved here from the main tool rail to match the crop_transform_editor mockup.
|
|
1236
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1237
|
-
orientation = LinearLayout.HORIZONTAL
|
|
1238
|
-
gravity = Gravity.CENTER
|
|
1239
|
-
addView(
|
|
1240
|
-
createIconButton(R.drawable.ic_rotate_left, "Rotate left", textColor) {
|
|
1241
|
-
session.update { it.rotatedRight().rotatedRight().rotatedRight() }
|
|
1242
|
-
schedulePhotoPreviewRender()
|
|
1243
|
-
imageView.post { imageView.resetToFit(); overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = true) }
|
|
1244
|
-
},
|
|
1245
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
1246
|
-
)
|
|
1247
|
-
addView(
|
|
1248
|
-
createIconButton(R.drawable.ic_rotate_right, "Rotate right", textColor) {
|
|
1249
|
-
session.update { it.rotatedRight() }
|
|
1250
|
-
schedulePhotoPreviewRender()
|
|
1251
|
-
imageView.post { imageView.resetToFit(); overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = true) }
|
|
1252
|
-
},
|
|
1253
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
1254
|
-
)
|
|
1255
|
-
}, LinearLayout.LayoutParams(MATCH, dp(DesignTokens.touchTargetMin)))
|
|
1256
|
-
|
|
1257
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1258
|
-
gravity = Gravity.CENTER_VERTICAL
|
|
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
|
-
)
|
|
1264
|
-
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
1265
|
-
isHorizontalScrollBarEnabled = false
|
|
1266
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1267
|
-
orientation = LinearLayout.HORIZONTAL
|
|
1268
|
-
aspectPresets.forEach { (label, ratio) ->
|
|
1269
|
-
val chip = actionButton(label) {
|
|
1270
|
-
overlay.setAspectRatio(ratio)
|
|
1271
|
-
session.update { it.withAspectRatio(ratio) }
|
|
1272
|
-
refreshSelection(cropAspectButtons, ratio, textColor, primaryColor)
|
|
1273
|
-
}.apply { setTextColor(textColor) }
|
|
1274
|
-
cropAspectButtons[ratio] = chip
|
|
1275
|
-
addView(chip, LinearLayout.LayoutParams(dp(64), MATCH))
|
|
1276
|
-
}
|
|
1277
|
-
addView(
|
|
1278
|
-
actionButton("Reset") {
|
|
1279
|
-
session.update { it.reset() }
|
|
1280
|
-
overlay.setAspectRatio(null)
|
|
1281
|
-
straightenSeekBar.progress = 45
|
|
1282
|
-
schedulePhotoPreviewRender()
|
|
1283
|
-
imageView.resetToFit()
|
|
1284
|
-
overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = true)
|
|
1285
|
-
refreshSelection(cropAspectButtons, null, textColor, primaryColor)
|
|
1286
|
-
}.apply { setTextColor(textColor) },
|
|
1287
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1288
|
-
)
|
|
1289
|
-
})
|
|
1290
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
1291
|
-
addView(
|
|
1292
|
-
actionButton("Done") { setCropMode(false, session, imageView, overlay) }.apply { setTextColor(primaryColor) },
|
|
1293
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1294
|
-
)
|
|
1295
|
-
}, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
1296
|
-
}
|
|
1260
|
+
fun refresh() { photoCropPanel?.sync(session.state); schedulePhotoPreviewRender() }
|
|
1261
|
+
return CropPanel(this, themeColor("primaryColor", DesignTokens.primaryContainer), themeColor("toolbarColor", DesignTokens.surfaceContainerLow), themeColor("textColor", DesignTokens.onSurface),
|
|
1262
|
+
originalRatio = { session.baseBitmap?.let { it.width.toFloat() / it.height } ?: 1f },
|
|
1263
|
+
onReset = { session.update { it.reset() }; refresh() },
|
|
1264
|
+
onDone = { setCropMode(false, session, imageView, overlay) },
|
|
1265
|
+
onCancel = { cancelCropMode(session, imageView, overlay) },
|
|
1266
|
+
onStraighten = { value -> session.update { it.withStraighten(value) }; refresh() },
|
|
1267
|
+
onRotate = {
|
|
1268
|
+
session.update { it.rotatedRight().copy(zoom = 1f, panX = 0f, panY = 0f, aspectRatio = null, aspectPreset = "Free") }; refresh()
|
|
1269
|
+
},
|
|
1270
|
+
onRatio = { label, ratio ->
|
|
1271
|
+
session.update { it.copy(aspectRatio = ratio, aspectPreset = label) }
|
|
1272
|
+
overlay.setAspectRatio(ratio)
|
|
1273
|
+
photoCropPanel?.sync(session.state)
|
|
1274
|
+
}
|
|
1275
|
+
).also { photoCropPanel = it; it.sync(session.state) }
|
|
1297
1276
|
}
|
|
1298
1277
|
|
|
1299
|
-
/** Canvas Resize & Social Presets (canvas_resize_social_presets mockup): reuses the crop-aspect mechanism to frame the shot, plus an exact export pixel size for the chosen platform. */
|
|
1300
1278
|
private fun setResizeMode(enabled: Boolean, session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
1301
1279
|
resizeMode = enabled
|
|
1302
1280
|
imageView.panZoomEnabled = !enabled
|
|
@@ -1410,7 +1388,12 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1410
1388
|
root.addView(header, LinearLayout.LayoutParams(MATCH, headerHeight))
|
|
1411
1389
|
|
|
1412
1390
|
val preview = FrameLayout(this).apply { setBackgroundColor(Color.BLACK) }
|
|
1413
|
-
root.addView(
|
|
1391
|
+
root.addView(
|
|
1392
|
+
preview,
|
|
1393
|
+
LinearLayout.LayoutParams(MATCH, 0, 1f).apply {
|
|
1394
|
+
setMargins(0, dp(DesignTokens.spaceXs), 0, dp(DesignTokens.spaceXs))
|
|
1395
|
+
}
|
|
1396
|
+
)
|
|
1414
1397
|
|
|
1415
1398
|
val session = VideoEditSession(this, sourceUri)
|
|
1416
1399
|
videoSession = session
|
|
@@ -1443,6 +1426,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1443
1426
|
useController = false
|
|
1444
1427
|
contentDescription = "Selected video preview"
|
|
1445
1428
|
}
|
|
1429
|
+
cropPlayerView = playerView
|
|
1446
1430
|
preview.addView(playerView, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1447
1431
|
|
|
1448
1432
|
val overlayView = ImageView(this).apply {
|
|
@@ -1454,9 +1438,33 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1454
1438
|
videoOverlayImageView = overlayView
|
|
1455
1439
|
preview.addView(overlayView, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1456
1440
|
|
|
1441
|
+
videoCropOverlay = CropOverlayView(this).apply {
|
|
1442
|
+
visibility = View.GONE
|
|
1443
|
+
onCropChanged = { l, t, r, b -> session.update { it.copy(crop = it.crop.withCrop(l, t, r, b)) } }
|
|
1444
|
+
onViewportChanged = { zoom, x, y -> session.update { it.copy(crop = it.crop.copy(zoom = zoom, panX = x, panY = y)) } }
|
|
1445
|
+
onMediaBoundsChanged = { bounds ->
|
|
1446
|
+
val fitted = videoMediaBounds(session, playerView, false)
|
|
1447
|
+
if (fitted.width() > 0) {
|
|
1448
|
+
playerView.pivotX = playerView.width / 2f; playerView.pivotY = playerView.height / 2f
|
|
1449
|
+
playerView.scaleX = bounds.width() / fitted.width(); playerView.scaleY = playerView.scaleX
|
|
1450
|
+
playerView.translationX = bounds.centerX() - fitted.centerX(); playerView.translationY = bounds.centerY() - fitted.centerY()
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
preview.addView(videoCropOverlay, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1455
|
+
preview.addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
|
|
1456
|
+
if (videoCropMode) videoCropOverlay.restore(videoMediaBounds(session, playerView, false), session.state.crop)
|
|
1457
|
+
}
|
|
1457
1458
|
val layerOverlay = LayerOverlayView(this)
|
|
1458
1459
|
videoLayerOverlay = layerOverlay
|
|
1459
1460
|
preview.addView(layerOverlay, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1461
|
+
layerOverlay.onLayerDelete = { id ->
|
|
1462
|
+
session.layerStack.commit { list -> list.filterNot { it.id == id } }
|
|
1463
|
+
selectVideoLayer(null, session); onVideoLayerStackChanged()
|
|
1464
|
+
}
|
|
1465
|
+
layerOverlay.onLayerDoubleTapped = { id ->
|
|
1466
|
+
session.layerStack.layers.firstOrNull { it.id == id && it.type == LayerType.TEXT }?.let { showVideoTextInputDialog(session, it) }
|
|
1467
|
+
}
|
|
1460
1468
|
layerOverlay.onLayerTapped = { id -> selectVideoLayer(id, session) }
|
|
1461
1469
|
layerOverlay.onLayerTransformChanged = { id, x, y, scale, rotation ->
|
|
1462
1470
|
if (videoDragStartSnapshot == null) videoDragStartSnapshot = session.layerStack.layers
|
|
@@ -1472,12 +1480,15 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1472
1480
|
onVideoLayerStackChanged()
|
|
1473
1481
|
}
|
|
1474
1482
|
|
|
1475
|
-
val
|
|
1476
|
-
|
|
1483
|
+
val accentColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1484
|
+
|
|
1485
|
+
val playPause = ImageButton(this).apply {
|
|
1477
1486
|
contentDescription = "Play video"
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1487
|
+
setImageResource(R.drawable.ic_play)
|
|
1488
|
+
setColorFilter(Color.WHITE)
|
|
1489
|
+
scaleType = ImageView.ScaleType.CENTER_INSIDE
|
|
1490
|
+
background = android.graphics.drawable.InsetDrawable(circleDrawable(Color.argb(140, 0, 0, 0)), dp(2))
|
|
1491
|
+
setPadding(dp(9), dp(9), dp(9), dp(9))
|
|
1481
1492
|
setOnClickListener {
|
|
1482
1493
|
if (session.player.isPlaying) {
|
|
1483
1494
|
session.player.pause()
|
|
@@ -1487,38 +1498,64 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1487
1498
|
}
|
|
1488
1499
|
session.player.play()
|
|
1489
1500
|
}
|
|
1501
|
+
setVideoControlsVisible(true)
|
|
1490
1502
|
}
|
|
1503
|
+
applyPressScale()
|
|
1491
1504
|
}
|
|
1492
1505
|
playPauseButton = playPause
|
|
1493
1506
|
val time = TextView(this).apply {
|
|
1494
1507
|
text = "0:00 / 0:00"
|
|
1495
|
-
setTextColor(
|
|
1496
|
-
textSize =
|
|
1508
|
+
setTextColor(Color.WHITE)
|
|
1509
|
+
textSize = 11f
|
|
1497
1510
|
}
|
|
1498
1511
|
timeLabel = time
|
|
1499
1512
|
val transport = LinearLayout(this).apply {
|
|
1500
1513
|
orientation = LinearLayout.HORIZONTAL
|
|
1501
1514
|
gravity = Gravity.CENTER_VERTICAL
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
addView(playPause, LinearLayout.LayoutParams(dp(72), WRAP))
|
|
1505
|
-
addView(time, LinearLayout.LayoutParams(WRAP, WRAP))
|
|
1515
|
+
addView(playPause, LinearLayout.LayoutParams(dp(44), dp(44)))
|
|
1516
|
+
addView(time, LinearLayout.LayoutParams(WRAP, WRAP).apply { marginStart = dp(DesignTokens.spaceSm) })
|
|
1506
1517
|
}
|
|
1507
|
-
root.addView(transport, LinearLayout.LayoutParams(MATCH, dp(40)))
|
|
1508
1518
|
|
|
1519
|
+
val seekTrack = android.graphics.drawable.LayerDrawable(
|
|
1520
|
+
arrayOf(
|
|
1521
|
+
roundedDrawable(Color.argb(90, 255, 255, 255), 999),
|
|
1522
|
+
android.graphics.drawable.ClipDrawable(roundedDrawable(accentColor, 999), Gravity.START, android.graphics.drawable.ClipDrawable.HORIZONTAL)
|
|
1523
|
+
)
|
|
1524
|
+
).apply {
|
|
1525
|
+
setId(0, android.R.id.background)
|
|
1526
|
+
setId(1, android.R.id.progress)
|
|
1527
|
+
setLayerInset(0, 0, dp(9), 0, dp(9))
|
|
1528
|
+
setLayerInset(1, 0, dp(9), 0, dp(9))
|
|
1529
|
+
}
|
|
1509
1530
|
val seek = SeekBar(this).apply {
|
|
1531
|
+
contentDescription = "Playback position"
|
|
1532
|
+
progressDrawable = seekTrack
|
|
1533
|
+
thumb = circleDrawable(Color.WHITE).apply { setSize(dp(12), dp(12)) }
|
|
1534
|
+
splitTrack = false
|
|
1510
1535
|
max = 1000
|
|
1511
1536
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
1512
1537
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
1513
1538
|
if (!fromUser || session.durationMs <= 0) return
|
|
1514
1539
|
session.seekToGlobalMs(session.durationMs * progress / 1000)
|
|
1540
|
+
updateTimeLabel()
|
|
1515
1541
|
}
|
|
1516
|
-
override fun onStartTrackingTouch(seekBar: SeekBar?) { seeking = true }
|
|
1517
|
-
override fun onStopTrackingTouch(seekBar: SeekBar?) { seeking = false }
|
|
1542
|
+
override fun onStartTrackingTouch(seekBar: SeekBar?) { seeking = true; setVideoControlsVisible(true) }
|
|
1543
|
+
override fun onStopTrackingTouch(seekBar: SeekBar?) { seeking = false; scheduleAutoHideVideoControls(session) }
|
|
1518
1544
|
})
|
|
1519
1545
|
}
|
|
1520
1546
|
positionSeekBar = seek
|
|
1521
|
-
|
|
1547
|
+
|
|
1548
|
+
val controlsOverlay = LinearLayout(this).apply {
|
|
1549
|
+
orientation = LinearLayout.VERTICAL
|
|
1550
|
+
setBackgroundColor(Color.argb(150, 0, 0, 0))
|
|
1551
|
+
setPadding(dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs))
|
|
1552
|
+
addView(transport, LinearLayout.LayoutParams(WRAP, WRAP))
|
|
1553
|
+
addView(seek, LinearLayout.LayoutParams(MATCH, dp(24)))
|
|
1554
|
+
}
|
|
1555
|
+
videoControlsOverlay = controlsOverlay
|
|
1556
|
+
preview.addView(controlsOverlay, FrameLayout.LayoutParams(MATCH, WRAP, Gravity.BOTTOM))
|
|
1557
|
+
preview.isClickable = true
|
|
1558
|
+
preview.setOnClickListener { toggleVideoControls(session) }
|
|
1522
1559
|
|
|
1523
1560
|
val trim = TrimRangeView(this).apply {
|
|
1524
1561
|
// Bound to the currently selected clip (Milestone 7): fractions are relative to that clip's
|
|
@@ -1565,6 +1602,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1565
1602
|
}
|
|
1566
1603
|
}
|
|
1567
1604
|
|
|
1605
|
+
val cropPanel = CropPanel(this, themeColor("primaryColor", DesignTokens.primaryContainer), toolbarColor, textColor,
|
|
1606
|
+
originalRatio = { session.sourceWidth.toFloat() / session.sourceHeight.coerceAtLeast(1) },
|
|
1607
|
+
onReset = { session.update { it.copy(crop = PhotoTransformState()) }; refreshVideoCrop(session) },
|
|
1608
|
+
onDone = { setVideoCropMode(false, session) },
|
|
1609
|
+
onCancel = { videoCropEntryState?.let { saved -> session.update { it.copy(crop = saved) } }; setVideoCropMode(false, session) },
|
|
1610
|
+
onStraighten = { value -> session.update { it.copy(crop = it.crop.withStraighten(value)) }; refreshVideoCrop(session) },
|
|
1611
|
+
onRotate = { session.update { it.copy(crop = it.crop.rotatedRight().copy(zoom = 1f, panX = 0f, panY = 0f, aspectRatio = null, aspectPreset = "Free")) }; refreshVideoCrop(session) },
|
|
1612
|
+
onRatio = { label, ratio -> session.update { it.copy(crop = it.crop.copy(aspectRatio = ratio, aspectPreset = label)) }; videoCropOverlay.setAspectRatio(ratio); videoCropPanel?.sync(session.state.crop) }
|
|
1613
|
+
).apply { visibility = View.GONE }
|
|
1614
|
+
videoCropPanel = cropPanel
|
|
1615
|
+
root.addView(cropPanel, LinearLayout.LayoutParams(MATCH, dp(CROP_SUB_BAR_HEIGHT_DP)))
|
|
1568
1616
|
val aspectBar = createVideoAspectSubBar(session, playerView)
|
|
1569
1617
|
aspectBar.visibility = View.GONE
|
|
1570
1618
|
videoAspectSubBar = aspectBar
|
|
@@ -1589,32 +1637,43 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1589
1637
|
videoFiltersSubBar = filtersBar
|
|
1590
1638
|
root.addView(filtersBar, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
1591
1639
|
|
|
1592
|
-
val layerPropertySeek =
|
|
1640
|
+
val layerPropertySeek = EditorToolSlider(this).apply {
|
|
1641
|
+
progressTintList = android.content.res.ColorStateList.valueOf(themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
1642
|
+
thumbTintList = progressTintList
|
|
1593
1643
|
visibility = View.GONE
|
|
1594
1644
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
1595
1645
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
1596
1646
|
if (fromUser) onVideoLayerPropertyChanged(progress, session)
|
|
1597
1647
|
}
|
|
1598
|
-
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
1599
|
-
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
1648
|
+
override fun onStartTrackingTouch(seekBar: SeekBar?) { videoDragStartSnapshot = session.layerStack.layers.map { it.copy() } }
|
|
1649
|
+
override fun onStopTrackingTouch(seekBar: SeekBar?) { videoDragStartSnapshot?.let { session.layerStack.commitSnapshot(it) }; videoDragStartSnapshot = null; onVideoLayerStackChanged() }
|
|
1600
1650
|
})
|
|
1601
1651
|
}
|
|
1602
1652
|
videoLayerPropertySeekBar = layerPropertySeek
|
|
1603
1653
|
root.addView(layerPropertySeek, LinearLayout.LayoutParams(MATCH, WRAP).apply { setMargins(dp(8), dp(4), dp(8), 0) })
|
|
1604
1654
|
|
|
1655
|
+
videoColorRow = createTextPalette(true) { color ->
|
|
1656
|
+
val id = selectedVideoLayerId ?: return@createTextPalette
|
|
1657
|
+
session.layerStack.commit { list -> list.map { if (it.id == id) it.copy(textColor = color) else it } }
|
|
1658
|
+
onVideoLayerStackChanged()
|
|
1659
|
+
setVideoLayerToolBarVisible(true, session)
|
|
1660
|
+
}.apply { visibility = View.GONE }
|
|
1661
|
+
root.addView(videoColorRow, LinearLayout.LayoutParams(MATCH, dp(48)))
|
|
1605
1662
|
val layerBar = createVideoLayerToolBar(session)
|
|
1606
1663
|
layerBar.visibility = View.GONE
|
|
1607
1664
|
videoLayerToolBar = layerBar
|
|
1608
|
-
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(
|
|
1665
|
+
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(76)))
|
|
1609
1666
|
|
|
1610
|
-
|
|
1667
|
+
// The dock builds its own horizontal inset and pill; this holder only reserves the row height
|
|
1668
|
+
// (plus an 8dp gap) so the system-bar inset lands on transparent space, not on the pill.
|
|
1669
|
+
val toolbarHeight = dp(VIDEO_DOCK_BAR_HEIGHT_DP)
|
|
1611
1670
|
val toolbar = createVideoToolBar(session, playerView)
|
|
1612
1671
|
videoToolBar = toolbar
|
|
1613
1672
|
root.addView(toolbar, LinearLayout.LayoutParams(MATCH, toolbarHeight))
|
|
1614
1673
|
applySystemBarInsets(
|
|
1615
1674
|
header,
|
|
1616
1675
|
headerHeight,
|
|
1617
|
-
listOf(toolbar to toolbarHeight, aspectBar to dp(CROP_SUB_BAR_HEIGHT_DP), filtersBar to dp(56), layerBar to dp(
|
|
1676
|
+
listOf(cropPanel to dp(CROP_SUB_BAR_HEIGHT_DP), toolbar to toolbarHeight, aspectBar to dp(CROP_SUB_BAR_HEIGHT_DP), filtersBar to dp(56), layerBar to dp(76))
|
|
1618
1677
|
)
|
|
1619
1678
|
|
|
1620
1679
|
startPositionPolling(session)
|
|
@@ -1622,6 +1681,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1622
1681
|
}
|
|
1623
1682
|
|
|
1624
1683
|
private fun startPositionPolling(session: VideoEditSession) {
|
|
1684
|
+
var wasPlaying = false
|
|
1625
1685
|
val runnable = object : Runnable {
|
|
1626
1686
|
override fun run() {
|
|
1627
1687
|
if (!seeking && session.durationMs > 0) {
|
|
@@ -1629,8 +1689,12 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1629
1689
|
updateTimeLabel()
|
|
1630
1690
|
}
|
|
1631
1691
|
val playing = session.player.isPlaying
|
|
1632
|
-
playPauseButton.
|
|
1692
|
+
playPauseButton.setImageResource(if (playing) R.drawable.ic_pause else R.drawable.ic_play)
|
|
1633
1693
|
playPauseButton.contentDescription = if (playing) "Pause video" else "Play video"
|
|
1694
|
+
if (playing != wasPlaying) {
|
|
1695
|
+
wasPlaying = playing
|
|
1696
|
+
if (playing) scheduleAutoHideVideoControls(session) else setVideoControlsVisible(true)
|
|
1697
|
+
}
|
|
1634
1698
|
refreshVideoOverlayPreview(session)
|
|
1635
1699
|
positionPollHandler.postDelayed(this, 250)
|
|
1636
1700
|
}
|
|
@@ -1639,6 +1703,35 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1639
1703
|
positionPollHandler.postDelayed(runnable, 250)
|
|
1640
1704
|
}
|
|
1641
1705
|
|
|
1706
|
+
/** Shows/hides the in-preview playback controls with a subtle fade (spec: auto-hide while playing). */
|
|
1707
|
+
private fun setVideoControlsVisible(visible: Boolean) {
|
|
1708
|
+
if (!::videoControlsOverlay.isInitialized) return
|
|
1709
|
+
positionPollHandler.removeCallbacks(hideVideoControlsRunnable)
|
|
1710
|
+
videoControlsVisible = visible
|
|
1711
|
+
videoControlsOverlay.animate().cancel()
|
|
1712
|
+
if (visible) {
|
|
1713
|
+
videoControlsOverlay.visibility = View.VISIBLE
|
|
1714
|
+
videoControlsOverlay.animate().alpha(1f).setDuration(200).start()
|
|
1715
|
+
} else {
|
|
1716
|
+
videoControlsOverlay.animate().alpha(0f).setDuration(200)
|
|
1717
|
+
.withEndAction { if (!videoControlsVisible) videoControlsOverlay.visibility = View.INVISIBLE }.start()
|
|
1718
|
+
}
|
|
1719
|
+
}
|
|
1720
|
+
|
|
1721
|
+
private fun scheduleAutoHideVideoControls(session: VideoEditSession) {
|
|
1722
|
+
positionPollHandler.removeCallbacks(hideVideoControlsRunnable)
|
|
1723
|
+
if (session.player.isPlaying) positionPollHandler.postDelayed(hideVideoControlsRunnable, 2500)
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
private fun toggleVideoControls(session: VideoEditSession) {
|
|
1727
|
+
if (videoControlsVisible) {
|
|
1728
|
+
setVideoControlsVisible(false)
|
|
1729
|
+
} else {
|
|
1730
|
+
setVideoControlsVisible(true)
|
|
1731
|
+
scheduleAutoHideVideoControls(session)
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1642
1735
|
private fun updateTimeLabel() {
|
|
1643
1736
|
val session = videoSession ?: return
|
|
1644
1737
|
timeLabel.text = "${formatMs(session.globalPositionMs())} / ${formatMs(session.durationMs)}"
|
|
@@ -1668,9 +1761,16 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1668
1761
|
videoOverlayRenderKey = null
|
|
1669
1762
|
return
|
|
1670
1763
|
}
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1764
|
+
// Deliberately NOT `session.player.videoSize`: ExoPlayer's live-reported size is an
|
|
1765
|
+
// independent source of truth from `videoOutputSize()` (which drives `computeVideoLetterboxBounds()`
|
|
1766
|
+
// above, i.e. the touch/gesture-normalization bounds for `videoLayerOverlay`). The two can disagree
|
|
1767
|
+
// (e.g. before the first decoded frame reports a size, or if the effects pipeline republishes a
|
|
1768
|
+
// different size under crop), which silently shifts a layer's normalized x/y/scale relative to what
|
|
1769
|
+
// the user saw on screen. Sourcing both from the same deterministic function keeps preview (this
|
|
1770
|
+
// bitmap) and gesture math in lockstep, and matches what `VideoExporter` computes for the real frame.
|
|
1771
|
+
val (sourceWidthF, sourceHeightF) = videoOutputSize(session, cropped = !videoCropMode)
|
|
1772
|
+
val sourceWidth = sourceWidthF.roundToInt().coerceAtLeast(1)
|
|
1773
|
+
val sourceHeight = sourceHeightF.roundToInt().coerceAtLeast(1)
|
|
1674
1774
|
val previewScale = minOf(1f, 1280f / maxOf(sourceWidth, sourceHeight))
|
|
1675
1775
|
val width = (sourceWidth * previewScale).roundToInt().coerceAtLeast(2)
|
|
1676
1776
|
val height = (sourceHeight * previewScale).roundToInt().coerceAtLeast(2)
|
|
@@ -1696,23 +1796,62 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1696
1796
|
return bitmap
|
|
1697
1797
|
}
|
|
1698
1798
|
|
|
1699
|
-
/**
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1799
|
+
/**
|
|
1800
|
+
* Where the video actually renders within [view] under `FIT_CENTER`-style letterboxing (mirrors
|
|
1801
|
+
* `ZoomableImageView.resetToFit`'s math). This is the SINGLE source of truth for overlay-relative
|
|
1802
|
+
* coordinates: [computeVideoLetterboxBounds] (touch/gesture normalization) and
|
|
1803
|
+
* [refreshVideoOverlayPreview] (the bitmap layers actually draw into) both derive their size from
|
|
1804
|
+
* here, so a layer's normalized x/y/scale always means the same on-screen place in both.
|
|
1805
|
+
*
|
|
1806
|
+
* Known gap (not currently reachable from the UI — the video tool rail only exposes Crop/Text/
|
|
1807
|
+
* Stickers, not a whole-video "Rotate" tool): [VideoTransformState.rotationDegrees] (the top-level
|
|
1808
|
+
* rotate field, distinct from `crop.rotationDegrees` handled below) is applied to `playerView` as a
|
|
1809
|
+
* cosmetic `View.rotation` only and is never folded in here, while `VideoExporter` bakes it into the
|
|
1810
|
+
* real output frame (swapping width/height for 90/270°). If that tool is ever wired back up, this
|
|
1811
|
+
* function's width/height swap must also account for `state.rotationDegrees`, or preview and export
|
|
1812
|
+
* will disagree the same way this function's fix for `crop.rotationDegrees` was needed.
|
|
1813
|
+
*/
|
|
1814
|
+
private fun videoOutputSize(session: VideoEditSession, cropped: Boolean): Pair<Float, Float> {
|
|
1815
|
+
var w = session.sourceWidth.coerceAtLeast(1).toFloat(); var h = session.sourceHeight.coerceAtLeast(1).toFloat()
|
|
1816
|
+
val crop = session.state.crop
|
|
1817
|
+
if (crop.rotationDegrees % 180 != 0) { val swap = w; w = h; h = swap }
|
|
1818
|
+
return if (cropped) w * (crop.cropRight - crop.cropLeft) to h * (crop.cropBottom - crop.cropTop) else w to h
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
private fun videoMediaBounds(session: VideoEditSession, view: View, cropped: Boolean): android.graphics.RectF {
|
|
1822
|
+
val (w, h) = videoOutputSize(session, cropped)
|
|
1823
|
+
val scale = minOf(view.width / w, view.height / h)
|
|
1824
|
+
val x = (view.width - w * scale) / 2; val y = (view.height - h * scale) / 2
|
|
1825
|
+
return android.graphics.RectF(x, y, x + w * scale, y + h * scale)
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
private fun computeVideoLetterboxBounds(session: VideoEditSession, view: View): android.graphics.RectF = videoMediaBounds(session, view, !videoCropMode)
|
|
1829
|
+
|
|
1830
|
+
private fun refreshVideoCrop(session: VideoEditSession) {
|
|
1831
|
+
session.player.setVideoEffects(com.photovideoeditor.video.render.VideoCropEffects.create(session.state.crop, !videoCropMode))
|
|
1832
|
+
videoCropPanel?.sync(session.state.crop)
|
|
1833
|
+
cropPlayerView.scaleX = 1f; cropPlayerView.scaleY = 1f; cropPlayerView.translationX = 0f; cropPlayerView.translationY = 0f
|
|
1834
|
+
if (videoCropMode) videoCropOverlay.restore(videoMediaBounds(session, cropPlayerView, false), session.state.crop)
|
|
1835
|
+
videoOverlayRenderKey = null
|
|
1836
|
+
refreshVideoOverlayPreview(session)
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
private fun setVideoCropMode(enabled: Boolean, session: VideoEditSession) {
|
|
1840
|
+
if (enabled && !videoCropMode) {
|
|
1841
|
+
videoCropEntryState = session.state.crop
|
|
1842
|
+
session.player.pause()
|
|
1843
|
+
selectVideoLayer(null, session)
|
|
1706
1844
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1845
|
+
videoCropMode = enabled
|
|
1846
|
+
videoCropPanel?.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1847
|
+
videoCropOverlay.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1848
|
+
videoLayerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1849
|
+
videoOverlayImageView.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1850
|
+
videoToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1851
|
+
if (!enabled) videoCropEntryState = null
|
|
1852
|
+
refreshVideoCrop(session)
|
|
1713
1853
|
}
|
|
1714
1854
|
|
|
1715
|
-
/** Mirrors [onLayerStackChanged] for the video overlay stack (Milestone 6). */
|
|
1716
1855
|
private fun onVideoLayerStackChanged() {
|
|
1717
1856
|
val session = videoSession ?: return
|
|
1718
1857
|
if (selectedVideoLayerId != null && session.layerStack.layers.none { it.id == selectedVideoLayerId }) {
|
|
@@ -1728,20 +1867,25 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1728
1867
|
}
|
|
1729
1868
|
|
|
1730
1869
|
private fun selectVideoLayer(id: String?, session: VideoEditSession) {
|
|
1870
|
+
if (selectedVideoLayerId != id) activeVideoLayerPropertyKey = ""
|
|
1731
1871
|
selectedVideoLayerId = id
|
|
1732
1872
|
videoLayerOverlay.selectedLayerId = id
|
|
1733
1873
|
setVideoLayerToolBarVisible(id != null, session)
|
|
1734
1874
|
}
|
|
1735
1875
|
|
|
1736
1876
|
private fun setVideoLayerToolBarVisible(visible: Boolean, session: VideoEditSession) {
|
|
1877
|
+
if (visible) setVideoControlsVisible(true)
|
|
1737
1878
|
videoLayerToolBar.visibility = if (visible) View.VISIBLE else View.GONE
|
|
1738
|
-
videoLayerPropertySeekBar.visibility = if (visible) View.VISIBLE else View.GONE
|
|
1739
1879
|
videoToolBar.visibility = if (visible) View.GONE else View.VISIBLE
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1880
|
+
val isText = currentSelectedVideoLayer(session)?.type == LayerType.TEXT
|
|
1881
|
+
videoLayerPropertyButtons["scale"]?.root?.visibility = if (isText || currentSelectedVideoLayer(session)?.type == LayerType.STICKER) View.GONE else View.VISIBLE
|
|
1882
|
+
listOf("edit", "color", "fontSize").forEach { videoLayerPropertyButtons[it]?.root?.visibility = if (isText) View.VISIBLE else View.GONE }
|
|
1883
|
+
if (!isText && activeVideoLayerPropertyKey in setOf("color", "fontSize")) activeVideoLayerPropertyKey = ""
|
|
1884
|
+
videoLayerPropertySeekBar.visibility = if (visible && activeVideoLayerPropertyKey in setOf("scale", "rotation", "opacity", "fontSize")) View.VISIBLE else View.GONE
|
|
1885
|
+
videoColorRow.visibility = if (visible && activeVideoLayerPropertyKey == "color") View.VISIBLE else View.GONE
|
|
1886
|
+
if (visible) syncVideoLayerPropertySeekBar(session)
|
|
1887
|
+
videoTextSwatches.forEach { (color, view) -> view.background = roundedDrawable(DesignTokens.surfaceContainerHigh, 24).apply { if (color == currentSelectedVideoLayer(session)?.textColor) setStroke(dp(2), themeColor("primaryColor", DesignTokens.primaryContainer)) } }
|
|
1888
|
+
refreshToolNodeSelection(videoLayerPropertyButtons, activeVideoLayerPropertyKey, Color.WHITE)
|
|
1745
1889
|
}
|
|
1746
1890
|
|
|
1747
1891
|
private fun currentSelectedVideoLayer(session: VideoEditSession): PhotoLayer? =
|
|
@@ -1750,12 +1894,14 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1750
1894
|
private fun syncVideoLayerPropertySeekBar(session: VideoEditSession) {
|
|
1751
1895
|
val layer = currentSelectedVideoLayer(session) ?: return
|
|
1752
1896
|
val (min, max, value) = when (activeVideoLayerPropertyKey) {
|
|
1753
|
-
"rotation" -> Triple(-180f, 180f, layer.rotationDegrees)
|
|
1897
|
+
"rotation" -> Triple(-180f, 180f, ((layer.rotationDegrees % 360f + 540f) % 360f) - 180f)
|
|
1754
1898
|
"opacity" -> Triple(0f, 100f, layer.opacity * 100f)
|
|
1899
|
+
"fontSize" -> Triple(12f, 160f, layer.fontSize)
|
|
1755
1900
|
"start" -> Triple(0f, session.durationMs.toFloat(), layer.startMs.toFloat())
|
|
1756
1901
|
"end" -> Triple(0f, session.durationMs.toFloat(), session.durationMs.effectiveEndOr(layer.endMs).toFloat())
|
|
1757
1902
|
else -> Triple(20f, 800f, layer.scale * 100f)
|
|
1758
1903
|
}
|
|
1904
|
+
(videoLayerPropertySeekBar as? EditorToolSlider)?.propertyKey = activeVideoLayerPropertyKey
|
|
1759
1905
|
videoLayerPropertySeekBar.max = (max - min).roundToInt()
|
|
1760
1906
|
videoLayerPropertySeekBar.progress = (value - min).roundToInt()
|
|
1761
1907
|
}
|
|
@@ -1767,6 +1913,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1767
1913
|
val min = when (activeVideoLayerPropertyKey) {
|
|
1768
1914
|
"rotation" -> -180f
|
|
1769
1915
|
"opacity", "start", "end" -> 0f
|
|
1916
|
+
"fontSize" -> 12f
|
|
1770
1917
|
else -> 20f
|
|
1771
1918
|
}
|
|
1772
1919
|
val value = min + progress
|
|
@@ -1776,6 +1923,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1776
1923
|
when (activeVideoLayerPropertyKey) {
|
|
1777
1924
|
"rotation" -> layer.copy(rotationDegrees = value)
|
|
1778
1925
|
"opacity" -> layer.copy(opacity = value / 100f)
|
|
1926
|
+
"fontSize" -> layer.copy(fontSize = value)
|
|
1779
1927
|
"start" -> layer.copy(startMs = value.toLong().coerceAtMost(layer.endMs.takeIf { it > 0 } ?: session.durationMs))
|
|
1780
1928
|
"end" -> layer.copy(endMs = value.toLong().coerceAtLeast(layer.startMs))
|
|
1781
1929
|
else -> layer.copy(scale = value / 100f)
|
|
@@ -1785,94 +1933,102 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1785
1933
|
refreshVideoOverlayPreview(session)
|
|
1786
1934
|
}
|
|
1787
1935
|
|
|
1788
|
-
private fun createVideoLayerToolBar(session: VideoEditSession): LinearLayout {
|
|
1789
|
-
val
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1936
|
+
private fun createVideoLayerToolBar(session: VideoEditSession): LinearLayout = createContextToolbar(true) { key ->
|
|
1937
|
+
val layer = currentSelectedVideoLayer(session) ?: return@createContextToolbar
|
|
1938
|
+
when (key) {
|
|
1939
|
+
"edit" -> showVideoTextInputDialog(session, layer)
|
|
1940
|
+
"done" -> selectVideoLayer(null, session)
|
|
1941
|
+
"delete" -> { session.layerStack.commit { it.filterNot { item -> item.id == layer.id } }; selectVideoLayer(null, session); onVideoLayerStackChanged() }
|
|
1942
|
+
"duplicate" -> {
|
|
1943
|
+
val copy = layer.copy(id = java.util.UUID.randomUUID().toString(), x = (layer.x + 0.04f).coerceIn(0f, 1f), y = (layer.y + 0.04f).coerceIn(0f, 1f))
|
|
1944
|
+
session.layerStack.commit { it + copy }; selectVideoLayer(copy.id, session); onVideoLayerStackChanged()
|
|
1945
|
+
}
|
|
1946
|
+
else -> { activeVideoLayerPropertyKey = if (activeVideoLayerPropertyKey == key) "" else key; setVideoLayerToolBarVisible(true, session) }
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
private fun showVideoTextInputDialog(session: VideoEditSession, editingLayer: PhotoLayer? = null) {
|
|
1951
|
+
showTextEditor(editingLayer) { text ->
|
|
1952
|
+
val layer = editingLayer?.copy(text = text) ?: PhotoLayer(type = LayerType.TEXT, text = text)
|
|
1953
|
+
session.layerStack.commit { list -> if (editingLayer == null) list + layer else list.map { if (it.id == layer.id) layer else it } }
|
|
1954
|
+
onVideoLayerStackChanged()
|
|
1955
|
+
selectVideoLayer(layer.id, session)
|
|
1956
|
+
}
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
/**
|
|
1960
|
+
* Compact video dock: two equal-width icon+label actions in one rounded pill. Deliberately not
|
|
1961
|
+
* [createToolNode] — that rail stacks a 44dp tile above a label (76dp of chrome), which is far more
|
|
1962
|
+
* than two actions warrant. Returns a transparent holder so [applySystemBarInsets] can pad the
|
|
1963
|
+
* gesture/nav inset without stretching the pill itself into the system bar.
|
|
1964
|
+
*/
|
|
1965
|
+
private fun createVideoToolBar(session: VideoEditSession, playerView: PlayerView): View {
|
|
1966
|
+
val features = request.optJSONObject("features")
|
|
1967
|
+
val dock = LinearLayout(this).apply {
|
|
1968
|
+
orientation = LinearLayout.HORIZONTAL
|
|
1794
1969
|
gravity = Gravity.CENTER_VERTICAL
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1800
|
-
orientation = LinearLayout.HORIZONTAL
|
|
1801
|
-
properties.forEach { (key, label) ->
|
|
1802
|
-
val chip = actionButton(label) {
|
|
1803
|
-
activeVideoLayerPropertyKey = key
|
|
1804
|
-
syncVideoLayerPropertySeekBar(session)
|
|
1805
|
-
refreshSelection(videoLayerPropertyButtons, activeVideoLayerPropertyKey, textColor, primaryColor)
|
|
1806
|
-
}.apply { setTextColor(textColor) }
|
|
1807
|
-
videoLayerPropertyButtons[key] = chip
|
|
1808
|
-
addView(chip, LinearLayout.LayoutParams(dp(72), MATCH))
|
|
1809
|
-
}
|
|
1970
|
+
background = roundedDrawable(DesignTokens.surfaceContainer, VIDEO_DOCK_RADIUS_DP)
|
|
1971
|
+
listOf("text" to "Text", "stickers" to "Stickers")
|
|
1972
|
+
.filter { features?.optBoolean(it.first, true) != false }
|
|
1973
|
+
.forEach { (key, label) ->
|
|
1810
1974
|
addView(
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
session.layerStack.commit { list -> list.filterNot { it.id == id } }
|
|
1814
|
-
selectVideoLayer(null, session)
|
|
1815
|
-
onVideoLayerStackChanged()
|
|
1816
|
-
}.apply { setTextColor(textColor) },
|
|
1817
|
-
LinearLayout.LayoutParams(dp(80), MATCH)
|
|
1975
|
+
createVideoDockAction(key, label) { onVideoToolTapped(key, session, playerView) },
|
|
1976
|
+
LinearLayout.LayoutParams(0, MATCH, 1f)
|
|
1818
1977
|
)
|
|
1819
|
-
}
|
|
1820
|
-
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return FrameLayout(this).apply {
|
|
1821
1981
|
addView(
|
|
1822
|
-
|
|
1823
|
-
|
|
1982
|
+
dock,
|
|
1983
|
+
FrameLayout.LayoutParams(MATCH, dp(VIDEO_DOCK_HEIGHT_DP), Gravity.TOP).apply {
|
|
1984
|
+
setMargins(dp(DesignTokens.spaceMd), 0, dp(DesignTokens.spaceMd), 0)
|
|
1985
|
+
}
|
|
1824
1986
|
)
|
|
1825
1987
|
}
|
|
1826
1988
|
}
|
|
1827
1989
|
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
.
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
}
|
|
1990
|
+
/** One dock action: icon + label on a single row, purple pill while pressed. */
|
|
1991
|
+
private fun createVideoDockAction(key: String, labelText: String, onClick: () -> Unit): LinearLayout {
|
|
1992
|
+
val icon: View = if (key == "text") {
|
|
1993
|
+
// A literal "Aa" reads as typography far better than R.drawable.ic_title, which is a lone
|
|
1994
|
+
// oversized "T".
|
|
1995
|
+
TextView(this).apply {
|
|
1996
|
+
text = "Aa"
|
|
1997
|
+
textSize = 15f
|
|
1998
|
+
includeFontPadding = false
|
|
1999
|
+
gravity = Gravity.CENTER
|
|
2000
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
2001
|
+
setTextColor(DesignTokens.textPrimary)
|
|
1841
2002
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
2003
|
+
} else {
|
|
2004
|
+
ImageView(this).apply {
|
|
2005
|
+
setImageResource(R.drawable.ic_sentiment_satisfied)
|
|
2006
|
+
setColorFilter(DesignTokens.textPrimary)
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
val accent = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1848
2010
|
return LinearLayout(this).apply {
|
|
1849
|
-
orientation = LinearLayout.
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
)
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
val node = createToolNode(iconRes, label) { onVideoToolTapped(key, session, playerView) }
|
|
1871
|
-
videoToolButtons[key] = node
|
|
1872
|
-
addView(node.root, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) })
|
|
1873
|
-
}
|
|
1874
|
-
})
|
|
1875
|
-
}, LinearLayout.LayoutParams(MATCH, WRAP))
|
|
2011
|
+
orientation = LinearLayout.HORIZONTAL
|
|
2012
|
+
gravity = Gravity.CENTER
|
|
2013
|
+
isClickable = true
|
|
2014
|
+
isFocusable = true
|
|
2015
|
+
contentDescription = labelText
|
|
2016
|
+
// A StateListDrawable rather than a touch listener, so it composes with applyPressScale()
|
|
2017
|
+
// (which installs its own OnTouchListener).
|
|
2018
|
+
background = android.graphics.drawable.StateListDrawable().apply {
|
|
2019
|
+
addState(intArrayOf(android.R.attr.state_pressed), roundedDrawable(accent, VIDEO_DOCK_RADIUS_DP))
|
|
2020
|
+
addState(intArrayOf(), roundedDrawable(Color.TRANSPARENT, VIDEO_DOCK_RADIUS_DP))
|
|
2021
|
+
}
|
|
2022
|
+
addView(icon, LinearLayout.LayoutParams(dp(22), dp(22)).apply { marginEnd = dp(DesignTokens.spaceSm) })
|
|
2023
|
+
addView(TextView(this@PhotoVideoEditorActivity).apply {
|
|
2024
|
+
text = labelText
|
|
2025
|
+
textSize = 13f
|
|
2026
|
+
maxLines = 1
|
|
2027
|
+
includeFontPadding = false
|
|
2028
|
+
setTextColor(DesignTokens.textPrimary)
|
|
2029
|
+
})
|
|
2030
|
+
setOnClickListener { onClick() }
|
|
2031
|
+
applyPressScale()
|
|
1876
2032
|
}
|
|
1877
2033
|
}
|
|
1878
2034
|
|
|
@@ -1890,6 +2046,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1890
2046
|
|
|
1891
2047
|
private fun onVideoToolTapped(key: String, session: VideoEditSession, playerView: PlayerView) {
|
|
1892
2048
|
when (key) {
|
|
2049
|
+
"crop" -> setVideoCropMode(true, session)
|
|
1893
2050
|
"cover" -> {
|
|
1894
2051
|
val globalMs = session.globalPositionMs()
|
|
1895
2052
|
session.update { it.withCoverFrame(globalMs) }
|
|
@@ -1915,7 +2072,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1915
2072
|
|
|
1916
2073
|
private fun showVideoStickerPicker(session: VideoEditSession) {
|
|
1917
2074
|
OnlineStickerSheet(this, runtimeStickerAssets()) { path ->
|
|
1918
|
-
val layer =
|
|
2075
|
+
val layer = newSticker(stickerUri = Uri.fromFile(File(path)).toString())
|
|
1919
2076
|
session.layerStack.commit { it + layer }
|
|
1920
2077
|
onVideoLayerStackChanged()
|
|
1921
2078
|
selectVideoLayer(layer.id, session)
|
|
@@ -2307,7 +2464,9 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2307
2464
|
|
|
2308
2465
|
addView(TextView(this@PhotoVideoEditorActivity).apply {
|
|
2309
2466
|
text = if (mediaType == "photo") "Photo Editor" else "Video Editor"
|
|
2310
|
-
textSize =
|
|
2467
|
+
textSize = 14f
|
|
2468
|
+
maxLines = 1
|
|
2469
|
+
ellipsize = android.text.TextUtils.TruncateAt.END
|
|
2311
2470
|
gravity = Gravity.CENTER
|
|
2312
2471
|
setTextColor(textColor)
|
|
2313
2472
|
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
|
@@ -2321,25 +2480,16 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2321
2480
|
if (mediaType == "photo") { photoSession?.layerStack?.redo(); onLayerStackChanged() }
|
|
2322
2481
|
else { videoSession?.layerStack?.redo(); onVideoLayerStackChanged() }
|
|
2323
2482
|
}
|
|
2483
|
+
undo.isEnabled = false; undo.alpha = 0.4f
|
|
2484
|
+
redo.isEnabled = false; redo.alpha = 0.4f
|
|
2324
2485
|
undoButton = undo
|
|
2325
2486
|
redoButton = redo
|
|
2326
2487
|
addView(undo, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2327
2488
|
addView(redo, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2328
2489
|
|
|
2329
|
-
if (mediaType == "photo") {
|
|
2330
|
-
addView(createIconButton(R.drawable.ic_compare, "Hold to compare with original", textColor) {}.apply {
|
|
2331
|
-
setOnTouchListener { _, event ->
|
|
2332
|
-
when (event.actionMasked) {
|
|
2333
|
-
MotionEvent.ACTION_DOWN -> showOriginalPreview(true)
|
|
2334
|
-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> showOriginalPreview(false)
|
|
2335
|
-
}
|
|
2336
|
-
true
|
|
2337
|
-
}
|
|
2338
|
-
}, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2339
|
-
}
|
|
2340
2490
|
|
|
2341
2491
|
addView(
|
|
2342
|
-
editorPrimaryButton(this@PhotoVideoEditorActivity, "Export",
|
|
2492
|
+
editorPrimaryButton(this@PhotoVideoEditorActivity, "Export", null, primaryColor, onPrimaryColor) {
|
|
2343
2493
|
if (mediaType == "photo") showPhotoExportConfiguration() else finishDone()
|
|
2344
2494
|
},
|
|
2345
2495
|
LinearLayout.LayoutParams(WRAP, dp(DesignTokens.touchTargetMin)).apply { marginStart = dp(DesignTokens.spaceXs) }
|
|
@@ -2461,17 +2611,20 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2461
2611
|
val label = TextView(this).apply {
|
|
2462
2612
|
text = labelText
|
|
2463
2613
|
textSize = 11f
|
|
2614
|
+
maxLines = 1
|
|
2615
|
+
includeFontPadding = false
|
|
2464
2616
|
gravity = Gravity.CENTER
|
|
2465
2617
|
setTextColor(DesignTokens.outline)
|
|
2466
|
-
setPadding(0, dp(
|
|
2618
|
+
setPadding(0, dp(2), 0, 0)
|
|
2467
2619
|
}
|
|
2468
2620
|
val root = LinearLayout(this).apply {
|
|
2469
2621
|
orientation = LinearLayout.VERTICAL
|
|
2470
2622
|
gravity = Gravity.CENTER_HORIZONTAL
|
|
2471
2623
|
isClickable = true
|
|
2472
2624
|
isFocusable = true
|
|
2625
|
+
contentDescription = labelText
|
|
2473
2626
|
background = roundedDrawable(Color.TRANSPARENT, DesignTokens.radiusLg)
|
|
2474
|
-
setPadding(0, dp(
|
|
2627
|
+
setPadding(0, dp(2), 0, dp(2))
|
|
2475
2628
|
addView(tile, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2476
2629
|
addView(label, LinearLayout.LayoutParams(WRAP, WRAP))
|
|
2477
2630
|
setOnClickListener { onClick() }
|
|
@@ -2481,17 +2634,23 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2481
2634
|
}
|
|
2482
2635
|
|
|
2483
2636
|
/** Highlights a tool-rail node the same way the mockups do: filled tile + text/icon tint switching to the theme's text color. */
|
|
2637
|
+
/**
|
|
2638
|
+
* Selection is a purple icon + label over a faint purple disc — not a filled purple tile, which at
|
|
2639
|
+
* dock size reads as a large button and fights the photo for attention.
|
|
2640
|
+
*/
|
|
2484
2641
|
private fun setToolNodeSelected(node: ToolNode, selected: Boolean, textColor: Int) {
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
)
|
|
2489
|
-
node.
|
|
2490
|
-
node.label.setTextColor(if (selected) DesignTokens.primary else DesignTokens.outline)
|
|
2642
|
+
val accent = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
2643
|
+
node.tile.background = if (selected) circleDrawable(DesignTokens.withAlphaPercent(accent, 18)) else null
|
|
2644
|
+
node.root.isSelected = selected
|
|
2645
|
+
node.icon.setColorFilter(if (selected) accent else DesignTokens.textSecondary)
|
|
2646
|
+
node.label.setTextColor(if (selected) accent else DesignTokens.textSecondary)
|
|
2491
2647
|
}
|
|
2492
2648
|
|
|
2493
2649
|
private fun <K> refreshToolNodeSelection(nodes: Map<K, ToolNode>, activeKey: K?, textColor: Int) {
|
|
2494
|
-
nodes.forEach { (key, node) ->
|
|
2650
|
+
nodes.forEach { (key, node) ->
|
|
2651
|
+
setToolNodeSelected(node, key == activeKey, textColor)
|
|
2652
|
+
if (key == "delete") { node.icon.setColorFilter(Color.rgb(255, 100, 115)); node.label.setTextColor(Color.rgb(255, 100, 115)) }
|
|
2653
|
+
}
|
|
2495
2654
|
}
|
|
2496
2655
|
|
|
2497
2656
|
private fun showPreviewError(container: FrameLayout) {
|
|
@@ -2540,7 +2699,13 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2540
2699
|
}
|
|
2541
2700
|
|
|
2542
2701
|
@Deprecated("Deprecated in Java")
|
|
2543
|
-
override fun onBackPressed()
|
|
2702
|
+
override fun onBackPressed() {
|
|
2703
|
+
if (cropMode) photoSession?.let { cancelCropMode(it, photoImageView, cropOverlay) }
|
|
2704
|
+
else if (videoCropMode) videoSession?.let { session ->
|
|
2705
|
+
videoCropEntryState?.let { saved -> session.update { it.copy(crop = saved) } }; setVideoCropMode(false, session)
|
|
2706
|
+
}
|
|
2707
|
+
else confirmDiscardChanges()
|
|
2708
|
+
}
|
|
2544
2709
|
|
|
2545
2710
|
override fun onTrimMemory(level: Int) {
|
|
2546
2711
|
super.onTrimMemory(level)
|
|
@@ -2573,12 +2738,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2573
2738
|
|
|
2574
2739
|
companion object {
|
|
2575
2740
|
/** Rotate icon row (44dp) + aspect-preset chip row (56dp). */
|
|
2576
|
-
const val CROP_SUB_BAR_HEIGHT_DP =
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2741
|
+
const val CROP_SUB_BAR_HEIGHT_DP = 232
|
|
2742
|
+
/** Header + shared slider + adjustment strip; see EditorPanelMetrics for the breakdown. */
|
|
2743
|
+
const val ADJUST_SUB_BAR_HEIGHT_DP = EditorPanelMetrics.TOTAL_HEIGHT_DP
|
|
2744
|
+
/** Same skeleton as Adjust, with a taller strip for the thumbnail + label. */
|
|
2745
|
+
const val FILTERS_SUB_BAR_HEIGHT_DP = EditorPanelMetrics.TOTAL_HEIGHT_DP + 24
|
|
2580
2746
|
/** 64dp asset card + optional caption + vertical padding, for the Stickers/Shapes grid. */
|
|
2581
2747
|
const val ASSET_GRID_BAR_HEIGHT_DP = 104
|
|
2748
|
+
/** Compact video dock (Text/Stickers pill) and the gap between it and the safe-area edge. */
|
|
2749
|
+
const val VIDEO_DOCK_HEIGHT_DP = 52
|
|
2750
|
+
const val VIDEO_DOCK_RADIUS_DP = 18
|
|
2751
|
+
const val VIDEO_DOCK_BAR_HEIGHT_DP = VIDEO_DOCK_HEIGHT_DP + 8
|
|
2582
2752
|
private const val TIMELINE_THUMBNAIL_COUNT = 12
|
|
2583
2753
|
const val EXTRA_URI = "photoVideoEditor.uri"
|
|
2584
2754
|
const val EXTRA_TYPE = "photoVideoEditor.type"
|