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
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
package com.photovideoeditor.ui.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Color
|
|
5
|
+
import android.graphics.Typeface
|
|
6
|
+
import android.view.Gravity
|
|
7
|
+
import android.view.View
|
|
8
|
+
import android.widget.FrameLayout
|
|
9
|
+
import android.widget.HorizontalScrollView
|
|
10
|
+
import android.widget.ImageView
|
|
11
|
+
import android.widget.LinearLayout
|
|
12
|
+
import android.widget.TextView
|
|
13
|
+
import com.photovideoeditor.ui.DesignTokens
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shared building blocks for the editor's bottom panels (Crop / Adjust / Filters / Text / Stickers).
|
|
17
|
+
*
|
|
18
|
+
* Before this existed each panel hand-rolled its own header, chips and spacing, which is how the
|
|
19
|
+
* Adjust panel ended up with four full-height rectangular buttons while Filters used thumbnail cards.
|
|
20
|
+
* Every panel now composes the same three pieces — [editorPanelHeader], one slider, one
|
|
21
|
+
* [editorToolStrip] — so heights, padding, radii, typography and selected states stay in step.
|
|
22
|
+
*
|
|
23
|
+
* Fixed vertical budget (see PANEL_* below): header 40dp + slider ~48dp + strip 76dp, so a panel
|
|
24
|
+
* never grows past ~164dp and the photo keeps the rest of the screen.
|
|
25
|
+
*/
|
|
26
|
+
object EditorPanelMetrics {
|
|
27
|
+
const val HEADER_HEIGHT_DP = 40
|
|
28
|
+
const val SLIDER_HEIGHT_DP = 48
|
|
29
|
+
const val STRIP_HEIGHT_DP = 76
|
|
30
|
+
const val STRIP_ITEM_WIDTH_DP = 68
|
|
31
|
+
const val ICON_SIZE_DP = 22
|
|
32
|
+
const val THUMBNAIL_WIDTH_DP = 64
|
|
33
|
+
const val THUMBNAIL_HEIGHT_DP = 72
|
|
34
|
+
const val TOTAL_HEIGHT_DP = HEADER_HEIGHT_DP + SLIDER_HEIGHT_DP + STRIP_HEIGHT_DP
|
|
35
|
+
/** Panel without the slider row (e.g. a picker that has nothing to scrub). */
|
|
36
|
+
const val HEADER_AND_STRIP_HEIGHT_DP = HEADER_HEIGHT_DP + STRIP_HEIGHT_DP
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Compact panel header: `Title [Reset] Done`.
|
|
41
|
+
*
|
|
42
|
+
* "Done" is an action, not a tool — it gets a 44dp touch target via padding while staying visually
|
|
43
|
+
* small, rather than becoming another full-width card.
|
|
44
|
+
*/
|
|
45
|
+
fun editorPanelHeader(
|
|
46
|
+
context: Context,
|
|
47
|
+
title: String,
|
|
48
|
+
accentColor: Int,
|
|
49
|
+
secondaryLabel: String? = null,
|
|
50
|
+
onSecondary: (() -> Unit)? = null,
|
|
51
|
+
onDone: () -> Unit
|
|
52
|
+
): LinearLayout = LinearLayout(context).apply {
|
|
53
|
+
orientation = LinearLayout.HORIZONTAL
|
|
54
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
55
|
+
setPadding(dpToPx(DesignTokens.spaceLg, context), 0, dpToPx(DesignTokens.spaceSm, context), 0)
|
|
56
|
+
addView(TextView(context).apply {
|
|
57
|
+
text = title
|
|
58
|
+
textSize = 14f
|
|
59
|
+
maxLines = 1
|
|
60
|
+
includeFontPadding = false
|
|
61
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
62
|
+
setTextColor(DesignTokens.textPrimary)
|
|
63
|
+
}, LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f))
|
|
64
|
+
if (secondaryLabel != null && onSecondary != null) {
|
|
65
|
+
addView(editorHeaderAction(context, secondaryLabel, DesignTokens.textSecondary, onSecondary))
|
|
66
|
+
}
|
|
67
|
+
addView(editorHeaderAction(context, "Done", accentColor, onDone))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Small text action sized for touch (44dp) without looking like a button. */
|
|
71
|
+
private fun editorHeaderAction(context: Context, label: String, color: Int, onClick: () -> Unit): TextView =
|
|
72
|
+
TextView(context).apply {
|
|
73
|
+
text = label
|
|
74
|
+
textSize = 13f
|
|
75
|
+
maxLines = 1
|
|
76
|
+
gravity = Gravity.CENTER
|
|
77
|
+
includeFontPadding = false
|
|
78
|
+
setTypeface(typeface, Typeface.BOLD)
|
|
79
|
+
setTextColor(color)
|
|
80
|
+
minWidth = dpToPx(DesignTokens.touchTargetMin, context)
|
|
81
|
+
minHeight = dpToPx(DesignTokens.touchTargetMin, context)
|
|
82
|
+
val padH = dpToPx(DesignTokens.spaceMd, context)
|
|
83
|
+
setPadding(padH, 0, padH, 0)
|
|
84
|
+
isClickable = true
|
|
85
|
+
isFocusable = true
|
|
86
|
+
setOnClickListener { onClick() }
|
|
87
|
+
applyPressScale()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Horizontally scrollable strip of compact tool items; the container every panel's list sits in. */
|
|
91
|
+
fun editorToolStrip(context: Context, build: LinearLayout.() -> Unit): HorizontalScrollView =
|
|
92
|
+
HorizontalScrollView(context).apply {
|
|
93
|
+
isHorizontalScrollBarEnabled = false
|
|
94
|
+
clipToPadding = false
|
|
95
|
+
val padH = dpToPx(DesignTokens.spaceMd, context)
|
|
96
|
+
setPadding(padH, 0, padH, 0)
|
|
97
|
+
addView(LinearLayout(context).apply {
|
|
98
|
+
orientation = LinearLayout.HORIZONTAL
|
|
99
|
+
gravity = Gravity.CENTER_VERTICAL
|
|
100
|
+
build()
|
|
101
|
+
}, LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.MATCH_PARENT))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** An icon + single-line label entry in a strip, with an optional "value changed" dot. */
|
|
105
|
+
class EditorStripItem(
|
|
106
|
+
val root: LinearLayout,
|
|
107
|
+
private val iconHolder: FrameLayout,
|
|
108
|
+
private val icon: ImageView,
|
|
109
|
+
private val label: TextView,
|
|
110
|
+
private val dot: View
|
|
111
|
+
) {
|
|
112
|
+
/**
|
|
113
|
+
* Selected state is a purple icon + label over a faint purple disc — never a filled purple tile,
|
|
114
|
+
* which at this size reads as a giant button rather than a selection.
|
|
115
|
+
*/
|
|
116
|
+
fun setSelected(selected: Boolean, accentColor: Int) {
|
|
117
|
+
icon.setColorFilter(if (selected) accentColor else DesignTokens.textSecondary)
|
|
118
|
+
label.setTextColor(if (selected) accentColor else DesignTokens.textSecondary)
|
|
119
|
+
iconHolder.background = if (selected) {
|
|
120
|
+
circleDrawable(DesignTokens.withAlphaPercent(accentColor, 18))
|
|
121
|
+
} else {
|
|
122
|
+
null
|
|
123
|
+
}
|
|
124
|
+
root.isSelected = selected
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Tiny dot marking an adjustment that is no longer at its default value. */
|
|
128
|
+
fun setModified(modified: Boolean, accentColor: Int) {
|
|
129
|
+
dot.visibility = if (modified) View.VISIBLE else View.INVISIBLE
|
|
130
|
+
dot.background = circleDrawable(accentColor)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private fun circleDrawable(color: Int): android.graphics.drawable.GradientDrawable =
|
|
135
|
+
android.graphics.drawable.GradientDrawable().apply {
|
|
136
|
+
shape = android.graphics.drawable.GradientDrawable.OVAL
|
|
137
|
+
setColor(color)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
fun editorStripItem(
|
|
141
|
+
context: Context,
|
|
142
|
+
iconRes: Int,
|
|
143
|
+
labelText: String,
|
|
144
|
+
onClick: () -> Unit
|
|
145
|
+
): EditorStripItem {
|
|
146
|
+
val icon = ImageView(context).apply {
|
|
147
|
+
setImageResource(iconRes)
|
|
148
|
+
setColorFilter(DesignTokens.textSecondary)
|
|
149
|
+
}
|
|
150
|
+
val iconHolder = FrameLayout(context).apply {
|
|
151
|
+
addView(
|
|
152
|
+
icon,
|
|
153
|
+
FrameLayout.LayoutParams(
|
|
154
|
+
dpToPx(EditorPanelMetrics.ICON_SIZE_DP, context),
|
|
155
|
+
dpToPx(EditorPanelMetrics.ICON_SIZE_DP, context),
|
|
156
|
+
Gravity.CENTER
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
val label = TextView(context).apply {
|
|
161
|
+
text = labelText
|
|
162
|
+
textSize = 11f
|
|
163
|
+
// Labels like "Brightness" and "Saturation" must never wrap to a second line.
|
|
164
|
+
maxLines = 1
|
|
165
|
+
includeFontPadding = false
|
|
166
|
+
gravity = Gravity.CENTER
|
|
167
|
+
setTextColor(DesignTokens.textSecondary)
|
|
168
|
+
setPadding(0, dpToPx(DesignTokens.spaceXs, context), 0, 0)
|
|
169
|
+
}
|
|
170
|
+
val dot = View(context).apply { visibility = View.INVISIBLE }
|
|
171
|
+
val root = LinearLayout(context).apply {
|
|
172
|
+
orientation = LinearLayout.VERTICAL
|
|
173
|
+
gravity = Gravity.CENTER_HORIZONTAL
|
|
174
|
+
isClickable = true
|
|
175
|
+
isFocusable = true
|
|
176
|
+
contentDescription = labelText
|
|
177
|
+
minimumHeight = dpToPx(DesignTokens.touchTargetMin, context)
|
|
178
|
+
addView(
|
|
179
|
+
iconHolder,
|
|
180
|
+
LinearLayout.LayoutParams(dpToPx(32, context), dpToPx(32, context))
|
|
181
|
+
)
|
|
182
|
+
addView(label, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT))
|
|
183
|
+
addView(dot, LinearLayout.LayoutParams(dpToPx(4, context), dpToPx(4, context)).apply {
|
|
184
|
+
topMargin = dpToPx(3, context)
|
|
185
|
+
})
|
|
186
|
+
setOnClickListener { onClick() }
|
|
187
|
+
applyPressScale()
|
|
188
|
+
}
|
|
189
|
+
return EditorStripItem(root, iconHolder, icon, label, dot)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Filter thumbnail: a preview of the current photo, outlined in purple when selected.
|
|
194
|
+
* Deliberately a thin outline rather than a filled container, so the photo stays readable.
|
|
195
|
+
*/
|
|
196
|
+
class EditorFilterThumbnail(val root: LinearLayout, val image: ImageView, private val frame: FrameLayout, private val label: TextView) {
|
|
197
|
+
fun setSelected(selected: Boolean, accentColor: Int) {
|
|
198
|
+
frame.background = roundedDrawable(
|
|
199
|
+
color = Color.TRANSPARENT,
|
|
200
|
+
radiusDp = DesignTokens.radiusMd,
|
|
201
|
+
context = root.context,
|
|
202
|
+
strokeColor = if (selected) accentColor else Color.TRANSPARENT,
|
|
203
|
+
strokeWidthDp = 2
|
|
204
|
+
)
|
|
205
|
+
label.setTextColor(if (selected) accentColor else DesignTokens.textSecondary)
|
|
206
|
+
root.isSelected = selected
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
fun editorFilterThumbnail(context: Context, labelText: String, onClick: () -> Unit): EditorFilterThumbnail {
|
|
211
|
+
val image = ImageView(context).apply {
|
|
212
|
+
scaleType = ImageView.ScaleType.CENTER_CROP
|
|
213
|
+
background = roundedDrawable(DesignTokens.surfaceContainerHigh, DesignTokens.radiusMd, context)
|
|
214
|
+
clipToOutline = true
|
|
215
|
+
outlineProvider = android.view.ViewOutlineProvider.BACKGROUND
|
|
216
|
+
}
|
|
217
|
+
// The outline lives on a frame *around* the image so the stroke never crops the preview.
|
|
218
|
+
val frame = FrameLayout(context).apply {
|
|
219
|
+
val inset = dpToPx(2, context)
|
|
220
|
+
setPadding(inset, inset, inset, inset)
|
|
221
|
+
addView(
|
|
222
|
+
image,
|
|
223
|
+
FrameLayout.LayoutParams(
|
|
224
|
+
dpToPx(EditorPanelMetrics.THUMBNAIL_WIDTH_DP, context),
|
|
225
|
+
dpToPx(EditorPanelMetrics.THUMBNAIL_HEIGHT_DP, context)
|
|
226
|
+
)
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
val label = TextView(context).apply {
|
|
230
|
+
text = labelText
|
|
231
|
+
textSize = 11f
|
|
232
|
+
maxLines = 1
|
|
233
|
+
includeFontPadding = false
|
|
234
|
+
gravity = Gravity.CENTER
|
|
235
|
+
setTextColor(DesignTokens.textSecondary)
|
|
236
|
+
setPadding(0, dpToPx(DesignTokens.spaceXs, context), 0, 0)
|
|
237
|
+
}
|
|
238
|
+
val root = LinearLayout(context).apply {
|
|
239
|
+
orientation = LinearLayout.VERTICAL
|
|
240
|
+
gravity = Gravity.CENTER_HORIZONTAL
|
|
241
|
+
isClickable = true
|
|
242
|
+
isFocusable = true
|
|
243
|
+
contentDescription = labelText
|
|
244
|
+
addView(frame, LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT))
|
|
245
|
+
addView(label, LinearLayout.LayoutParams(dpToPx(EditorPanelMetrics.THUMBNAIL_WIDTH_DP + 8, context), LinearLayout.LayoutParams.WRAP_CONTENT))
|
|
246
|
+
setOnClickListener { onClick() }
|
|
247
|
+
applyPressScale()
|
|
248
|
+
}
|
|
249
|
+
return EditorFilterThumbnail(root, image, frame, label)
|
|
250
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
package com.photovideoeditor.ui.components
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.graphics.Canvas
|
|
5
|
+
import android.graphics.Color
|
|
6
|
+
import android.graphics.Paint
|
|
7
|
+
import android.graphics.drawable.ClipDrawable
|
|
8
|
+
import android.graphics.drawable.GradientDrawable
|
|
9
|
+
import android.graphics.drawable.LayerDrawable
|
|
10
|
+
import android.view.Gravity
|
|
11
|
+
import android.widget.SeekBar
|
|
12
|
+
import com.photovideoeditor.ui.DesignTokens
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The editor's one slider. Draws its own `Label ................ value` line above a thin track, so
|
|
16
|
+
* a panel never needs a separate caption row, and the same instance can be re-pointed at whichever
|
|
17
|
+
* adjustment/property is selected instead of every property owning its own slider.
|
|
18
|
+
*/
|
|
19
|
+
class EditorToolSlider(context: Context) : SeekBar(context) {
|
|
20
|
+
var propertyKey: String = ""
|
|
21
|
+
set(value) { field = value; contentDescription = labelText ?: value; invalidate() }
|
|
22
|
+
|
|
23
|
+
/** Overrides the [propertyKey]-derived caption (e.g. "Brightness", "Intensity"). */
|
|
24
|
+
var labelText: String? = null
|
|
25
|
+
set(value) { field = value; contentDescription = value ?: propertyKey; invalidate() }
|
|
26
|
+
|
|
27
|
+
/** Overrides the value readout, given the raw progress. */
|
|
28
|
+
var valueFormatter: ((Int) -> String)? = null
|
|
29
|
+
set(value) { field = value; invalidate() }
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Progress at which the underlying value is neutral/default, for bipolar ranges. When set, a tick
|
|
33
|
+
* is drawn there so it is obvious where "no change" sits.
|
|
34
|
+
*/
|
|
35
|
+
var neutralProgress: Int? = null
|
|
36
|
+
set(value) { field = value; invalidate() }
|
|
37
|
+
|
|
38
|
+
private val density = resources.displayMetrics.density
|
|
39
|
+
private val labelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
40
|
+
color = DesignTokens.textSecondary
|
|
41
|
+
textSize = 12 * density
|
|
42
|
+
}
|
|
43
|
+
private val tickPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = DesignTokens.textSubtle }
|
|
44
|
+
|
|
45
|
+
init {
|
|
46
|
+
minimumHeight = (48 * density).toInt()
|
|
47
|
+
setPadding((16 * density).toInt(), (20 * density).toInt(), (16 * density).toInt(), 0)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Thin track, accent-coloured progress, compact thumb — the slider styling used editor-wide. */
|
|
51
|
+
fun applyCompactTrack(accentColor: Int) {
|
|
52
|
+
val inset = (9 * density).toInt()
|
|
53
|
+
progressDrawable = LayerDrawable(
|
|
54
|
+
arrayOf(
|
|
55
|
+
pill(Color.argb(70, 255, 255, 255)),
|
|
56
|
+
ClipDrawable(pill(accentColor), Gravity.START, ClipDrawable.HORIZONTAL)
|
|
57
|
+
)
|
|
58
|
+
).apply {
|
|
59
|
+
setId(0, android.R.id.background)
|
|
60
|
+
setId(1, android.R.id.progress)
|
|
61
|
+
setLayerInset(0, 0, inset, 0, inset)
|
|
62
|
+
setLayerInset(1, 0, inset, 0, inset)
|
|
63
|
+
}
|
|
64
|
+
thumb = GradientDrawable().apply {
|
|
65
|
+
shape = GradientDrawable.OVAL
|
|
66
|
+
setColor(Color.WHITE)
|
|
67
|
+
setSize((12 * density).toInt(), (12 * density).toInt())
|
|
68
|
+
}
|
|
69
|
+
splitTrack = false
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private fun pill(color: Int) = GradientDrawable().apply {
|
|
73
|
+
cornerRadius = 999f
|
|
74
|
+
setColor(color)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override fun onDraw(canvas: Canvas) {
|
|
78
|
+
neutralProgress?.let { neutral ->
|
|
79
|
+
if (max > 0) {
|
|
80
|
+
val trackWidth = width - paddingLeft - paddingRight
|
|
81
|
+
val x = paddingLeft + trackWidth * (neutral.toFloat() / max)
|
|
82
|
+
val centerY = (height + paddingTop) / 2f
|
|
83
|
+
canvas.drawRect(x - density, centerY - 5 * density, x + density, centerY + 5 * density, tickPaint)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
super.onDraw(canvas)
|
|
87
|
+
val label = labelText ?: when (propertyKey) {
|
|
88
|
+
"fontSize" -> "Size"
|
|
89
|
+
"rotation" -> "Rotate"
|
|
90
|
+
else -> propertyKey.replaceFirstChar { it.uppercase() }
|
|
91
|
+
}
|
|
92
|
+
val value = valueFormatter?.invoke(progress) ?: when (propertyKey) {
|
|
93
|
+
"rotation" -> "${progress - 180}°"
|
|
94
|
+
"fontSize" -> "${progress + 12}"
|
|
95
|
+
"opacity" -> "$progress%"
|
|
96
|
+
else -> "${progress + 20}%"
|
|
97
|
+
}
|
|
98
|
+
labelPaint.textAlign = Paint.Align.LEFT
|
|
99
|
+
canvas.drawText(label, paddingLeft.toFloat(), 14 * density, labelPaint)
|
|
100
|
+
labelPaint.textAlign = Paint.Align.RIGHT
|
|
101
|
+
canvas.drawText(value, (width - paddingRight).toFloat(), 14 * density, labelPaint)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -10,6 +10,7 @@ import android.os.Looper
|
|
|
10
10
|
import androidx.media3.common.Effect
|
|
11
11
|
import androidx.media3.common.MediaItem
|
|
12
12
|
import androidx.media3.common.MimeTypes
|
|
13
|
+
import androidx.media3.common.OverlaySettings
|
|
13
14
|
import androidx.media3.common.util.Size
|
|
14
15
|
import androidx.media3.effect.BitmapOverlay
|
|
15
16
|
import androidx.media3.effect.Brightness
|
|
@@ -18,6 +19,7 @@ import androidx.media3.effect.HslAdjustment
|
|
|
18
19
|
import androidx.media3.effect.OverlayEffect
|
|
19
20
|
import androidx.media3.effect.Presentation
|
|
20
21
|
import androidx.media3.effect.ScaleAndRotateTransformation
|
|
22
|
+
import androidx.media3.effect.StaticOverlaySettings
|
|
21
23
|
import androidx.media3.effect.TextureOverlay
|
|
22
24
|
import androidx.media3.transformer.Composition
|
|
23
25
|
import androidx.media3.transformer.EditedMediaItem
|
|
@@ -28,6 +30,7 @@ import androidx.media3.transformer.ExportResult
|
|
|
28
30
|
import androidx.media3.transformer.ProgressHolder
|
|
29
31
|
import androidx.media3.transformer.Transformer
|
|
30
32
|
import com.photovideoeditor.files.SourceResolver
|
|
33
|
+
import com.photovideoeditor.photo.render.OverlayGeometry
|
|
31
34
|
import com.photovideoeditor.photo.render.PhotoLayer
|
|
32
35
|
import com.photovideoeditor.photo.render.PhotoLayerRenderer
|
|
33
36
|
import com.photovideoeditor.video.render.VideoClip
|
|
@@ -120,6 +123,8 @@ class VideoExporter(private val context: Context) {
|
|
|
120
123
|
effects.add(Presentation.createForAspectRatio(ratio, Presentation.LAYOUT_SCALE_TO_FIT_WITH_CROP))
|
|
121
124
|
}
|
|
122
125
|
|
|
126
|
+
effects.addAll(com.photovideoeditor.video.render.VideoCropEffects.create(state.crop))
|
|
127
|
+
|
|
123
128
|
// Video-level filters: export-only for now, no live preview — see docs/video-editor.md.
|
|
124
129
|
if (state.brightness != 0f) effects.add(Brightness(state.brightness / 100f))
|
|
125
130
|
if (state.contrast != 0f) effects.add(Contrast(state.contrast / 100f))
|
|
@@ -148,20 +153,37 @@ class VideoExporter(private val context: Context) {
|
|
|
148
153
|
object : BitmapOverlay() {
|
|
149
154
|
@Volatile private var outputWidth = sourceWidth.coerceAtLeast(2)
|
|
150
155
|
@Volatile private var outputHeight = sourceHeight.coerceAtLeast(2)
|
|
156
|
+
@Volatile private var settings: OverlaySettings = StaticOverlaySettings.Builder().build()
|
|
151
157
|
|
|
152
158
|
override fun configure(videoSize: Size) {
|
|
153
159
|
super.configure(videoSize)
|
|
154
|
-
// Media3
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
// Media3 composites an overlay texture **1:1 in pixels, centred** on the frame — it
|
|
161
|
+
// does NOT stretch it to fill (that is what iOS's `contentsGravity = .resizeAspect`
|
|
162
|
+
// does, which is why only Android was affected). Verified against the 1.8.0 bytecode
|
|
163
|
+
// of `OverlayMatrixProvider.getTransformationMatrix`: with default anchors/rotation
|
|
164
|
+
// its matrix chain collapses to
|
|
165
|
+
// net scale = (overlaySize / frameSize) * OverlaySettings.scale
|
|
166
|
+
// (the inner scale/scaleInv pair only exists to apply the anchor offset in unscaled
|
|
167
|
+
// space; the chain multiplies by `scaleMatrix` once more at the end).
|
|
168
|
+
//
|
|
169
|
+
// `PhotoLayerRenderer` draws in coordinates normalized to the bitmap it is handed,
|
|
170
|
+
// so an overlay bitmap smaller than the frame made every layer render *smaller and
|
|
171
|
+
// pulled toward the frame centre* by exactly the shortfall — e.g. a 4K export
|
|
172
|
+
// (3840px long edge) downscaled to a 1920px overlay put every sticker/text at half
|
|
173
|
+
// its preview size. Videos at or under the cap were unaffected, which is why this
|
|
174
|
+
// only reproduced on high-resolution sources.
|
|
175
|
+
//
|
|
176
|
+
// Fix: keep the memory cap, but tell Media3 to scale the overlay back up to the
|
|
177
|
+
// full frame. The compensation divides by the *rounded* bitmap size actually
|
|
178
|
+
// allocated (not the pre-rounding ratio) so rounding cannot reintroduce drift.
|
|
179
|
+
val plan = OverlayGeometry.canvasPlan(videoSize.width, videoSize.height, MAX_OVERLAY_EDGE_PX)
|
|
180
|
+
outputWidth = plan.width
|
|
181
|
+
outputHeight = plan.height
|
|
182
|
+
settings = StaticOverlaySettings.Builder().setScale(plan.scaleX, plan.scaleY).build()
|
|
163
183
|
}
|
|
164
184
|
|
|
185
|
+
override fun getOverlaySettings(presentationTimeUs: Long): OverlaySettings = settings
|
|
186
|
+
|
|
165
187
|
override fun getBitmap(presentationTimeUs: Long): Bitmap {
|
|
166
188
|
val positionMs = presentationTimeUs / 1000 + clipOffsetMs
|
|
167
189
|
val activeLayers = layers.filter { it.isActiveAt(positionMs, totalDurationMs) }
|
|
@@ -313,7 +335,12 @@ class VideoExporter(private val context: Context) {
|
|
|
313
335
|
}
|
|
314
336
|
|
|
315
337
|
private companion object {
|
|
316
|
-
/**
|
|
317
|
-
|
|
338
|
+
/**
|
|
339
|
+
* Overlay-compositing bitmap longest-edge cap — purely a memory guard (an ARGB_8888 bitmap this
|
|
340
|
+
* size is allocated twice transiently while rendering). Geometry no longer depends on it: any
|
|
341
|
+
* shortfall is scaled back to the full frame via `OverlaySettings` (see the `configure` comment),
|
|
342
|
+
* so the cap only trades overlay sharpness for peak memory on above-1440p exports.
|
|
343
|
+
*/
|
|
344
|
+
const val MAX_OVERLAY_EDGE_PX = 2560f
|
|
318
345
|
}
|
|
319
346
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package com.photovideoeditor.video.render
|
|
2
|
+
|
|
3
|
+
import android.graphics.Matrix
|
|
4
|
+
import androidx.media3.common.Effect
|
|
5
|
+
import androidx.media3.common.util.Size
|
|
6
|
+
import androidx.media3.effect.Crop
|
|
7
|
+
import androidx.media3.effect.MatrixTransformation
|
|
8
|
+
import androidx.media3.effect.ScaleAndRotateTransformation
|
|
9
|
+
import com.photovideoeditor.photo.render.CropGeometry
|
|
10
|
+
import com.photovideoeditor.photo.render.PhotoTransformState
|
|
11
|
+
import kotlin.math.cos
|
|
12
|
+
import kotlin.math.sin
|
|
13
|
+
|
|
14
|
+
/** Identical effect order for ExoPlayer preview and Transformer export. */
|
|
15
|
+
object VideoCropEffects {
|
|
16
|
+
fun create(state: PhotoTransformState, includeCrop: Boolean = true): List<Effect> = buildList {
|
|
17
|
+
if (state.rotationDegrees != 0) add(ScaleAndRotateTransformation.Builder().setRotationDegrees(-state.rotationDegrees.toFloat()).build())
|
|
18
|
+
if (state.straightenDegrees != 0f) add(object : MatrixTransformation {
|
|
19
|
+
private val matrix = Matrix()
|
|
20
|
+
override fun configure(inputWidth: Int, inputHeight: Int): Size {
|
|
21
|
+
val w = inputWidth.toFloat(); val h = inputHeight.toFloat()
|
|
22
|
+
val scale = CropGeometry.fillScale(w, h, state.straightenDegrees)
|
|
23
|
+
val radians = Math.toRadians(state.straightenDegrees.toDouble())
|
|
24
|
+
val c = cos(radians).toFloat() * scale; val s = sin(radians).toFloat() * scale
|
|
25
|
+
// GL's Y axis points up; UI rotation is clockwise with Y pointing down.
|
|
26
|
+
matrix.setValues(floatArrayOf(c, s * h / w, 0f, -s * w / h, c, 0f, 0f, 0f, 1f))
|
|
27
|
+
return Size(inputWidth, inputHeight)
|
|
28
|
+
}
|
|
29
|
+
override fun getMatrix(presentationTimeUs: Long) = matrix
|
|
30
|
+
})
|
|
31
|
+
if (includeCrop && (state.cropLeft != 0f || state.cropTop != 0f || state.cropRight != 1f || state.cropBottom != 1f)) {
|
|
32
|
+
add(Crop(state.cropLeft * 2 - 1, state.cropRight * 2 - 1, 1 - state.cropBottom * 2, 1 - state.cropTop * 2))
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -17,6 +17,10 @@ import java.io.File
|
|
|
17
17
|
* clip list again to produce the exported file.
|
|
18
18
|
*/
|
|
19
19
|
class VideoEditSession(context: Context, val sourceUri: String) {
|
|
20
|
+
var sourceWidth = 0
|
|
21
|
+
private set
|
|
22
|
+
var sourceHeight = 0
|
|
23
|
+
private set
|
|
20
24
|
val player: ExoPlayer = ExoPlayer.Builder(context).build()
|
|
21
25
|
|
|
22
26
|
/** Text/sticker/shape overlays. Reuses the photo layer model/stack — see `PhotoLayer.startMs`/`endMs` for timing. */
|
|
@@ -49,6 +53,12 @@ class VideoEditSession(context: Context, val sourceUri: String) {
|
|
|
49
53
|
|
|
50
54
|
init {
|
|
51
55
|
player.addListener(object : Player.Listener {
|
|
56
|
+
override fun onVideoSizeChanged(size: androidx.media3.common.VideoSize) {
|
|
57
|
+
if (sourceWidth == 0 && size.width > 0 && size.height > 0) {
|
|
58
|
+
sourceWidth = (size.width * size.pixelWidthHeightRatio).toInt()
|
|
59
|
+
sourceHeight = size.height
|
|
60
|
+
}
|
|
61
|
+
}
|
|
52
62
|
override fun onPlaybackStateChanged(playbackState: Int) {
|
|
53
63
|
if (playbackState == Player.STATE_READY && !initialTimelineCreated && player.duration > 0) {
|
|
54
64
|
initialTimelineCreated = true
|
|
@@ -9,6 +9,7 @@ package com.photovideoeditor.video.render
|
|
|
9
9
|
*/
|
|
10
10
|
data class VideoTransformState(
|
|
11
11
|
val rotationDegrees: Int = 0,
|
|
12
|
+
val crop: com.photovideoeditor.photo.render.PhotoTransformState = com.photovideoeditor.photo.render.PhotoTransformState(),
|
|
12
13
|
val aspectRatio: Float? = null,
|
|
13
14
|
val coverFrameMs: Long = 0L,
|
|
14
15
|
/** Preview-only for now: cycled via the Speed tool. Export does not yet honor this — see docs/video-editor.md. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp" android:height="24dp" android:viewportWidth="960" android:viewportHeight="960">
|
|
3
|
+
<group android:translateY="960">
|
|
4
|
+
<path android:fillColor="#FFFFFF" android:pathData="M160-120v-170l527-526q12-12 27-18t30-6q16 0 30.5 6t25.5 18l56 56q12 11 18 25.5t6 30.5q0 15-6 30t-18 27L330-120H160Zm80-80h56l393-392-28-29-29-28-392 393v56Zm320 80q74 0 137-37t63-103q0-36-19-62t-51-45l-59 59q23 10 36 22t13 26q0 23-36.5 41.5T560-200q-17 0-28.5 11.5T520-160q0 17 11.5 28.5T560-120ZM183-426l60-60q-20-8-31.5-16.5T200-520q0-12 18-24t76-37q88-38 117-69t29-70q0-55-44-87.5T280-840q-45 0-80.5 16T145-785q-11 13-9 29t15 26q13 11 29 9t27-13q14-14 31-20t42-6q41 0 60.5 12t19.5 28q0 14-17.5 25.5T262-654q-80 35-111 63.5T120-520q0 32 17 54.5t46 39.5Z"/>
|
|
5
|
+
</group>
|
|
6
|
+
</vector>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
|
2
|
+
android:width="24dp"
|
|
3
|
+
android:height="24dp"
|
|
4
|
+
android:viewportWidth="24"
|
|
5
|
+
android:viewportHeight="24">
|
|
6
|
+
<path android:fillColor="#FFFFFFFF" android:pathData="M6,19h4V5H6v14zM14,5v14h4V5h-4z" />
|
|
7
|
+
</vector>
|
package/docs/api.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
`openEditor(options)` validates defaults, invokes native code, and returns `Promise<EditorResult>`. In milestone one it returns the source URI/type and `cancelled: false`; it does not present UI or export. `cancelExport(jobId?)` currently returns `false`. `isAvailable()` synchronously checks native linkage. `addExportProgressListener(listener)` returns `{ remove() }`; no fake events are emitted.
|
|
4
4
|
|
|
5
|
-
Defaults enable all feature flags
|
|
5
|
+
Defaults enable all feature flags—including `text`, `stickers`, `overlays`, and `draw`—set quality to `high`, preserve metadata, and disable gallery saving. For photos, `trim` normalizes to `false`. Photo/video format mismatches and unknown enum values are rejected.
|
|
6
6
|
|
|
7
7
|
Dimensions use pixels, duration milliseconds, file size bytes, bitrate bits/second, frame rate frames/second, and progress ranges from 0 to 1. Public types and option-level JSDoc live in `src/types.ts`.
|
|
8
8
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Native editor UI
|
|
2
|
+
|
|
3
|
+
The Kotlin and UIKit editors reuse their existing layer stacks, normalized media coordinates, renderers, and export paths. Tool selection is transient UI state; opening a palette or slider does not mutate a layer. Slider gestures commit one history snapshot, and duplication copies all layer properties including timing and drawing data.
|
|
4
|
+
|
|
5
|
+
Both editors use contextual icon rails, an explicit Color palette for text, and custom multiline text entry. Unsupported font-family and alignment controls are omitted. Existing crop, adjustment, filtering, drawing, and layer actions remain available.
|
|
6
|
+
|
|
7
|
+
New stickers start at 30% of rendered media width. Their original image aspect ratio is retained using the existing `overlayAspectRatio` field. Existing layer scales retain their meaning. Bitmap stickers now render at their source aspect ratio instead of stretching into squares. Selection controls belong to a separate view and are not part of the bitmap/video compositor.
|
|
8
|
+
|
|
9
|
+
## Device validation
|
|
10
|
+
|
|
11
|
+
The following needs device/simulator verification, particularly on iOS where UIKit cannot be built on Linux:
|
|
12
|
+
|
|
13
|
+
- Open portrait, landscape, and square photos/videos; check complete media visibility and centered placement.
|
|
14
|
+
- Add multiline text with the keyboard open in portrait and landscape. Check Add, Edit, Cancel, focus, and keyboard dismissal.
|
|
15
|
+
- Change text color, size, rotation, and opacity; switch tools; check one-step undo/redo and retained position/timing.
|
|
16
|
+
- Add a non-square sticker, drag, pinch, rotate, release one finger, duplicate, and delete through the canvas handle.
|
|
17
|
+
- Tap outside to dismiss selection; check the main toolbar returns without extra panels.
|
|
18
|
+
- Seek/play/pause video; verify overlay visibility at its start/end times.
|
|
19
|
+
- Export at original resolution and compare overlay centers, dimensions, colors, opacity, rotation, and ordering with the preview. Selection chrome must be absent.
|
|
20
|
+
- Check small screens, large text settings, navigation insets, VoiceOver/TalkBack, and horizontal toolbar scrolling.
|
|
21
|
+
|
|
22
|
+
Android unit coverage includes portrait/landscape/square sticker sizing, preview/export proportional geometry, layer-edit state preservation, and undo/redo. These tests do not replace rendered export comparison or keyboard/gesture checks on devices.
|
package/docs/photo-editor.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Milestone 2 — transform and export
|
|
4
4
|
|
|
5
|
-
- **Crop** —
|
|
5
|
+
- **Crop** — dedicated ZLImageEditor-inspired crop mode with a dimmed mask, rule-of-thirds grid, four corner and four edge handles, movable crop frame, and aspect presets (Free, 1:1, 4:5, 3:4, 9:16, 16:9). **Cancel** restores the exact pre-crop state; **Done** commits the crop.
|
|
6
6
|
- **Rotate** — rotates 90° per tap. Resets the current crop (a rotated image has a different frame of reference for the crop rectangle).
|
|
7
7
|
- **Flip** — cycles none → horizontal → vertical → none.
|
|
8
8
|
- **Straighten** — a fine -45°...45° slider, shown while crop mode is active.
|
|
@@ -18,13 +18,13 @@ Tap **Filters** to enter adjustment mode: a horizontal chip row selects which co
|
|
|
18
18
|
- **Reset** (filters sub-bar) — restores all adjustments to their zero/no-op defaults, independent of crop/transform/layers.
|
|
19
19
|
- Same pipeline and formulas applied in preview and export on both platforms.
|
|
20
20
|
|
|
21
|
-
## Milestone 4 — layers: text, stickers,
|
|
21
|
+
## Milestone 4 — layers: text, stickers, images, drawing
|
|
22
22
|
|
|
23
23
|
- **Text** — tap **Text**, type a string in the dialog, and it's added as a centered, draggable text layer.
|
|
24
|
-
- **Stickers** — tap **Stickers** for
|
|
24
|
+
- **Stickers** — tap **Stickers** for the searchable, categorized sticker collection bundled with the package. No third-party sticker API or network connection is used.
|
|
25
25
|
- **Draw** — tap **Draw** for a freehand canvas: pick a color and thin/thick width, draw multiple strokes, undo the last stroke or clear all, then **Done** commits every stroke as its own layer (so strokes with different colors/widths keep their own settings).
|
|
26
26
|
- **Selection** — tap any layer on the canvas to select it (shown with a dashed bounding box) and open its mini toolbar.
|
|
27
|
-
- **Transform** —
|
|
27
|
+
- **Transform** — drag layers to move them, pinch with two fingers to scale and rotate, or use the corner handle. Scale/Rotate/Opacity sliders remain available for precise changes.
|
|
28
28
|
- **Duplicate / Lock / Hide / Front / Back / Delete** — per-layer actions in the mini toolbar.
|
|
29
29
|
- **Undo/redo** — header buttons, backed by a snapshot history (50 entries). One entry is recorded per *committed* operation — add/delete/duplicate/reorder, or a drag/slider gesture's final value — never per intermediate touch-move frame.
|
|
30
30
|
- Layers are burned into the image identically in preview and at export, positioned in the same full-frame coordinate space as the crop overlay (so cropping after adding layers can crop them out, matching what the crop preview would suggest).
|
|
@@ -32,8 +32,7 @@ Tap **Filters** to enter adjustment mode: a horizontal chip row selects which co
|
|
|
32
32
|
### Known gaps in Milestone 4
|
|
33
33
|
|
|
34
34
|
- **Font registration is not wired to custom font files yet.** `EditorOptions.fonts` is accepted structurally (passed through to the native layer) but text layers currently only resolve `fontFamily` against system-installed font names (Android `Typeface.create`, iOS `UIFont(name:)`) — a consumer-provided font *file* is not loaded. This is an honest scope cut, not a silent gap: closing it means adding a font-file loader (`Typeface.createFromFile` / `CTFontManagerRegisterFontsForURL`) on each platform, planned as a small follow-up.
|
|
35
|
-
-
|
|
36
|
-
- No sealed per-type layer model — one flat `PhotoLayer` struct/data class covers text/sticker/shape/drawing, a deliberate simplification.
|
|
35
|
+
- No sealed per-type layer model — one flat `PhotoLayer` struct/data class covers text/sticker/image/drawing, a deliberate simplification.
|
|
37
36
|
- Text styling is limited to color and font size for now (no stroke/background/shadow/alignment controls yet, though the fields exist on `PhotoLayer` for a later pass).
|
|
38
37
|
|
|
39
38
|
Export applies the combined transform + adjustments + layers to the full-resolution source (never mutated) and writes JPEG or PNG (WebP on Android only; iOS falls back to JPEG). Output width/height/fileSize/mimeType are returned on the `EditorResult`.
|