react-native-photo-video-editor 0.1.0 → 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 +20 -0
- package/PhotoVideoEditor.podspec +1 -0
- package/README.md +10 -6
- package/THIRD_PARTY_NOTICES.md +20 -0
- package/android/build.gradle +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
- package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
- package/android/src/main/assets/Stickers/LOL_1.png +0 -0
- package/android/src/main/assets/Stickers/LOL_10.png +0 -0
- package/android/src/main/assets/Stickers/LOL_11.png +0 -0
- package/android/src/main/assets/Stickers/LOL_12.png +0 -0
- package/android/src/main/assets/Stickers/LOL_13.png +0 -0
- package/android/src/main/assets/Stickers/LOL_14.png +0 -0
- package/android/src/main/assets/Stickers/LOL_15.png +0 -0
- package/android/src/main/assets/Stickers/LOL_16.png +0 -0
- package/android/src/main/assets/Stickers/LOL_17.png +0 -0
- package/android/src/main/assets/Stickers/LOL_18.png +0 -0
- package/android/src/main/assets/Stickers/LOL_19.png +0 -0
- package/android/src/main/assets/Stickers/LOL_2.png +0 -0
- package/android/src/main/assets/Stickers/LOL_3.png +0 -0
- package/android/src/main/assets/Stickers/LOL_4.png +0 -0
- package/android/src/main/assets/Stickers/LOL_5.png +0 -0
- package/android/src/main/assets/Stickers/LOL_6.png +0 -0
- package/android/src/main/assets/Stickers/LOL_7.png +0 -0
- package/android/src/main/assets/Stickers/LOL_8.png +0 -0
- package/android/src/main/assets/Stickers/LOL_9.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
- package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
- package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
- package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
- package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +901 -651
- 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/PhotoLayer.kt +5 -1
- package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +24 -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 +112 -122
- package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
- package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +63 -14
- package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
- package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
- 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/android/src/main/res/drawable/ic_draw.xml +6 -0
- package/android/src/main/res/drawable/ic_pause.xml +7 -0
- package/android/src/main/res/drawable/ic_play.xml +7 -0
- package/docs/api.md +1 -1
- package/docs/editor-ui-validation.md +22 -0
- package/docs/photo-editor.md +5 -6
- package/ios/Editor/PhotoVideoEditorViewController.swift +918 -494
- 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 +14 -2
- package/ios/Photo/Rendering/PhotoLayerRenderer.swift +16 -1
- package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
- package/ios/Photo/UI/CropOverlayView.swift +98 -134
- package/ios/Photo/UI/DrawOverlayView.swift +48 -0
- package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
- package/ios/Photo/UI/LayerOverlayView.swift +36 -11
- package/ios/Photo/UI/ZoomableImageView.swift +19 -1
- package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
- package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
- package/ios/Resources/Stickers/LOL_1.png +0 -0
- package/ios/Resources/Stickers/LOL_10.png +0 -0
- package/ios/Resources/Stickers/LOL_11.png +0 -0
- package/ios/Resources/Stickers/LOL_12.png +0 -0
- package/ios/Resources/Stickers/LOL_13.png +0 -0
- package/ios/Resources/Stickers/LOL_14.png +0 -0
- package/ios/Resources/Stickers/LOL_15.png +0 -0
- package/ios/Resources/Stickers/LOL_16.png +0 -0
- package/ios/Resources/Stickers/LOL_17.png +0 -0
- package/ios/Resources/Stickers/LOL_18.png +0 -0
- package/ios/Resources/Stickers/LOL_19.png +0 -0
- package/ios/Resources/Stickers/LOL_2.png +0 -0
- package/ios/Resources/Stickers/LOL_3.png +0 -0
- package/ios/Resources/Stickers/LOL_4.png +0 -0
- package/ios/Resources/Stickers/LOL_5.png +0 -0
- package/ios/Resources/Stickers/LOL_6.png +0 -0
- package/ios/Resources/Stickers/LOL_7.png +0 -0
- package/ios/Resources/Stickers/LOL_8.png +0 -0
- package/ios/Resources/Stickers/LOL_9.png +0 -0
- package/ios/Resources/Stickers/SPRING_1.png +0 -0
- package/ios/Resources/Stickers/SPRING_2.png +0 -0
- package/ios/Resources/Stickers/SPRING_3.png +0 -0
- package/ios/Resources/Stickers/SPRING_4.png +0 -0
- package/ios/Resources/Stickers/SPRING_5.png +0 -0
- package/ios/Resources/Stickers/SPRING_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_1.png +0 -0
- package/ios/Resources/Stickers/SUMMER_10.png +0 -0
- package/ios/Resources/Stickers/SUMMER_11.png +0 -0
- package/ios/Resources/Stickers/SUMMER_12.png +0 -0
- package/ios/Resources/Stickers/SUMMER_13.png +0 -0
- package/ios/Resources/Stickers/SUMMER_14.png +0 -0
- package/ios/Resources/Stickers/SUMMER_15.png +0 -0
- package/ios/Resources/Stickers/SUMMER_16.png +0 -0
- package/ios/Resources/Stickers/SUMMER_17.png +0 -0
- package/ios/Resources/Stickers/SUMMER_18.png +0 -0
- package/ios/Resources/Stickers/SUMMER_19.png +0 -0
- package/ios/Resources/Stickers/SUMMER_2.png +0 -0
- package/ios/Resources/Stickers/SUMMER_20.png +0 -0
- package/ios/Resources/Stickers/SUMMER_21.png +0 -0
- package/ios/Resources/Stickers/SUMMER_3.png +0 -0
- package/ios/Resources/Stickers/SUMMER_4.png +0 -0
- package/ios/Resources/Stickers/SUMMER_5.png +0 -0
- package/ios/Resources/Stickers/SUMMER_6.png +0 -0
- package/ios/Resources/Stickers/SUMMER_7.png +0 -0
- package/ios/Resources/Stickers/SUMMER_8.png +0 -0
- package/ios/Resources/Stickers/SUMMER_9.png +0 -0
- package/ios/Resources/Stickers/TEXT_1.png +0 -0
- package/ios/Resources/Stickers/TEXT_12.png +0 -0
- package/ios/Resources/Stickers/TEXT_13.png +0 -0
- package/ios/Resources/Stickers/TEXT_14.png +0 -0
- package/ios/Resources/Stickers/TEXT_15.png +0 -0
- package/ios/Resources/Stickers/TEXT_16.png +0 -0
- package/ios/Resources/Stickers/TEXT_2.png +0 -0
- package/ios/Resources/Stickers/TEXT_25.png +0 -0
- package/ios/Resources/Stickers/TEXT_26.png +0 -0
- package/ios/Resources/Stickers/TEXT_27.png +0 -0
- package/ios/Resources/Stickers/TEXT_28.png +0 -0
- package/ios/Resources/Stickers/TEXT_29.png +0 -0
- package/ios/Resources/Stickers/TEXT_3.png +0 -0
- package/ios/Resources/Stickers/TEXT_30.png +0 -0
- package/ios/Resources/Stickers/TEXT_31.png +0 -0
- package/ios/Resources/Stickers/TEXT_32.png +0 -0
- package/ios/Resources/Stickers/TEXT_7.png +0 -0
- package/ios/Resources/Stickers/TEXT_8.png +0 -0
- package/ios/Resources/Stickers/TEXT_9.png +0 -0
- package/ios/Resources/Stickers/a_pety_icon.png +0 -0
- package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
- package/ios/Video/Export/VideoExporter.swift +10 -1
- package/ios/Video/Render/VideoCropGeometry.swift +48 -0
- package/ios/Video/Render/VideoTransformState.swift +1 -0
- package/lib/module/index.js +2 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/types.d.ts +2 -2
- package/lib/typescript/src/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/src/index.ts +1 -1
- package/src/types.ts +2 -2
- package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
- package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
- package/ios/Stickers/OpenMojiCatalog.swift +0 -188
- package/ios/Stickers/RemoteImageLoader.swift +0 -127
|
@@ -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,13 +34,16 @@ 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
|
|
39
41
|
import com.photovideoeditor.photo.render.PhotoFilterPresets
|
|
40
42
|
import com.photovideoeditor.photo.render.PhotoLayer
|
|
41
43
|
import com.photovideoeditor.photo.render.PhotoLayerRenderer
|
|
44
|
+
import com.photovideoeditor.photo.render.PhotoTransformState
|
|
42
45
|
import com.photovideoeditor.photo.ui.CropOverlayView
|
|
46
|
+
import com.photovideoeditor.photo.ui.DrawOverlayView
|
|
43
47
|
import com.photovideoeditor.photo.ui.FilterThumbnailLoader
|
|
44
48
|
import com.photovideoeditor.video.export.VideoExportException
|
|
45
49
|
import com.photovideoeditor.video.export.VideoExporter
|
|
@@ -51,10 +55,20 @@ import com.photovideoeditor.photo.ui.LayerOverlayView
|
|
|
51
55
|
import com.photovideoeditor.photo.ui.PhotoPreviewRenderCoordinator
|
|
52
56
|
import com.photovideoeditor.photo.ui.ZoomableImageView
|
|
53
57
|
import com.photovideoeditor.stickers.OnlineStickerSheet
|
|
58
|
+
import com.photovideoeditor.stickers.RuntimeSticker
|
|
54
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
|
|
55
65
|
import com.photovideoeditor.ui.components.applyPressScale
|
|
56
66
|
import com.photovideoeditor.ui.components.editorChip
|
|
67
|
+
import com.photovideoeditor.ui.components.editorFilterThumbnail
|
|
68
|
+
import com.photovideoeditor.ui.components.editorPanelHeader
|
|
57
69
|
import com.photovideoeditor.ui.components.editorPrimaryButton
|
|
70
|
+
import com.photovideoeditor.ui.components.editorStripItem
|
|
71
|
+
import com.photovideoeditor.ui.components.editorToolStrip
|
|
58
72
|
import org.json.JSONObject
|
|
59
73
|
import java.io.File
|
|
60
74
|
import kotlin.math.roundToInt
|
|
@@ -67,22 +81,30 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
67
81
|
|
|
68
82
|
private var photoSession: PhotoEditSession? = null
|
|
69
83
|
private var cropMode = false
|
|
84
|
+
private var cropEntryState: PhotoTransformState? = null
|
|
85
|
+
private var photoCropPanel: CropPanel? = null
|
|
70
86
|
private var adjustMode = false
|
|
71
87
|
private var filtersMode = false
|
|
88
|
+
private var drawMode = false
|
|
72
89
|
private var stickersMode = false
|
|
73
90
|
private lateinit var stickersSubBar: View
|
|
74
91
|
private var activeAdjustmentKey = "brightness"
|
|
75
92
|
private var selectedLayerId: String? = null
|
|
76
|
-
private var activeLayerPropertyKey = "
|
|
93
|
+
private var activeLayerPropertyKey = ""
|
|
77
94
|
private var dragStartSnapshot: List<PhotoLayer>? = null
|
|
78
95
|
private lateinit var photoImageView: ZoomableImageView
|
|
79
96
|
private lateinit var cropOverlay: CropOverlayView
|
|
80
97
|
private lateinit var layerOverlay: LayerOverlayView
|
|
98
|
+
private lateinit var drawOverlay: DrawOverlayView
|
|
81
99
|
private lateinit var straightenSeekBar: SeekBar
|
|
82
100
|
private lateinit var cropSubBar: View
|
|
83
|
-
|
|
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
|
|
84
105
|
private lateinit var filtersSubBar: View
|
|
85
106
|
private lateinit var adjustSubBar: View
|
|
107
|
+
private lateinit var drawSubBar: View
|
|
86
108
|
private lateinit var layerToolBar: View
|
|
87
109
|
private lateinit var layerPropertySeekBar: SeekBar
|
|
88
110
|
private lateinit var layerColorSwatchRow: View
|
|
@@ -97,9 +119,14 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
97
119
|
private var videoSession: VideoEditSession? = null
|
|
98
120
|
private var videoExporter: VideoExporter? = null
|
|
99
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
|
|
100
127
|
private lateinit var trimRangeView: TrimRangeView
|
|
101
128
|
private lateinit var positionSeekBar: SeekBar
|
|
102
|
-
private lateinit var playPauseButton:
|
|
129
|
+
private lateinit var playPauseButton: ImageButton
|
|
103
130
|
private lateinit var timeLabel: TextView
|
|
104
131
|
private lateinit var videoAspectSubBar: View
|
|
105
132
|
private lateinit var videoToolBar: View
|
|
@@ -111,20 +138,22 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
111
138
|
private val positionPollHandler = android.os.Handler(android.os.Looper.getMainLooper())
|
|
112
139
|
private var positionPollRunnable: Runnable? = null
|
|
113
140
|
private var seeking = false
|
|
141
|
+
private lateinit var videoControlsOverlay: View
|
|
142
|
+
private var videoControlsVisible = true
|
|
143
|
+
private val hideVideoControlsRunnable = Runnable { setVideoControlsVisible(false) }
|
|
114
144
|
|
|
115
145
|
private val photoToolButtons = mutableMapOf<String, ToolNode>()
|
|
116
146
|
private var photoToolScroll: HorizontalScrollView? = null
|
|
117
147
|
private val cropAspectButtons = mutableMapOf<Float?, Button>()
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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>()
|
|
121
152
|
private var resizeMode = false
|
|
122
153
|
private lateinit var resizeSubBar: View
|
|
123
154
|
private var resizePresetSize: Pair<Int, Int>? = null
|
|
124
155
|
private val resizePresetButtons = mutableMapOf<Pair<Int, Int>?, Button>()
|
|
125
|
-
private val layerPropertyButtons = mutableMapOf<String,
|
|
126
|
-
/** Non-Delete/Duplicate layer action icons, hidden for TEXT layers (only Opacity/Colors/Duplicate/Delete apply there). */
|
|
127
|
-
private val layerIconButtons = mutableMapOf<String, View>()
|
|
156
|
+
private val layerPropertyButtons = mutableMapOf<String, ToolNode>()
|
|
128
157
|
private val videoToolButtons = mutableMapOf<String, ToolNode>()
|
|
129
158
|
private val videoAspectButtons = mutableMapOf<Float?, Button>()
|
|
130
159
|
private val videoFilterButtons = mutableMapOf<String, Button>()
|
|
@@ -136,9 +165,12 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
136
165
|
private lateinit var videoLayerToolBar: View
|
|
137
166
|
private lateinit var videoLayerPropertySeekBar: SeekBar
|
|
138
167
|
private var selectedVideoLayerId: String? = null
|
|
139
|
-
private var activeVideoLayerPropertyKey = "
|
|
168
|
+
private var activeVideoLayerPropertyKey = ""
|
|
140
169
|
private var videoDragStartSnapshot: List<PhotoLayer>? = null
|
|
141
|
-
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>()
|
|
142
174
|
private var selectedClipIndex = 0
|
|
143
175
|
private lateinit var clipStripBar: LinearLayout
|
|
144
176
|
private var timelineThumbnailRepository: TimelineThumbnailRepository? = null
|
|
@@ -227,25 +259,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
227
259
|
}
|
|
228
260
|
photoImageView = imageView
|
|
229
261
|
photoPreviewRenderer = PhotoPreviewRenderCoordinator(
|
|
230
|
-
render = { session.renderPreview() },
|
|
262
|
+
render = { session.renderPreview(cropMode || resizeMode) },
|
|
231
263
|
display = { bitmap ->
|
|
232
|
-
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
|
+
}
|
|
233
269
|
},
|
|
234
270
|
)
|
|
235
271
|
previewContainer.addView(imageView, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
236
272
|
|
|
237
|
-
previewContainer.addView(TextView(this).apply {
|
|
238
|
-
text = "● ORIGINAL • 100%"
|
|
239
|
-
textSize = 10f
|
|
240
|
-
letterSpacing = 0.08f
|
|
241
|
-
setTextColor(DesignTokens.onSurfaceVariant)
|
|
242
|
-
gravity = Gravity.CENTER
|
|
243
|
-
background = roundedDrawable(DesignTokens.withAlphaPercent(DesignTokens.surfaceContainerHigh, 92), DesignTokens.radiusLg)
|
|
244
|
-
setPadding(dp(DesignTokens.spaceMd), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceMd), dp(DesignTokens.spaceXs))
|
|
245
|
-
}, FrameLayout.LayoutParams(WRAP, WRAP, Gravity.TOP or Gravity.CENTER_HORIZONTAL).apply {
|
|
246
|
-
topMargin = dp(DesignTokens.spaceSm)
|
|
247
|
-
})
|
|
248
|
-
|
|
249
273
|
val overlay = CropOverlayView(this).apply { visibility = View.GONE }
|
|
250
274
|
cropOverlay = overlay
|
|
251
275
|
previewContainer.addView(overlay, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
@@ -254,12 +278,26 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
254
278
|
layerOverlay = layers
|
|
255
279
|
previewContainer.addView(layers, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
256
280
|
|
|
281
|
+
val drawing = DrawOverlayView(this).apply { visibility = View.GONE }
|
|
282
|
+
drawOverlay = drawing
|
|
283
|
+
previewContainer.addView(drawing, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
284
|
+
|
|
257
285
|
imageView.onBoundsChanged = { bounds ->
|
|
258
|
-
if (cropMode) overlay.
|
|
286
|
+
if (cropMode) overlay.restore(bounds, session.state)
|
|
259
287
|
layers.setImageBounds(bounds)
|
|
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) }
|
|
298
|
+
layers.onLayerDoubleTapped = { id ->
|
|
299
|
+
session.layerStack.layers.firstOrNull { it.id == id && it.type == LayerType.TEXT }?.let { showTextInputDialog(session, it) }
|
|
300
|
+
}
|
|
263
301
|
layers.onLayerTransformChanged = { id, x, y, scale, rotation ->
|
|
264
302
|
if (dragStartSnapshot == null) dragStartSnapshot = session.layerStack.layers
|
|
265
303
|
session.layerStack.updateLive { list ->
|
|
@@ -295,10 +333,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
295
333
|
})
|
|
296
334
|
}
|
|
297
335
|
straightenSeekBar = straighten
|
|
298
|
-
|
|
299
|
-
straighten,
|
|
300
|
-
LinearLayout.LayoutParams(MATCH, WRAP).apply { setMargins(dp(16), dp(4), dp(16), 0) }
|
|
301
|
-
)
|
|
336
|
+
|
|
302
337
|
|
|
303
338
|
val cropBar = createCropSubBar(session, imageView, overlay)
|
|
304
339
|
cropSubBar = cropBar
|
|
@@ -310,22 +345,35 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
310
345
|
resizeBar.visibility = View.GONE
|
|
311
346
|
root.addView(resizeBar, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
312
347
|
|
|
313
|
-
val
|
|
314
|
-
|
|
348
|
+
val accentColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
349
|
+
adjustmentSeekBar = EditorToolSlider(this).apply {
|
|
350
|
+
applyCompactTrack(accentColor)
|
|
315
351
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
316
352
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
317
353
|
if (!fromUser) return
|
|
318
354
|
val (min, _) = PhotoAdjustments.rangeFor(activeAdjustmentKey)
|
|
319
|
-
|
|
320
|
-
|
|
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()) }
|
|
321
371
|
schedulePhotoPreviewRender()
|
|
322
372
|
}
|
|
323
373
|
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
324
374
|
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
|
325
375
|
})
|
|
326
376
|
}
|
|
327
|
-
adjustmentSeekBar = adjustmentSeek
|
|
328
|
-
root.addView(adjustmentSeek, LinearLayout.LayoutParams(MATCH, WRAP).apply { setMargins(dp(16), dp(4), dp(16), 0) })
|
|
329
377
|
|
|
330
378
|
val adjustBar = createAdjustSubBar(session, imageView)
|
|
331
379
|
adjustSubBar = adjustBar
|
|
@@ -342,15 +390,22 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
342
390
|
stickersBar.visibility = View.GONE
|
|
343
391
|
root.addView(stickersBar, LinearLayout.LayoutParams(MATCH, dp(ASSET_GRID_BAR_HEIGHT_DP)))
|
|
344
392
|
|
|
345
|
-
val
|
|
393
|
+
val drawBar = createDrawSubBar(session)
|
|
394
|
+
drawSubBar = drawBar
|
|
395
|
+
drawBar.visibility = View.GONE
|
|
396
|
+
root.addView(drawBar, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
397
|
+
|
|
398
|
+
val layerPropertySeek = EditorToolSlider(this).apply {
|
|
399
|
+
progressTintList = android.content.res.ColorStateList.valueOf(themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
400
|
+
thumbTintList = progressTintList
|
|
346
401
|
visibility = View.GONE
|
|
347
402
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
348
403
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
349
404
|
if (!fromUser) return
|
|
350
405
|
onLayerPropertyChanged(progress)
|
|
351
406
|
}
|
|
352
|
-
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
353
|
-
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() }
|
|
354
409
|
})
|
|
355
410
|
}
|
|
356
411
|
layerPropertySeekBar = layerPropertySeek
|
|
@@ -364,7 +419,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
364
419
|
val layerBar = createLayerToolBar(session)
|
|
365
420
|
layerToolBar = layerBar
|
|
366
421
|
layerBar.visibility = View.GONE
|
|
367
|
-
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(
|
|
422
|
+
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(76)))
|
|
368
423
|
|
|
369
424
|
val toolbarHeight = dp(76)
|
|
370
425
|
val toolbar = createPhotoToolBar(session, imageView, overlay)
|
|
@@ -381,7 +436,8 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
381
436
|
resizeBar to dp(56),
|
|
382
437
|
filtersBar to dp(FILTERS_SUB_BAR_HEIGHT_DP),
|
|
383
438
|
stickersBar to dp(ASSET_GRID_BAR_HEIGHT_DP),
|
|
384
|
-
|
|
439
|
+
drawBar to dp(56),
|
|
440
|
+
layerBar to dp(76)
|
|
385
441
|
)
|
|
386
442
|
)
|
|
387
443
|
return outer
|
|
@@ -394,42 +450,58 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
394
450
|
Triple("filters", "Filters", R.drawable.ic_photo_filter),
|
|
395
451
|
Triple("text", "Text", R.drawable.ic_title),
|
|
396
452
|
Triple("stickers", "Stickers", R.drawable.ic_sentiment_satisfied),
|
|
397
|
-
Triple("
|
|
398
|
-
Triple("
|
|
399
|
-
Triple("
|
|
400
|
-
Triple("resize", "Resize", R.drawable.ic_aspect_ratio)
|
|
453
|
+
Triple("draw", "Draw", R.drawable.ic_draw),
|
|
454
|
+
Triple("resize", "Resize", R.drawable.ic_aspect_ratio),
|
|
455
|
+
Triple("compare", "Original", R.drawable.ic_compare)
|
|
401
456
|
)
|
|
402
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
|
+
*/
|
|
403
463
|
private fun createPhotoToolBar(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView): LinearLayout {
|
|
404
464
|
val backgroundColor = themeColor("toolbarColor", DesignTokens.withAlphaPercent(DesignTokens.surfaceContainerLowest, 95))
|
|
465
|
+
val itemWidth = photoToolItemWidth()
|
|
405
466
|
return LinearLayout(this).apply {
|
|
406
467
|
orientation = LinearLayout.VERTICAL
|
|
407
468
|
setBackgroundColor(backgroundColor)
|
|
408
|
-
setPadding(dp(DesignTokens.spaceXs), dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceLg))
|
|
409
|
-
|
|
410
|
-
addView(
|
|
411
|
-
View(this@PhotoVideoEditorActivity).apply { background = roundedDrawable(DesignTokens.surfaceVariant, DesignTokens.radiusSm) },
|
|
412
|
-
LinearLayout.LayoutParams(dp(36), dp(4)).apply { gravity = Gravity.CENTER_HORIZONTAL; bottomMargin = dp(DesignTokens.spaceXs) }
|
|
413
|
-
)
|
|
414
469
|
|
|
415
470
|
val features = request.optJSONObject("features")
|
|
416
471
|
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
417
472
|
isHorizontalScrollBarEnabled = false
|
|
473
|
+
clipToPadding = false
|
|
418
474
|
photoToolScroll = this
|
|
419
475
|
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
420
476
|
orientation = LinearLayout.HORIZONTAL
|
|
421
477
|
gravity = Gravity.CENTER_VERTICAL
|
|
422
|
-
setPadding(dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs))
|
|
423
478
|
photoToolDefinitions.filter { features?.optBoolean(it.first, true) != false }.forEach { (key, label, iconRes) ->
|
|
424
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
|
+
}
|
|
425
490
|
photoToolButtons[key] = node
|
|
426
|
-
addView(node.root, LinearLayout.LayoutParams(
|
|
491
|
+
addView(node.root, LinearLayout.LayoutParams(itemWidth, WRAP))
|
|
427
492
|
}
|
|
428
493
|
})
|
|
429
494
|
}, LinearLayout.LayoutParams(MATCH, WRAP))
|
|
430
495
|
}
|
|
431
496
|
}
|
|
432
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
|
+
|
|
433
505
|
private fun refreshPhotoToolSelection() {
|
|
434
506
|
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
435
507
|
photoToolButtons.forEach { (key, node) ->
|
|
@@ -438,6 +510,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
438
510
|
"adjust" -> adjustMode
|
|
439
511
|
"filters" -> filtersMode
|
|
440
512
|
"stickers" -> stickersMode
|
|
513
|
+
"draw" -> drawMode
|
|
441
514
|
"resize" -> resizeMode
|
|
442
515
|
else -> false
|
|
443
516
|
}
|
|
@@ -460,63 +533,120 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
460
533
|
}
|
|
461
534
|
|
|
462
535
|
private fun onPhotoToolTapped(key: String, session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
536
|
+
if (key != "compare" && comparingOriginal) showOriginalPreview(false)
|
|
463
537
|
when (key) {
|
|
538
|
+
"compare" -> showOriginalPreview(!comparingOriginal)
|
|
464
539
|
"crop" -> setCropMode(true, session, imageView, overlay)
|
|
465
540
|
"adjust" -> setAdjustMode(true, session)
|
|
466
541
|
"filters" -> setFiltersMode(true, session)
|
|
467
542
|
"text" -> showTextInputDialog(session)
|
|
468
|
-
"stickers" ->
|
|
469
|
-
"
|
|
543
|
+
"stickers" -> showStickerBottomSheet(session)
|
|
544
|
+
"draw" -> setDrawMode(true, session)
|
|
470
545
|
"resize" -> setResizeMode(true, session, imageView, overlay)
|
|
471
|
-
"retouch" -> setAdjustMode(true, session)
|
|
472
|
-
"layers" -> openLayerManager(session)
|
|
473
546
|
else -> Toast.makeText(this, "$key is coming in a later milestone.", Toast.LENGTH_SHORT).show()
|
|
474
547
|
}
|
|
475
548
|
}
|
|
476
549
|
|
|
477
|
-
private fun openLayerManager(session: PhotoEditSession) {
|
|
478
|
-
val top = session.layerStack.layers.lastOrNull()
|
|
479
|
-
if (top == null) Toast.makeText(this, "Add content to start a layer stack.", Toast.LENGTH_LONG).show()
|
|
480
|
-
else selectLayer(top.id)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
550
|
// ---------------------------------------------------------------------
|
|
484
551
|
// Layers: text, stickers, shapes, freehand drawing, selection, and
|
|
485
552
|
// transform/duplicate/reorder/lock/hide/delete with undo/redo (Milestone 4).
|
|
486
553
|
// ---------------------------------------------------------------------
|
|
487
554
|
|
|
488
|
-
private fun showTextInputDialog(session: PhotoEditSession) {
|
|
555
|
+
private fun showTextInputDialog(session: PhotoEditSession, editingLayer: PhotoLayer? = null) {
|
|
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
|
+
})
|
|
489
578
|
val input = EditText(this).apply {
|
|
490
|
-
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
|
|
491
|
-
setTextColor(DesignTokens.onSurface)
|
|
492
|
-
setHintTextColor(DesignTokens.outline)
|
|
579
|
+
inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE or InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
|
|
493
580
|
hint = "Type something..."
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
.
|
|
502
|
-
.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
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)
|
|
588
|
+
setText(editingLayer?.text.orEmpty())
|
|
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) }
|
|
612
|
+
}
|
|
613
|
+
dialog.show()
|
|
513
614
|
}
|
|
514
615
|
|
|
515
616
|
private fun setStickersMode(enabled: Boolean, session: PhotoEditSession) {
|
|
516
617
|
stickersMode = enabled
|
|
517
618
|
stickersSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
518
|
-
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
619
|
+
mainToolBar.visibility = if (enabled || selectedLayerId != null) View.GONE else View.VISIBLE
|
|
620
|
+
refreshPhotoToolSelection()
|
|
621
|
+
}
|
|
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
|
+
|
|
637
|
+
private fun showStickerBottomSheet(session: PhotoEditSession) {
|
|
638
|
+
stickersMode = true
|
|
519
639
|
refreshPhotoToolSelection()
|
|
640
|
+
OnlineStickerSheet(this, runtimeStickerAssets()) { path ->
|
|
641
|
+
val layer = newSticker(stickerUri = Uri.fromFile(File(path)).toString())
|
|
642
|
+
session.layerStack.commit { it + layer }
|
|
643
|
+
selectLayer(layer.id)
|
|
644
|
+
}.apply {
|
|
645
|
+
setOnDismissListener {
|
|
646
|
+
stickersMode = false
|
|
647
|
+
refreshPhotoToolSelection()
|
|
648
|
+
}
|
|
649
|
+
}.show()
|
|
520
650
|
}
|
|
521
651
|
|
|
522
652
|
/** One asset card in the Stickers library grid (see stickers_shapes_library mockup). */
|
|
@@ -567,7 +697,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
567
697
|
val emoji = TextView(this@PhotoVideoEditorActivity).apply { text = PhotoLayerRenderer.glyphFor(id); textSize = 26f }
|
|
568
698
|
addView(
|
|
569
699
|
createAssetCard(emoji, null) {
|
|
570
|
-
val layer =
|
|
700
|
+
val layer = newSticker(stickerId = id)
|
|
571
701
|
session.layerStack.commit { it + layer }
|
|
572
702
|
selectLayer(layer.id)
|
|
573
703
|
setStickersMode(false, session)
|
|
@@ -586,7 +716,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
586
716
|
}
|
|
587
717
|
addView(
|
|
588
718
|
createAssetCard(icon, id) {
|
|
589
|
-
val layer =
|
|
719
|
+
val layer = newSticker(stickerUri = uri)
|
|
590
720
|
session.layerStack.commit { it + layer }
|
|
591
721
|
selectLayer(layer.id)
|
|
592
722
|
setStickersMode(false, session)
|
|
@@ -605,24 +735,6 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
605
735
|
},
|
|
606
736
|
LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) }
|
|
607
737
|
)
|
|
608
|
-
val features = request.optJSONObject("features")
|
|
609
|
-
if (features?.optBoolean("onlineStickers", true) != false) {
|
|
610
|
-
val browseIcon = ImageView(this@PhotoVideoEditorActivity).apply {
|
|
611
|
-
setImageResource(R.drawable.ic_photo_filter)
|
|
612
|
-
setColorFilter(DesignTokens.tertiary)
|
|
613
|
-
}
|
|
614
|
-
addView(
|
|
615
|
-
createAssetCard(browseIcon, "Browse") {
|
|
616
|
-
OnlineStickerSheet(this@PhotoVideoEditorActivity) { path ->
|
|
617
|
-
val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = Uri.fromFile(File(path)).toString())
|
|
618
|
-
session.layerStack.commit { it + layer }
|
|
619
|
-
selectLayer(layer.id)
|
|
620
|
-
setStickersMode(false, session)
|
|
621
|
-
}.show()
|
|
622
|
-
},
|
|
623
|
-
LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) }
|
|
624
|
-
)
|
|
625
|
-
}
|
|
626
738
|
})
|
|
627
739
|
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
628
740
|
addView(
|
|
@@ -632,6 +744,69 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
632
744
|
}
|
|
633
745
|
}
|
|
634
746
|
|
|
747
|
+
private fun setDrawMode(enabled: Boolean, session: PhotoEditSession) {
|
|
748
|
+
drawMode = enabled
|
|
749
|
+
drawOverlay.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
750
|
+
drawSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
751
|
+
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
752
|
+
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
753
|
+
photoImageView.panZoomEnabled = !enabled
|
|
754
|
+
if (enabled) {
|
|
755
|
+
drawOverlay.strokeColor = Color.RED
|
|
756
|
+
drawOverlay.strokeWidthPx = dp(4).toFloat()
|
|
757
|
+
drawOverlay.setImageBounds(photoImageView.currentImageBounds())
|
|
758
|
+
} else {
|
|
759
|
+
commitDrawStrokes(session)
|
|
760
|
+
drawOverlay.clearStrokes()
|
|
761
|
+
}
|
|
762
|
+
refreshPhotoToolSelection()
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
private fun commitDrawStrokes(session: PhotoEditSession) {
|
|
766
|
+
if (!drawOverlay.hasStrokes) return
|
|
767
|
+
val bounds = photoImageView.currentImageBounds()
|
|
768
|
+
val shortSide = minOf(bounds.width(), bounds.height())
|
|
769
|
+
if (shortSide <= 0f) return
|
|
770
|
+
val newLayers = drawOverlay.normalizedStrokes().mapNotNull { (points, stroke) ->
|
|
771
|
+
if (points.size < 2) return@mapNotNull null
|
|
772
|
+
val centerX = points.sumOf { it.first.toDouble() }.toFloat() / points.size
|
|
773
|
+
val centerY = points.sumOf { it.second.toDouble() }.toFloat() / points.size
|
|
774
|
+
PhotoLayer(
|
|
775
|
+
type = LayerType.DRAWING,
|
|
776
|
+
x = centerX,
|
|
777
|
+
y = centerY,
|
|
778
|
+
drawColor = stroke.color,
|
|
779
|
+
drawStrokeWidth = stroke.widthPx / shortSide,
|
|
780
|
+
drawPoints = points.map { (x, y) -> (x - centerX) to (y - centerY) }
|
|
781
|
+
)
|
|
782
|
+
}
|
|
783
|
+
if (newLayers.isNotEmpty()) session.layerStack.commit { it + newLayers }
|
|
784
|
+
onLayerStackChanged()
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
private fun createDrawSubBar(session: PhotoEditSession): LinearLayout {
|
|
788
|
+
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
789
|
+
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
790
|
+
val colors = listOf(Color.RED, Color.rgb(255, 214, 51), Color.rgb(76, 217, 100), Color.rgb(10, 132, 255), Color.WHITE, Color.BLACK)
|
|
791
|
+
return LinearLayout(this).apply {
|
|
792
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
793
|
+
setPadding(dp(12), 0, dp(12), 0)
|
|
794
|
+
setBackgroundColor(themeColor("toolbarColor", DesignTokens.surfaceContainerLow))
|
|
795
|
+
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
796
|
+
isHorizontalScrollBarEnabled = false
|
|
797
|
+
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
798
|
+
orientation = LinearLayout.HORIZONTAL; gravity = Gravity.CENTER_VERTICAL
|
|
799
|
+
colors.forEach { color -> addView(createColorSwatch(color) { drawOverlay.strokeColor = color }, LinearLayout.LayoutParams(dp(36), dp(36)).apply { marginEnd = dp(6) }) }
|
|
800
|
+
addView(actionButton("Thin") { drawOverlay.strokeWidthPx = dp(2).toFloat() }.apply { setTextColor(textColor) })
|
|
801
|
+
addView(actionButton("Thick") { drawOverlay.strokeWidthPx = dp(10).toFloat() }.apply { setTextColor(textColor) })
|
|
802
|
+
addView(actionButton("Undo") { drawOverlay.undoLastStroke() }.apply { setTextColor(textColor) })
|
|
803
|
+
addView(actionButton("Clear") { drawOverlay.clearStrokes() }.apply { setTextColor(textColor) })
|
|
804
|
+
})
|
|
805
|
+
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
806
|
+
addView(actionButton("Done") { setDrawMode(false, session) }.apply { setTextColor(primaryColor) }, LinearLayout.LayoutParams(dp(72), MATCH))
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
635
810
|
/** Launches the SAF image picker for either the sticker-upload or overlay-upload flow, dispatched in [onActivityResult]. */
|
|
636
811
|
private fun launchOverlayPicker(requestCode: Int) {
|
|
637
812
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).setType("image/*").addCategory(Intent.CATEGORY_OPENABLE)
|
|
@@ -659,7 +834,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
659
834
|
when (requestCode) {
|
|
660
835
|
REQUEST_CODE_STICKER_UPLOAD -> {
|
|
661
836
|
val session = photoSession ?: return
|
|
662
|
-
val layer =
|
|
837
|
+
val layer = newSticker(stickerUri = resolvedUri)
|
|
663
838
|
session.layerStack.commit { it + layer }
|
|
664
839
|
selectLayer(layer.id)
|
|
665
840
|
setStickersMode(false, session)
|
|
@@ -679,7 +854,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
679
854
|
}
|
|
680
855
|
REQUEST_CODE_VIDEO_STICKER_UPLOAD -> {
|
|
681
856
|
val session = videoSession ?: return
|
|
682
|
-
val layer =
|
|
857
|
+
val layer = newSticker(stickerUri = resolvedUri)
|
|
683
858
|
session.layerStack.commit { it + layer }
|
|
684
859
|
onVideoLayerStackChanged()
|
|
685
860
|
selectVideoLayer(layer.id, session)
|
|
@@ -703,52 +878,37 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
703
878
|
|
|
704
879
|
private fun selectLayer(id: String?) {
|
|
705
880
|
val session = photoSession
|
|
881
|
+
if (selectedLayerId != id) activeLayerPropertyKey = ""
|
|
706
882
|
selectedLayerId = id
|
|
707
883
|
layerOverlay.selectedLayerId = id
|
|
708
884
|
if (session != null) {
|
|
709
885
|
layerOverlay.layers = session.layerStack.layers
|
|
710
886
|
schedulePhotoPreviewRender()
|
|
711
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
|
+
}
|
|
712
894
|
setLayerToolBarVisible(id != null)
|
|
713
895
|
}
|
|
714
896
|
|
|
715
897
|
private fun setLayerToolBarVisible(visible: Boolean) {
|
|
716
898
|
layerToolBar.visibility = if (visible) View.VISIBLE else View.GONE
|
|
717
899
|
mainToolBar.visibility = if (visible) View.GONE else View.VISIBLE
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
layerPropertyButtons["fontSize"]?.visibility = View.GONE
|
|
729
|
-
layerPropertyButtons["color"]?.visibility = if (isText) View.VISIBLE else View.GONE
|
|
730
|
-
listOf("lock", "visibility", "front", "back").forEach { key ->
|
|
731
|
-
layerIconButtons[key]?.visibility = if (reducedControls) View.GONE else View.VISIBLE
|
|
732
|
-
}
|
|
733
|
-
val allowedKeys = when {
|
|
734
|
-
isText -> setOf("opacity", "color")
|
|
735
|
-
isSticker -> setOf("opacity")
|
|
736
|
-
else -> setOf("scale", "rotation", "opacity")
|
|
737
|
-
}
|
|
738
|
-
if (activeLayerPropertyKey !in allowedKeys) {
|
|
739
|
-
activeLayerPropertyKey = "opacity"
|
|
740
|
-
}
|
|
741
|
-
layerPropertySeekBar.visibility = if (activeLayerPropertyKey == "color") View.GONE else View.VISIBLE
|
|
742
|
-
layerColorSwatchRow.visibility = if (activeLayerPropertyKey == "color") View.VISIBLE else View.GONE
|
|
743
|
-
syncLayerPropertySeekBar()
|
|
744
|
-
refreshSelection(layerPropertyButtons, activeLayerPropertyKey, themeColor("textColor", DesignTokens.onSurface), themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
745
|
-
} else {
|
|
746
|
-
layerPropertySeekBar.visibility = View.GONE
|
|
747
|
-
layerColorSwatchRow.visibility = View.GONE
|
|
748
|
-
}
|
|
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)
|
|
749
910
|
}
|
|
750
911
|
|
|
751
|
-
/** Circular color swatch, matching the drawing_brush_studio mockup's palette dots. */
|
|
752
912
|
private fun createColorSwatch(color: Int, onClick: () -> Unit): FrameLayout {
|
|
753
913
|
val outer = FrameLayout(this).apply {
|
|
754
914
|
background = circleDrawable(DesignTokens.surfaceContainerHigh)
|
|
@@ -761,38 +921,29 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
761
921
|
}
|
|
762
922
|
|
|
763
923
|
/** Text color swatch row, shown when a TEXT layer's "Color" property is active (text_editor_typography_studio mockup). */
|
|
764
|
-
private fun createTextColorSwatchRow(session: PhotoEditSession): LinearLayout {
|
|
765
|
-
val
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
)
|
|
788
|
-
}
|
|
789
|
-
})
|
|
790
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
791
|
-
addView(
|
|
792
|
-
actionButton("Done") { selectLayer(null) }.apply { setTextColor(themeColor("primaryColor", DesignTokens.primaryContainer)) },
|
|
793
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
794
|
-
)
|
|
795
|
-
}
|
|
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)))
|
|
796
947
|
}
|
|
797
948
|
|
|
798
949
|
private fun currentSelectedLayer(): PhotoLayer? {
|
|
@@ -803,13 +954,14 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
803
954
|
private fun syncLayerPropertySeekBar() {
|
|
804
955
|
val layer = currentSelectedLayer() ?: return
|
|
805
956
|
val (min, max, value) = when (activeLayerPropertyKey) {
|
|
806
|
-
"rotation" -> Triple(-180f, 180f, layer.rotationDegrees)
|
|
957
|
+
"rotation" -> Triple(-180f, 180f, ((layer.rotationDegrees % 360f + 540f) % 360f) - 180f)
|
|
807
958
|
"opacity" -> Triple(0f, 100f, layer.opacity * 100f)
|
|
808
959
|
"fontSize" -> Triple(12f, 160f, layer.fontSize)
|
|
809
960
|
else -> Triple(20f, 800f, layer.scale * 100f)
|
|
810
961
|
}
|
|
811
962
|
layerPropertySeekBar.max = (max - min).roundToInt()
|
|
812
963
|
layerPropertySeekBar.progress = (value - min).roundToInt()
|
|
964
|
+
(layerPropertySeekBar as? EditorToolSlider)?.propertyKey = activeLayerPropertyKey
|
|
813
965
|
}
|
|
814
966
|
|
|
815
967
|
private fun onLayerPropertyChanged(progress: Int) {
|
|
@@ -837,373 +989,292 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
837
989
|
schedulePhotoPreviewRender()
|
|
838
990
|
}
|
|
839
991
|
|
|
840
|
-
private fun createLayerToolBar(session: PhotoEditSession): LinearLayout {
|
|
841
|
-
val
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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)))
|
|
846
1022
|
return LinearLayout(this).apply {
|
|
847
1023
|
gravity = Gravity.CENTER_VERTICAL
|
|
848
|
-
|
|
849
|
-
setBackgroundColor(toolbarColor)
|
|
1024
|
+
setBackgroundColor(themeColor("toolbarColor", DesignTokens.surfaceContainerLow))
|
|
850
1025
|
addView(HorizontalScrollView(this@PhotoVideoEditorActivity).apply {
|
|
851
1026
|
isHorizontalScrollBarEnabled = false
|
|
852
1027
|
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
853
|
-
orientation = LinearLayout.HORIZONTAL
|
|
854
1028
|
gravity = Gravity.CENTER_VERTICAL
|
|
855
|
-
|
|
856
|
-
val
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (key != "color") syncLayerPropertySeekBar()
|
|
861
|
-
refreshSelection(layerPropertyButtons, activeLayerPropertyKey, textColor, primaryColor)
|
|
862
|
-
}.apply { setTextColor(textColor) }
|
|
863
|
-
layerPropertyButtons[key] = chip
|
|
864
|
-
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))
|
|
865
1034
|
}
|
|
866
|
-
addView(
|
|
867
|
-
createIconButton(R.drawable.ic_content_copy, "Duplicate layer", textColor) {
|
|
868
|
-
val original = currentSelectedLayer() ?: return@createIconButton
|
|
869
|
-
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))
|
|
870
|
-
session.layerStack.commit { it + copy }
|
|
871
|
-
selectLayer(copy.id)
|
|
872
|
-
},
|
|
873
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
874
|
-
)
|
|
875
|
-
addView(
|
|
876
|
-
createIconButton(R.drawable.ic_lock, "Toggle lock", textColor) {
|
|
877
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
878
|
-
session.layerStack.commit { list -> list.map { if (it.id == id) it.copy(locked = !it.locked) else it } }
|
|
879
|
-
onLayerStackChanged()
|
|
880
|
-
}.also { layerIconButtons["lock"] = it },
|
|
881
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
882
|
-
)
|
|
883
|
-
addView(
|
|
884
|
-
createIconButton(R.drawable.ic_visibility_off, "Toggle visibility", textColor) {
|
|
885
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
886
|
-
session.layerStack.commit { list -> list.map { if (it.id == id) it.copy(visible = !it.visible) else it } }
|
|
887
|
-
onLayerStackChanged()
|
|
888
|
-
}.also { layerIconButtons["visibility"] = it },
|
|
889
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
890
|
-
)
|
|
891
|
-
addView(
|
|
892
|
-
createIconButton(R.drawable.ic_chevron_right, "Bring to front", textColor) {
|
|
893
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
894
|
-
session.layerStack.commit { list -> (list.filter { it.id != id } + list.first { it.id == id }) }
|
|
895
|
-
onLayerStackChanged()
|
|
896
|
-
}.also { layerIconButtons["front"] = it },
|
|
897
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
898
|
-
)
|
|
899
|
-
addView(
|
|
900
|
-
createIconButton(R.drawable.ic_chevron_left, "Send to back", textColor) {
|
|
901
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
902
|
-
session.layerStack.commit { list -> (listOf(list.first { it.id == id }) + list.filter { it.id != id }) }
|
|
903
|
-
onLayerStackChanged()
|
|
904
|
-
}.also { layerIconButtons["back"] = it },
|
|
905
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
906
|
-
)
|
|
907
|
-
addView(
|
|
908
|
-
createIconButton(R.drawable.ic_delete, "Delete layer", textColor) {
|
|
909
|
-
val id = selectedLayerId ?: return@createIconButton
|
|
910
|
-
session.layerStack.commit { list -> list.filterNot { it.id == id } }
|
|
911
|
-
selectLayer(null)
|
|
912
|
-
onLayerStackChanged()
|
|
913
|
-
},
|
|
914
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
915
|
-
)
|
|
916
1035
|
})
|
|
917
1036
|
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
)
|
|
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))
|
|
922
1040
|
}
|
|
923
1041
|
}
|
|
924
1042
|
|
|
925
|
-
/** Adjust (image_adjustments_workspace mockup) — Brightness/Contrast/Saturation/etc sliders + Mirror. */
|
|
926
1043
|
private fun setAdjustMode(enabled: Boolean, session: PhotoEditSession) {
|
|
927
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.
|
|
928
1047
|
adjustSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
929
|
-
adjustmentSeekBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
930
1048
|
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
931
1049
|
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
932
|
-
|
|
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) }
|
|
933
1053
|
refreshPhotoToolSelection()
|
|
934
|
-
refreshAdjustSelection()
|
|
1054
|
+
refreshAdjustSelection(session)
|
|
935
1055
|
}
|
|
936
1056
|
|
|
937
|
-
/** Filters
|
|
1057
|
+
/** Filters — preset thumbnail carousel with an intensity slider above it. */
|
|
938
1058
|
private fun setFiltersMode(enabled: Boolean, session: PhotoEditSession) {
|
|
939
1059
|
filtersMode = enabled
|
|
940
1060
|
filtersSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
941
|
-
adjustmentSeekBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
942
1061
|
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
943
1062
|
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
944
|
-
if (enabled) {
|
|
1063
|
+
if (enabled) { syncAdjustmentSeekBar(session); fadeInPanel(filtersSubBar) }
|
|
945
1064
|
refreshPhotoToolSelection()
|
|
946
1065
|
refreshFiltersSelection(session)
|
|
947
1066
|
}
|
|
948
1067
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
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()
|
|
953
1073
|
}
|
|
954
1074
|
|
|
955
|
-
private fun
|
|
956
|
-
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
1075
|
+
private fun refreshAdjustSelection(session: PhotoEditSession) {
|
|
957
1076
|
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
958
|
-
val
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
android.graphics.drawable.GradientDrawable().apply {
|
|
965
|
-
cornerRadius = dp(DesignTokens.radiusLg).toFloat()
|
|
966
|
-
setStroke(dp(2), DesignTokens.primaryContainer)
|
|
967
|
-
}
|
|
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)
|
|
968
1083
|
} else {
|
|
969
|
-
|
|
1084
|
+
item.setSelected(key == activeAdjustmentKey, primaryColor)
|
|
1085
|
+
item.setModified(session.adjustments.value(key) != defaults.value(key), primaryColor)
|
|
970
1086
|
}
|
|
971
|
-
label.setTextColor(if (selected) primaryColor else DesignTokens.outline)
|
|
972
|
-
label.setTypeface(label.typeface, if (selected) android.graphics.Typeface.BOLD else android.graphics.Typeface.NORMAL)
|
|
973
1087
|
}
|
|
974
1088
|
}
|
|
975
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. */
|
|
976
1099
|
private fun syncAdjustmentSeekBar(session: PhotoEditSession) {
|
|
977
1100
|
val (min, max) = PhotoAdjustments.rangeFor(activeAdjustmentKey)
|
|
978
1101
|
adjustmentSeekBar.max = (max - min).roundToInt()
|
|
979
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()
|
|
980
1111
|
}
|
|
981
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"
|
|
1116
|
+
}
|
|
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
|
+
*/
|
|
982
1138
|
private fun createAdjustSubBar(session: PhotoEditSession, imageView: ZoomableImageView): LinearLayout {
|
|
983
|
-
val toolbarColor = themeColor("toolbarColor", DesignTokens.surfaceContainerLow)
|
|
984
|
-
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
985
1139
|
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
986
|
-
val adjustmentChips = listOf(
|
|
987
|
-
"brightness" to "Brightness", "contrast" to "Contrast", "saturation" to "Saturation",
|
|
988
|
-
"exposure" to "Exposure", "temperature" to "Temp", "blurRadius" to "Blur"
|
|
989
|
-
)
|
|
990
1140
|
return LinearLayout(this).apply {
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
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) {
|
|
1000
1166
|
activeAdjustmentKey = key
|
|
1001
1167
|
syncAdjustmentSeekBar(session)
|
|
1002
|
-
refreshAdjustSelection()
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
addView(
|
|
1168
|
+
refreshAdjustSelection(session)
|
|
1169
|
+
}
|
|
1170
|
+
adjustmentStripItems[key] = item
|
|
1171
|
+
addView(item.root, LinearLayout.LayoutParams(dp(EditorPanelMetrics.STRIP_ITEM_WIDTH_DP), WRAP))
|
|
1006
1172
|
}
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
schedulePhotoPreviewRender()
|
|
1019
|
-
refreshAdjustSelection()
|
|
1020
|
-
}.apply { setTextColor(textColor) },
|
|
1021
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1022
|
-
)
|
|
1023
|
-
})
|
|
1024
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
1025
|
-
addView(
|
|
1026
|
-
actionButton("Done") { setAdjustMode(false, session) }.apply { setTextColor(primaryColor) },
|
|
1027
|
-
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))
|
|
1028
1184
|
)
|
|
1029
1185
|
}
|
|
1030
1186
|
}
|
|
1031
1187
|
|
|
1032
|
-
/**
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
1038
|
-
}
|
|
1039
|
-
val frame = FrameLayout(this).apply {
|
|
1040
|
-
background = roundedDrawable(DesignTokens.surfaceContainerHighest, DesignTokens.radiusLg)
|
|
1041
|
-
clipToOutline = true
|
|
1042
|
-
addView(image, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1043
|
-
}
|
|
1044
|
-
val label = TextView(this).apply {
|
|
1045
|
-
text = labelText
|
|
1046
|
-
textSize = 11f
|
|
1047
|
-
gravity = Gravity.CENTER
|
|
1048
|
-
setTextColor(DesignTokens.outline)
|
|
1049
|
-
setPadding(0, dp(DesignTokens.spaceXs), 0, 0)
|
|
1050
|
-
maxLines = 1
|
|
1051
|
-
}
|
|
1052
|
-
val root = LinearLayout(this).apply {
|
|
1053
|
-
orientation = LinearLayout.VERTICAL
|
|
1054
|
-
gravity = Gravity.CENTER_HORIZONTAL
|
|
1055
|
-
isClickable = true
|
|
1056
|
-
setPadding(dp(4), 0, dp(4), 0)
|
|
1057
|
-
addView(frame, LinearLayout.LayoutParams(thumbSize, thumbSize))
|
|
1058
|
-
addView(label, LinearLayout.LayoutParams(dp(72), WRAP))
|
|
1059
|
-
setOnClickListener { onClick() }
|
|
1060
|
-
}
|
|
1061
|
-
return Triple(root, frame, label)
|
|
1062
|
-
}
|
|
1063
|
-
|
|
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
|
+
*/
|
|
1064
1193
|
private fun createFiltersSubBar(session: PhotoEditSession, imageView: ZoomableImageView): LinearLayout {
|
|
1065
|
-
val toolbarColor = themeColor("toolbarColor", DesignTokens.surfaceContainerLow)
|
|
1066
|
-
val textColor = themeColor("textColor", DesignTokens.onSurface)
|
|
1067
1194
|
val primaryColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1068
1195
|
return LinearLayout(this).apply {
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
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") {
|
|
1078
1211
|
session.updateAdjustments { it.copy(filterPreset = null, filterStrength = 100f) }
|
|
1079
1212
|
syncAdjustmentSeekBar(session)
|
|
1080
1213
|
schedulePhotoPreviewRender()
|
|
1081
1214
|
refreshFiltersSelection(session)
|
|
1082
1215
|
}
|
|
1083
|
-
|
|
1216
|
+
original.image.setImageBitmap(session.baseBitmap)
|
|
1217
|
+
filterPresetCards[""] = original
|
|
1218
|
+
addView(original.root, LinearLayout.LayoutParams(WRAP, WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) })
|
|
1084
1219
|
PhotoFilterPresets.PRESET_IDS.forEach { id ->
|
|
1085
|
-
val
|
|
1220
|
+
val thumbnail = editorFilterThumbnail(this@PhotoVideoEditorActivity, PhotoFilterPresets.labelFor(id)) {
|
|
1086
1221
|
session.updateAdjustments { it.copy(filterPreset = if (it.filterPreset == id) null else id) }
|
|
1087
|
-
activeAdjustmentKey = "filterStrength"
|
|
1088
1222
|
syncAdjustmentSeekBar(session)
|
|
1089
1223
|
schedulePhotoPreviewRender()
|
|
1090
1224
|
refreshFiltersSelection(session)
|
|
1091
1225
|
}
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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) })
|
|
1096
1233
|
}
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
1100
|
-
addView(
|
|
1101
|
-
actionButton("Done") { setFiltersMode(false, session) }.apply { setTextColor(primaryColor) },
|
|
1102
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1234
|
+
},
|
|
1235
|
+
LinearLayout.LayoutParams(MATCH, dp(EditorPanelMetrics.STRIP_HEIGHT_DP + 24))
|
|
1103
1236
|
)
|
|
1104
1237
|
}
|
|
1105
1238
|
}
|
|
1106
1239
|
|
|
1107
1240
|
private fun setCropMode(enabled: Boolean, session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
1241
|
+
if (enabled && !cropMode) cropEntryState = session.state
|
|
1108
1242
|
cropMode = enabled
|
|
1109
1243
|
imageView.panZoomEnabled = !enabled
|
|
1110
1244
|
overlay.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1111
|
-
straightenSeekBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1112
1245
|
cropSubBar.visibility = if (enabled) View.VISIBLE else View.GONE
|
|
1113
1246
|
mainToolBar.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1114
1247
|
layerOverlay.visibility = if (enabled) View.GONE else View.VISIBLE
|
|
1115
|
-
if (enabled)
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
val isIdentityCrop = state.cropLeft == 0f && state.cropTop == 0f && state.cropRight == 1f && state.cropBottom == 1f
|
|
1119
|
-
overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = isIdentityCrop)
|
|
1120
|
-
}
|
|
1248
|
+
if (!enabled) cropEntryState = null
|
|
1249
|
+
photoCropPanel?.sync(session.state)
|
|
1250
|
+
schedulePhotoPreviewRender()
|
|
1121
1251
|
refreshPhotoToolSelection()
|
|
1122
|
-
refreshSelection(cropAspectButtons, session.state.aspectRatio, themeColor("textColor", DesignTokens.onSurface), themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
1123
1252
|
}
|
|
1124
1253
|
|
|
1125
|
-
private fun
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
val aspectPresets = listOf<Pair<String, Float?>>(
|
|
1130
|
-
"Free" to null, "1:1" to 1f, "4:5" to 4f / 5f, "3:4" to 3f / 4f, "9:16" to 9f / 16f, "16:9" to 16f / 9f
|
|
1131
|
-
)
|
|
1132
|
-
return LinearLayout(this).apply {
|
|
1133
|
-
orientation = LinearLayout.VERTICAL
|
|
1134
|
-
setPadding(0, dp(6), 0, dp(6))
|
|
1135
|
-
setBackgroundColor(toolbarColor)
|
|
1136
|
-
|
|
1137
|
-
addView(TextView(this@PhotoVideoEditorActivity).apply {
|
|
1138
|
-
text = "Crop Mode • TRANSFORM & ASPECT RATIO"
|
|
1139
|
-
textSize = 14f
|
|
1140
|
-
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
|
1141
|
-
setTextColor(textColor)
|
|
1142
|
-
gravity = Gravity.CENTER_VERTICAL
|
|
1143
|
-
setPadding(dp(DesignTokens.spaceLg), 0, dp(DesignTokens.spaceLg), 0)
|
|
1144
|
-
}, LinearLayout.LayoutParams(MATCH, dp(36)))
|
|
1145
|
-
|
|
1146
|
-
// Rotate/Flip row — moved here from the main tool rail to match the crop_transform_editor mockup.
|
|
1147
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1148
|
-
orientation = LinearLayout.HORIZONTAL
|
|
1149
|
-
gravity = Gravity.CENTER
|
|
1150
|
-
addView(
|
|
1151
|
-
createIconButton(R.drawable.ic_rotate_left, "Rotate left", textColor) {
|
|
1152
|
-
session.update { it.rotatedRight().rotatedRight().rotatedRight() }
|
|
1153
|
-
schedulePhotoPreviewRender()
|
|
1154
|
-
imageView.post { imageView.resetToFit(); overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = true) }
|
|
1155
|
-
},
|
|
1156
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
1157
|
-
)
|
|
1158
|
-
addView(
|
|
1159
|
-
createIconButton(R.drawable.ic_rotate_right, "Rotate right", textColor) {
|
|
1160
|
-
session.update { it.rotatedRight() }
|
|
1161
|
-
schedulePhotoPreviewRender()
|
|
1162
|
-
imageView.post { imageView.resetToFit(); overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = true) }
|
|
1163
|
-
},
|
|
1164
|
-
LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin))
|
|
1165
|
-
)
|
|
1166
|
-
}, LinearLayout.LayoutParams(MATCH, dp(DesignTokens.touchTargetMin)))
|
|
1254
|
+
private fun cancelCropMode(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
1255
|
+
cropEntryState?.let { snapshot -> session.update { snapshot } }
|
|
1256
|
+
setCropMode(false, session, imageView, overlay)
|
|
1257
|
+
}
|
|
1167
1258
|
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
actionButton("Reset") {
|
|
1186
|
-
session.update { it.reset() }
|
|
1187
|
-
overlay.setAspectRatio(null)
|
|
1188
|
-
straightenSeekBar.progress = 45
|
|
1189
|
-
schedulePhotoPreviewRender()
|
|
1190
|
-
imageView.resetToFit()
|
|
1191
|
-
overlay.setImageBounds(imageView.currentImageBounds(), resetCrop = true)
|
|
1192
|
-
refreshSelection(cropAspectButtons, null, textColor, primaryColor)
|
|
1193
|
-
}.apply { setTextColor(textColor) },
|
|
1194
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1195
|
-
)
|
|
1196
|
-
})
|
|
1197
|
-
}, LinearLayout.LayoutParams(0, MATCH, 1f))
|
|
1198
|
-
addView(
|
|
1199
|
-
actionButton("Done") { setCropMode(false, session, imageView, overlay) }.apply { setTextColor(primaryColor) },
|
|
1200
|
-
LinearLayout.LayoutParams(dp(72), MATCH)
|
|
1201
|
-
)
|
|
1202
|
-
}, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
1203
|
-
}
|
|
1259
|
+
private fun createCropSubBar(session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView): LinearLayout {
|
|
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) }
|
|
1204
1276
|
}
|
|
1205
1277
|
|
|
1206
|
-
/** 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. */
|
|
1207
1278
|
private fun setResizeMode(enabled: Boolean, session: PhotoEditSession, imageView: ZoomableImageView, overlay: CropOverlayView) {
|
|
1208
1279
|
resizeMode = enabled
|
|
1209
1280
|
imageView.panZoomEnabled = !enabled
|
|
@@ -1266,6 +1337,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1266
1337
|
finishWithError("Nothing to export.", "E_INTERNAL")
|
|
1267
1338
|
return
|
|
1268
1339
|
}
|
|
1340
|
+
if (drawMode) setDrawMode(false, session)
|
|
1269
1341
|
exportProgressView.visibility = View.VISIBLE
|
|
1270
1342
|
val state = session.state
|
|
1271
1343
|
val adjustments = session.adjustments
|
|
@@ -1316,7 +1388,12 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1316
1388
|
root.addView(header, LinearLayout.LayoutParams(MATCH, headerHeight))
|
|
1317
1389
|
|
|
1318
1390
|
val preview = FrameLayout(this).apply { setBackgroundColor(Color.BLACK) }
|
|
1319
|
-
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
|
+
)
|
|
1320
1397
|
|
|
1321
1398
|
val session = VideoEditSession(this, sourceUri)
|
|
1322
1399
|
videoSession = session
|
|
@@ -1349,6 +1426,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1349
1426
|
useController = false
|
|
1350
1427
|
contentDescription = "Selected video preview"
|
|
1351
1428
|
}
|
|
1429
|
+
cropPlayerView = playerView
|
|
1352
1430
|
preview.addView(playerView, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1353
1431
|
|
|
1354
1432
|
val overlayView = ImageView(this).apply {
|
|
@@ -1360,9 +1438,33 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1360
1438
|
videoOverlayImageView = overlayView
|
|
1361
1439
|
preview.addView(overlayView, FrameLayout.LayoutParams(MATCH, MATCH))
|
|
1362
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
|
+
}
|
|
1363
1458
|
val layerOverlay = LayerOverlayView(this)
|
|
1364
1459
|
videoLayerOverlay = layerOverlay
|
|
1365
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
|
+
}
|
|
1366
1468
|
layerOverlay.onLayerTapped = { id -> selectVideoLayer(id, session) }
|
|
1367
1469
|
layerOverlay.onLayerTransformChanged = { id, x, y, scale, rotation ->
|
|
1368
1470
|
if (videoDragStartSnapshot == null) videoDragStartSnapshot = session.layerStack.layers
|
|
@@ -1378,44 +1480,82 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1378
1480
|
onVideoLayerStackChanged()
|
|
1379
1481
|
}
|
|
1380
1482
|
|
|
1381
|
-
val
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1483
|
+
val accentColor = themeColor("primaryColor", DesignTokens.primaryContainer)
|
|
1484
|
+
|
|
1485
|
+
val playPause = ImageButton(this).apply {
|
|
1486
|
+
contentDescription = "Play video"
|
|
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))
|
|
1385
1492
|
setOnClickListener {
|
|
1386
|
-
if (session.player.isPlaying)
|
|
1493
|
+
if (session.player.isPlaying) {
|
|
1494
|
+
session.player.pause()
|
|
1495
|
+
} else {
|
|
1496
|
+
if (session.player.playbackState == Player.STATE_ENDED || session.globalPositionMs() >= session.durationMs - 50L) {
|
|
1497
|
+
session.seekToGlobalMs(0)
|
|
1498
|
+
}
|
|
1499
|
+
session.player.play()
|
|
1500
|
+
}
|
|
1501
|
+
setVideoControlsVisible(true)
|
|
1387
1502
|
}
|
|
1503
|
+
applyPressScale()
|
|
1388
1504
|
}
|
|
1389
1505
|
playPauseButton = playPause
|
|
1390
1506
|
val time = TextView(this).apply {
|
|
1391
1507
|
text = "0:00 / 0:00"
|
|
1392
|
-
setTextColor(
|
|
1393
|
-
textSize =
|
|
1508
|
+
setTextColor(Color.WHITE)
|
|
1509
|
+
textSize = 11f
|
|
1394
1510
|
}
|
|
1395
1511
|
timeLabel = time
|
|
1396
1512
|
val transport = LinearLayout(this).apply {
|
|
1397
1513
|
orientation = LinearLayout.HORIZONTAL
|
|
1398
1514
|
gravity = Gravity.CENTER_VERTICAL
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
addView(playPause, LinearLayout.LayoutParams(dp(72), WRAP))
|
|
1402
|
-
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) })
|
|
1403
1517
|
}
|
|
1404
|
-
root.addView(transport, LinearLayout.LayoutParams(MATCH, dp(40)))
|
|
1405
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
|
+
}
|
|
1406
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
|
|
1407
1535
|
max = 1000
|
|
1408
1536
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
1409
1537
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
1410
1538
|
if (!fromUser || session.durationMs <= 0) return
|
|
1411
1539
|
session.seekToGlobalMs(session.durationMs * progress / 1000)
|
|
1540
|
+
updateTimeLabel()
|
|
1412
1541
|
}
|
|
1413
|
-
override fun onStartTrackingTouch(seekBar: SeekBar?) { seeking = true }
|
|
1414
|
-
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) }
|
|
1415
1544
|
})
|
|
1416
1545
|
}
|
|
1417
1546
|
positionSeekBar = seek
|
|
1418
|
-
|
|
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) }
|
|
1419
1559
|
|
|
1420
1560
|
val trim = TrimRangeView(this).apply {
|
|
1421
1561
|
// Bound to the currently selected clip (Milestone 7): fractions are relative to that clip's
|
|
@@ -1433,14 +1573,16 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1433
1573
|
}
|
|
1434
1574
|
}
|
|
1435
1575
|
trimRangeView = trim
|
|
1436
|
-
|
|
1576
|
+
// Single-video editor: keep the internal range initialized for export,
|
|
1577
|
+
// but do not show the trim/clip timeline UI.
|
|
1437
1578
|
|
|
1438
1579
|
val strip = LinearLayout(this).apply {
|
|
1439
1580
|
orientation = LinearLayout.HORIZONTAL
|
|
1440
1581
|
setPadding(dp(8), dp(4), dp(8), dp(4))
|
|
1441
1582
|
}
|
|
1583
|
+
// Keep the internal container initialized for the single-clip session,
|
|
1584
|
+
// but do not add the multi-clip strip/actions to the video editor UI.
|
|
1442
1585
|
clipStripBar = strip
|
|
1443
|
-
root.addView(strip, LinearLayout.LayoutParams(MATCH, WRAP))
|
|
1444
1586
|
|
|
1445
1587
|
session.onClipsReady = {
|
|
1446
1588
|
runOnUiThread {
|
|
@@ -1460,6 +1602,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1460
1602
|
}
|
|
1461
1603
|
}
|
|
1462
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)))
|
|
1463
1616
|
val aspectBar = createVideoAspectSubBar(session, playerView)
|
|
1464
1617
|
aspectBar.visibility = View.GONE
|
|
1465
1618
|
videoAspectSubBar = aspectBar
|
|
@@ -1484,32 +1637,43 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1484
1637
|
videoFiltersSubBar = filtersBar
|
|
1485
1638
|
root.addView(filtersBar, LinearLayout.LayoutParams(MATCH, dp(56)))
|
|
1486
1639
|
|
|
1487
|
-
val layerPropertySeek =
|
|
1640
|
+
val layerPropertySeek = EditorToolSlider(this).apply {
|
|
1641
|
+
progressTintList = android.content.res.ColorStateList.valueOf(themeColor("primaryColor", DesignTokens.primaryContainer))
|
|
1642
|
+
thumbTintList = progressTintList
|
|
1488
1643
|
visibility = View.GONE
|
|
1489
1644
|
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
|
1490
1645
|
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
|
1491
1646
|
if (fromUser) onVideoLayerPropertyChanged(progress, session)
|
|
1492
1647
|
}
|
|
1493
|
-
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
|
1494
|
-
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() }
|
|
1495
1650
|
})
|
|
1496
1651
|
}
|
|
1497
1652
|
videoLayerPropertySeekBar = layerPropertySeek
|
|
1498
1653
|
root.addView(layerPropertySeek, LinearLayout.LayoutParams(MATCH, WRAP).apply { setMargins(dp(8), dp(4), dp(8), 0) })
|
|
1499
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)))
|
|
1500
1662
|
val layerBar = createVideoLayerToolBar(session)
|
|
1501
1663
|
layerBar.visibility = View.GONE
|
|
1502
1664
|
videoLayerToolBar = layerBar
|
|
1503
|
-
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(
|
|
1665
|
+
root.addView(layerBar, LinearLayout.LayoutParams(MATCH, dp(76)))
|
|
1504
1666
|
|
|
1505
|
-
|
|
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)
|
|
1506
1670
|
val toolbar = createVideoToolBar(session, playerView)
|
|
1507
1671
|
videoToolBar = toolbar
|
|
1508
1672
|
root.addView(toolbar, LinearLayout.LayoutParams(MATCH, toolbarHeight))
|
|
1509
1673
|
applySystemBarInsets(
|
|
1510
1674
|
header,
|
|
1511
1675
|
headerHeight,
|
|
1512
|
-
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))
|
|
1513
1677
|
)
|
|
1514
1678
|
|
|
1515
1679
|
startPositionPolling(session)
|
|
@@ -1517,13 +1681,20 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1517
1681
|
}
|
|
1518
1682
|
|
|
1519
1683
|
private fun startPositionPolling(session: VideoEditSession) {
|
|
1684
|
+
var wasPlaying = false
|
|
1520
1685
|
val runnable = object : Runnable {
|
|
1521
1686
|
override fun run() {
|
|
1522
1687
|
if (!seeking && session.durationMs > 0) {
|
|
1523
1688
|
positionSeekBar.progress = (session.globalPositionMs() * 1000 / session.durationMs).toInt().coerceIn(0, 1000)
|
|
1524
1689
|
updateTimeLabel()
|
|
1525
1690
|
}
|
|
1526
|
-
|
|
1691
|
+
val playing = session.player.isPlaying
|
|
1692
|
+
playPauseButton.setImageResource(if (playing) R.drawable.ic_pause else R.drawable.ic_play)
|
|
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
|
+
}
|
|
1527
1698
|
refreshVideoOverlayPreview(session)
|
|
1528
1699
|
positionPollHandler.postDelayed(this, 250)
|
|
1529
1700
|
}
|
|
@@ -1532,6 +1703,35 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1532
1703
|
positionPollHandler.postDelayed(runnable, 250)
|
|
1533
1704
|
}
|
|
1534
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
|
+
|
|
1535
1735
|
private fun updateTimeLabel() {
|
|
1536
1736
|
val session = videoSession ?: return
|
|
1537
1737
|
timeLabel.text = "${formatMs(session.globalPositionMs())} / ${formatMs(session.durationMs)}"
|
|
@@ -1561,9 +1761,16 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1561
1761
|
videoOverlayRenderKey = null
|
|
1562
1762
|
return
|
|
1563
1763
|
}
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
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)
|
|
1567
1774
|
val previewScale = minOf(1f, 1280f / maxOf(sourceWidth, sourceHeight))
|
|
1568
1775
|
val width = (sourceWidth * previewScale).roundToInt().coerceAtLeast(2)
|
|
1569
1776
|
val height = (sourceHeight * previewScale).roundToInt().coerceAtLeast(2)
|
|
@@ -1589,23 +1796,62 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1589
1796
|
return bitmap
|
|
1590
1797
|
}
|
|
1591
1798
|
|
|
1592
|
-
/**
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
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)
|
|
1599
1844
|
}
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
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)
|
|
1606
1853
|
}
|
|
1607
1854
|
|
|
1608
|
-
/** Mirrors [onLayerStackChanged] for the video overlay stack (Milestone 6). */
|
|
1609
1855
|
private fun onVideoLayerStackChanged() {
|
|
1610
1856
|
val session = videoSession ?: return
|
|
1611
1857
|
if (selectedVideoLayerId != null && session.layerStack.layers.none { it.id == selectedVideoLayerId }) {
|
|
@@ -1621,16 +1867,25 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1621
1867
|
}
|
|
1622
1868
|
|
|
1623
1869
|
private fun selectVideoLayer(id: String?, session: VideoEditSession) {
|
|
1870
|
+
if (selectedVideoLayerId != id) activeVideoLayerPropertyKey = ""
|
|
1624
1871
|
selectedVideoLayerId = id
|
|
1625
1872
|
videoLayerOverlay.selectedLayerId = id
|
|
1626
1873
|
setVideoLayerToolBarVisible(id != null, session)
|
|
1627
1874
|
}
|
|
1628
1875
|
|
|
1629
1876
|
private fun setVideoLayerToolBarVisible(visible: Boolean, session: VideoEditSession) {
|
|
1877
|
+
if (visible) setVideoControlsVisible(true)
|
|
1630
1878
|
videoLayerToolBar.visibility = if (visible) View.VISIBLE else View.GONE
|
|
1631
|
-
videoLayerPropertySeekBar.visibility = if (visible) View.VISIBLE else View.GONE
|
|
1632
1879
|
videoToolBar.visibility = if (visible) View.GONE else View.VISIBLE
|
|
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
|
|
1633
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)
|
|
1634
1889
|
}
|
|
1635
1890
|
|
|
1636
1891
|
private fun currentSelectedVideoLayer(session: VideoEditSession): PhotoLayer? =
|
|
@@ -1639,12 +1894,14 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1639
1894
|
private fun syncVideoLayerPropertySeekBar(session: VideoEditSession) {
|
|
1640
1895
|
val layer = currentSelectedVideoLayer(session) ?: return
|
|
1641
1896
|
val (min, max, value) = when (activeVideoLayerPropertyKey) {
|
|
1642
|
-
"rotation" -> Triple(-180f, 180f, layer.rotationDegrees)
|
|
1897
|
+
"rotation" -> Triple(-180f, 180f, ((layer.rotationDegrees % 360f + 540f) % 360f) - 180f)
|
|
1643
1898
|
"opacity" -> Triple(0f, 100f, layer.opacity * 100f)
|
|
1899
|
+
"fontSize" -> Triple(12f, 160f, layer.fontSize)
|
|
1644
1900
|
"start" -> Triple(0f, session.durationMs.toFloat(), layer.startMs.toFloat())
|
|
1645
1901
|
"end" -> Triple(0f, session.durationMs.toFloat(), session.durationMs.effectiveEndOr(layer.endMs).toFloat())
|
|
1646
1902
|
else -> Triple(20f, 800f, layer.scale * 100f)
|
|
1647
1903
|
}
|
|
1904
|
+
(videoLayerPropertySeekBar as? EditorToolSlider)?.propertyKey = activeVideoLayerPropertyKey
|
|
1648
1905
|
videoLayerPropertySeekBar.max = (max - min).roundToInt()
|
|
1649
1906
|
videoLayerPropertySeekBar.progress = (value - min).roundToInt()
|
|
1650
1907
|
}
|
|
@@ -1656,6 +1913,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1656
1913
|
val min = when (activeVideoLayerPropertyKey) {
|
|
1657
1914
|
"rotation" -> -180f
|
|
1658
1915
|
"opacity", "start", "end" -> 0f
|
|
1916
|
+
"fontSize" -> 12f
|
|
1659
1917
|
else -> 20f
|
|
1660
1918
|
}
|
|
1661
1919
|
val value = min + progress
|
|
@@ -1665,6 +1923,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1665
1923
|
when (activeVideoLayerPropertyKey) {
|
|
1666
1924
|
"rotation" -> layer.copy(rotationDegrees = value)
|
|
1667
1925
|
"opacity" -> layer.copy(opacity = value / 100f)
|
|
1926
|
+
"fontSize" -> layer.copy(fontSize = value)
|
|
1668
1927
|
"start" -> layer.copy(startMs = value.toLong().coerceAtMost(layer.endMs.takeIf { it > 0 } ?: session.durationMs))
|
|
1669
1928
|
"end" -> layer.copy(endMs = value.toLong().coerceAtLeast(layer.startMs))
|
|
1670
1929
|
else -> layer.copy(scale = value / 100f)
|
|
@@ -1674,101 +1933,102 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1674
1933
|
refreshVideoOverlayPreview(session)
|
|
1675
1934
|
}
|
|
1676
1935
|
|
|
1677
|
-
private fun createVideoLayerToolBar(session: VideoEditSession): LinearLayout {
|
|
1678
|
-
val
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
"
|
|
1683
|
-
|
|
1684
|
-
|
|
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
|
|
1685
1969
|
gravity = Gravity.CENTER_VERTICAL
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1691
|
-
orientation = LinearLayout.HORIZONTAL
|
|
1692
|
-
properties.forEach { (key, label) ->
|
|
1693
|
-
val chip = actionButton(label) {
|
|
1694
|
-
activeVideoLayerPropertyKey = key
|
|
1695
|
-
syncVideoLayerPropertySeekBar(session)
|
|
1696
|
-
refreshSelection(videoLayerPropertyButtons, activeVideoLayerPropertyKey, textColor, primaryColor)
|
|
1697
|
-
}.apply { setTextColor(textColor) }
|
|
1698
|
-
videoLayerPropertyButtons[key] = chip
|
|
1699
|
-
addView(chip, LinearLayout.LayoutParams(dp(72), MATCH))
|
|
1700
|
-
}
|
|
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) ->
|
|
1701
1974
|
addView(
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
session.layerStack.commit { list -> list.filterNot { it.id == id } }
|
|
1705
|
-
selectVideoLayer(null, session)
|
|
1706
|
-
onVideoLayerStackChanged()
|
|
1707
|
-
}.apply { setTextColor(textColor) },
|
|
1708
|
-
LinearLayout.LayoutParams(dp(80), MATCH)
|
|
1975
|
+
createVideoDockAction(key, label) { onVideoToolTapped(key, session, playerView) },
|
|
1976
|
+
LinearLayout.LayoutParams(0, MATCH, 1f)
|
|
1709
1977
|
)
|
|
1710
|
-
}
|
|
1711
|
-
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return FrameLayout(this).apply {
|
|
1712
1981
|
addView(
|
|
1713
|
-
|
|
1714
|
-
|
|
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
|
+
}
|
|
1715
1986
|
)
|
|
1716
1987
|
}
|
|
1717
1988
|
}
|
|
1718
1989
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
.
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
}
|
|
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)
|
|
1732
2002
|
}
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
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)
|
|
1739
2010
|
return LinearLayout(this).apply {
|
|
1740
|
-
orientation = LinearLayout.
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
)
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
)
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
addView(LinearLayout(this@PhotoVideoEditorActivity).apply {
|
|
1762
|
-
orientation = LinearLayout.HORIZONTAL
|
|
1763
|
-
gravity = Gravity.CENTER_VERTICAL
|
|
1764
|
-
setPadding(dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs), dp(DesignTokens.spaceSm), dp(DesignTokens.spaceXs))
|
|
1765
|
-
tools.filter { features?.optBoolean(it.first, true) != false }.forEach { (key, label, iconRes) ->
|
|
1766
|
-
val node = createToolNode(iconRes, label) { onVideoToolTapped(key, session, playerView) }
|
|
1767
|
-
videoToolButtons[key] = node
|
|
1768
|
-
addView(node.root, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), WRAP).apply { marginEnd = dp(DesignTokens.spaceSm) })
|
|
1769
|
-
}
|
|
1770
|
-
})
|
|
1771
|
-
}, 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()
|
|
1772
2032
|
}
|
|
1773
2033
|
}
|
|
1774
2034
|
|
|
@@ -1786,6 +2046,7 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1786
2046
|
|
|
1787
2047
|
private fun onVideoToolTapped(key: String, session: VideoEditSession, playerView: PlayerView) {
|
|
1788
2048
|
when (key) {
|
|
2049
|
+
"crop" -> setVideoCropMode(true, session)
|
|
1789
2050
|
"cover" -> {
|
|
1790
2051
|
val globalMs = session.globalPositionMs()
|
|
1791
2052
|
session.update { it.withCoverFrame(globalMs) }
|
|
@@ -1810,45 +2071,21 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
1810
2071
|
}
|
|
1811
2072
|
|
|
1812
2073
|
private fun showVideoStickerPicker(session: VideoEditSession) {
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
.
|
|
1826
|
-
.
|
|
1827
|
-
|
|
1828
|
-
index < builtin.size -> {
|
|
1829
|
-
val layer = PhotoLayer(type = LayerType.STICKER, stickerId = builtin[index])
|
|
1830
|
-
session.layerStack.commit { it + layer }
|
|
1831
|
-
onVideoLayerStackChanged()
|
|
1832
|
-
selectVideoLayer(layer.id, session)
|
|
1833
|
-
}
|
|
1834
|
-
index < builtin.size + consumerCount -> {
|
|
1835
|
-
val asset = consumerAssets?.optJSONObject(index - builtin.size)
|
|
1836
|
-
val uri = asset?.optString("uri").takeUnless { it.isNullOrBlank() } ?: return@setItems
|
|
1837
|
-
val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = uri)
|
|
1838
|
-
session.layerStack.commit { it + layer }
|
|
1839
|
-
onVideoLayerStackChanged()
|
|
1840
|
-
selectVideoLayer(layer.id, session)
|
|
1841
|
-
}
|
|
1842
|
-
index == builtin.size + consumerCount -> launchOverlayPicker(REQUEST_CODE_VIDEO_STICKER_UPLOAD)
|
|
1843
|
-
else -> OnlineStickerSheet(this) { path ->
|
|
1844
|
-
val layer = PhotoLayer(type = LayerType.STICKER, stickerUri = Uri.fromFile(File(path)).toString())
|
|
1845
|
-
session.layerStack.commit { it + layer }
|
|
1846
|
-
onVideoLayerStackChanged()
|
|
1847
|
-
selectVideoLayer(layer.id, session)
|
|
1848
|
-
}.show()
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
.show()
|
|
2074
|
+
OnlineStickerSheet(this, runtimeStickerAssets()) { path ->
|
|
2075
|
+
val layer = newSticker(stickerUri = Uri.fromFile(File(path)).toString())
|
|
2076
|
+
session.layerStack.commit { it + layer }
|
|
2077
|
+
onVideoLayerStackChanged()
|
|
2078
|
+
selectVideoLayer(layer.id, session)
|
|
2079
|
+
}.show()
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
private fun runtimeStickerAssets(): List<RuntimeSticker> {
|
|
2083
|
+
val assets = request.optJSONArray("stickerAssets") ?: return emptyList()
|
|
2084
|
+
return (0 until assets.length()).mapNotNull { index ->
|
|
2085
|
+
val asset = assets.optJSONObject(index) ?: return@mapNotNull null
|
|
2086
|
+
val uri = asset.optString("uri").takeIf { it.isNotBlank() } ?: return@mapNotNull null
|
|
2087
|
+
RuntimeSticker(asset.optString("id").takeIf { it.isNotBlank() } ?: "sticker-${index + 1}", uri)
|
|
2088
|
+
}
|
|
1852
2089
|
}
|
|
1853
2090
|
|
|
1854
2091
|
private fun formatSpeedLabel(speed: Float): String {
|
|
@@ -2227,7 +2464,9 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2227
2464
|
|
|
2228
2465
|
addView(TextView(this@PhotoVideoEditorActivity).apply {
|
|
2229
2466
|
text = if (mediaType == "photo") "Photo Editor" else "Video Editor"
|
|
2230
|
-
textSize =
|
|
2467
|
+
textSize = 14f
|
|
2468
|
+
maxLines = 1
|
|
2469
|
+
ellipsize = android.text.TextUtils.TruncateAt.END
|
|
2231
2470
|
gravity = Gravity.CENTER
|
|
2232
2471
|
setTextColor(textColor)
|
|
2233
2472
|
setTypeface(typeface, android.graphics.Typeface.BOLD)
|
|
@@ -2241,25 +2480,16 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2241
2480
|
if (mediaType == "photo") { photoSession?.layerStack?.redo(); onLayerStackChanged() }
|
|
2242
2481
|
else { videoSession?.layerStack?.redo(); onVideoLayerStackChanged() }
|
|
2243
2482
|
}
|
|
2483
|
+
undo.isEnabled = false; undo.alpha = 0.4f
|
|
2484
|
+
redo.isEnabled = false; redo.alpha = 0.4f
|
|
2244
2485
|
undoButton = undo
|
|
2245
2486
|
redoButton = redo
|
|
2246
2487
|
addView(undo, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2247
2488
|
addView(redo, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2248
2489
|
|
|
2249
|
-
if (mediaType == "photo") {
|
|
2250
|
-
addView(createIconButton(R.drawable.ic_compare, "Hold to compare with original", textColor) {}.apply {
|
|
2251
|
-
setOnTouchListener { _, event ->
|
|
2252
|
-
when (event.actionMasked) {
|
|
2253
|
-
MotionEvent.ACTION_DOWN -> showOriginalPreview(true)
|
|
2254
|
-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> showOriginalPreview(false)
|
|
2255
|
-
}
|
|
2256
|
-
true
|
|
2257
|
-
}
|
|
2258
|
-
}, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2259
|
-
}
|
|
2260
2490
|
|
|
2261
2491
|
addView(
|
|
2262
|
-
editorPrimaryButton(this@PhotoVideoEditorActivity, "Export",
|
|
2492
|
+
editorPrimaryButton(this@PhotoVideoEditorActivity, "Export", null, primaryColor, onPrimaryColor) {
|
|
2263
2493
|
if (mediaType == "photo") showPhotoExportConfiguration() else finishDone()
|
|
2264
2494
|
},
|
|
2265
2495
|
LinearLayout.LayoutParams(WRAP, dp(DesignTokens.touchTargetMin)).apply { marginStart = dp(DesignTokens.spaceXs) }
|
|
@@ -2381,17 +2611,20 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2381
2611
|
val label = TextView(this).apply {
|
|
2382
2612
|
text = labelText
|
|
2383
2613
|
textSize = 11f
|
|
2614
|
+
maxLines = 1
|
|
2615
|
+
includeFontPadding = false
|
|
2384
2616
|
gravity = Gravity.CENTER
|
|
2385
2617
|
setTextColor(DesignTokens.outline)
|
|
2386
|
-
setPadding(0, dp(
|
|
2618
|
+
setPadding(0, dp(2), 0, 0)
|
|
2387
2619
|
}
|
|
2388
2620
|
val root = LinearLayout(this).apply {
|
|
2389
2621
|
orientation = LinearLayout.VERTICAL
|
|
2390
2622
|
gravity = Gravity.CENTER_HORIZONTAL
|
|
2391
2623
|
isClickable = true
|
|
2392
2624
|
isFocusable = true
|
|
2625
|
+
contentDescription = labelText
|
|
2393
2626
|
background = roundedDrawable(Color.TRANSPARENT, DesignTokens.radiusLg)
|
|
2394
|
-
setPadding(0, dp(
|
|
2627
|
+
setPadding(0, dp(2), 0, dp(2))
|
|
2395
2628
|
addView(tile, LinearLayout.LayoutParams(dp(DesignTokens.touchTargetMin), dp(DesignTokens.touchTargetMin)))
|
|
2396
2629
|
addView(label, LinearLayout.LayoutParams(WRAP, WRAP))
|
|
2397
2630
|
setOnClickListener { onClick() }
|
|
@@ -2401,17 +2634,23 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2401
2634
|
}
|
|
2402
2635
|
|
|
2403
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
|
+
*/
|
|
2404
2641
|
private fun setToolNodeSelected(node: ToolNode, selected: Boolean, textColor: Int) {
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
)
|
|
2409
|
-
node.
|
|
2410
|
-
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)
|
|
2411
2647
|
}
|
|
2412
2648
|
|
|
2413
2649
|
private fun <K> refreshToolNodeSelection(nodes: Map<K, ToolNode>, activeKey: K?, textColor: Int) {
|
|
2414
|
-
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
|
+
}
|
|
2415
2654
|
}
|
|
2416
2655
|
|
|
2417
2656
|
private fun showPreviewError(container: FrameLayout) {
|
|
@@ -2460,7 +2699,13 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2460
2699
|
}
|
|
2461
2700
|
|
|
2462
2701
|
@Deprecated("Deprecated in Java")
|
|
2463
|
-
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
|
+
}
|
|
2464
2709
|
|
|
2465
2710
|
override fun onTrimMemory(level: Int) {
|
|
2466
2711
|
super.onTrimMemory(level)
|
|
@@ -2493,12 +2738,17 @@ class PhotoVideoEditorActivity : Activity() {
|
|
|
2493
2738
|
|
|
2494
2739
|
companion object {
|
|
2495
2740
|
/** Rotate icon row (44dp) + aspect-preset chip row (56dp). */
|
|
2496
|
-
const val CROP_SUB_BAR_HEIGHT_DP =
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
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
|
|
2500
2746
|
/** 64dp asset card + optional caption + vertical padding, for the Stickers/Shapes grid. */
|
|
2501
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
|
|
2502
2752
|
private const val TIMELINE_THUMBNAIL_COUNT = 12
|
|
2503
2753
|
const val EXTRA_URI = "photoVideoEditor.uri"
|
|
2504
2754
|
const val EXTRA_TYPE = "photoVideoEditor.type"
|