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,162 +7,152 @@ import android.graphics.Paint
|
|
|
7
7
|
import android.graphics.RectF
|
|
8
8
|
import android.view.MotionEvent
|
|
9
9
|
import android.view.View
|
|
10
|
+
import com.photovideoeditor.photo.render.CropGeometry
|
|
11
|
+
import com.photovideoeditor.photo.render.PhotoTransformState
|
|
10
12
|
import kotlin.math.abs
|
|
13
|
+
import kotlin.math.hypot
|
|
11
14
|
|
|
12
|
-
/**
|
|
13
|
-
* Draws a draggable/resizable crop rectangle over [imageBounds] (the on-screen
|
|
14
|
-
* rect of the preview image, in this view's own coordinate space) and reports
|
|
15
|
-
* the crop edges back normalized to that rect via [onCropChanged].
|
|
16
|
-
*/
|
|
15
|
+
/** Crop handles own resize gestures; interior drags/pinches move the media behind a fixed box. */
|
|
17
16
|
class CropOverlayView(context: Context) : View(context) {
|
|
18
17
|
private var imageBounds = RectF()
|
|
18
|
+
private var fittedBounds = RectF()
|
|
19
19
|
private var cropRect = RectF()
|
|
20
20
|
private var aspectRatio: Float? = null
|
|
21
|
-
var onCropChanged: ((
|
|
21
|
+
var onCropChanged: ((Float, Float, Float, Float) -> Unit)? = null
|
|
22
|
+
var onMediaBoundsChanged: ((RectF) -> Unit)? = null
|
|
23
|
+
var onViewportChanged: ((Float, Float, Float) -> Unit)? = null
|
|
24
|
+
private val density = resources.displayMetrics.density
|
|
25
|
+
private val dimPaint = Paint().apply { color = Color.argb(165, 0, 0, 0) }
|
|
26
|
+
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE; style = Paint.Style.STROKE; strokeWidth = density }
|
|
27
|
+
private val gridPaint = Paint().apply { color = Color.argb(85, 255, 255, 255); strokeWidth = density * .5f }
|
|
28
|
+
private val handlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE; strokeWidth = 3 * density; strokeCap = Paint.Cap.ROUND; style = Paint.Style.STROKE }
|
|
29
|
+
private var activeHandle: CropGeometry.Handle? = null
|
|
30
|
+
private var lastX = 0f; private var lastY = 0f; private var lastDistance = 0f
|
|
31
|
+
private var movingMedia = false
|
|
22
32
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
fun restore(bounds: RectF, state: PhotoTransformState) {
|
|
34
|
+
if (bounds.width() <= 0 || bounds.height() <= 0) return
|
|
35
|
+
fittedBounds = RectF(bounds)
|
|
36
|
+
val zoom = state.zoom.coerceIn(1f, 8f)
|
|
37
|
+
val cx = bounds.centerX() + state.panX * bounds.width()
|
|
38
|
+
val cy = bounds.centerY() + state.panY * bounds.height()
|
|
39
|
+
imageBounds = RectF(cx - bounds.width() * zoom / 2, cy - bounds.height() * zoom / 2,
|
|
40
|
+
cx + bounds.width() * zoom / 2, cy + bounds.height() * zoom / 2)
|
|
41
|
+
aspectRatio = state.aspectRatio
|
|
42
|
+
setCrop(state.cropLeft, state.cropTop, state.cropRight, state.cropBottom)
|
|
43
|
+
onMediaBoundsChanged?.invoke(RectF(imageBounds))
|
|
44
|
+
invalidate()
|
|
45
|
+
}
|
|
35
46
|
|
|
36
|
-
/** Sets the on-screen image rect this overlay crops against. Resets the crop to full-frame when [resetCrop] is true. */
|
|
37
47
|
fun setImageBounds(bounds: RectF, resetCrop: Boolean) {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
48
|
+
val old = normalized()
|
|
49
|
+
imageBounds = RectF(bounds); fittedBounds = RectF(bounds)
|
|
50
|
+
if (resetCrop || cropRect.isEmpty) cropRect = RectF(bounds) else setCrop(old[0], old[1], old[2], old[3])
|
|
51
|
+
invalidate()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fun setCrop(left: Float, top: Float, right: Float, bottom: Float) {
|
|
55
|
+
cropRect = RectF(imageBounds.left + left * imageBounds.width(), imageBounds.top + top * imageBounds.height(),
|
|
56
|
+
imageBounds.left + right * imageBounds.width(), imageBounds.top + bottom * imageBounds.height())
|
|
43
57
|
invalidate()
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
fun setAspectRatio(ratio: Float?) {
|
|
47
61
|
aspectRatio = ratio
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (height > imageBounds.height()) {
|
|
54
|
-
height = cropRect.height()
|
|
55
|
-
width = height * ratio
|
|
56
|
-
}
|
|
57
|
-
cropRect = RectF(centerX - width / 2, centerY - height / 2, centerX + width / 2, centerY + height / 2)
|
|
58
|
-
clampToImageBounds()
|
|
59
|
-
invalidate()
|
|
60
|
-
}
|
|
61
|
-
reportCrop()
|
|
62
|
+
// A new preset starts with the largest centered rectangle in the visible media.
|
|
63
|
+
val available = RectF(imageBounds)
|
|
64
|
+
available.intersect(0f, 0f, width.toFloat(), height.toFloat())
|
|
65
|
+
cropRect = CropGeometry.fit(available.geometry(), ratio).native()
|
|
66
|
+
reportCrop(); invalidate()
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
override fun onDraw(canvas: Canvas) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
canvas.drawRect(
|
|
68
|
-
canvas.drawRect(
|
|
69
|
-
canvas.drawRect(
|
|
70
|
-
canvas.drawRect(cropRect.right, cropRect.top, imageBounds.right, cropRect.bottom, dimPaint)
|
|
70
|
+
if (cropRect.isEmpty) return
|
|
71
|
+
canvas.drawRect(0f, 0f, width.toFloat(), cropRect.top, dimPaint)
|
|
72
|
+
canvas.drawRect(0f, cropRect.bottom, width.toFloat(), height.toFloat(), dimPaint)
|
|
73
|
+
canvas.drawRect(0f, cropRect.top, cropRect.left, cropRect.bottom, dimPaint)
|
|
74
|
+
canvas.drawRect(cropRect.right, cropRect.top, width.toFloat(), cropRect.bottom, dimPaint)
|
|
71
75
|
canvas.drawRect(cropRect, borderPaint)
|
|
72
|
-
val thirdWidth = cropRect.width() / 3
|
|
73
|
-
val thirdHeight = cropRect.height() / 3
|
|
74
76
|
for (i in 1..2) {
|
|
75
|
-
|
|
76
|
-
canvas.drawLine(
|
|
77
|
+
val x = cropRect.left + cropRect.width() * i / 3; val y = cropRect.top + cropRect.height() * i / 3
|
|
78
|
+
canvas.drawLine(x, cropRect.top, x, cropRect.bottom, gridPaint)
|
|
79
|
+
canvas.drawLine(cropRect.left, y, cropRect.right, y, gridPaint)
|
|
80
|
+
}
|
|
81
|
+
val corner = 14 * density
|
|
82
|
+
for ((x, y) in listOf(cropRect.left to cropRect.top, cropRect.right to cropRect.top, cropRect.left to cropRect.bottom, cropRect.right to cropRect.bottom)) {
|
|
83
|
+
canvas.drawLine(x, y, x + if (x == cropRect.left) corner else -corner, y, handlePaint)
|
|
84
|
+
canvas.drawLine(x, y, x, y + if (y == cropRect.top) corner else -corner, handlePaint)
|
|
77
85
|
}
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
cropRect.left to cropRect.bottom,
|
|
82
|
-
cropRect.right to cropRect.bottom
|
|
83
|
-
).forEach { (x, y) -> canvas.drawCircle(x, y, 8f, handlePaint) }
|
|
86
|
+
val edge = 8 * density
|
|
87
|
+
for (y in listOf(cropRect.top, cropRect.bottom)) canvas.drawLine(cropRect.centerX() - edge, y, cropRect.centerX() + edge, y, handlePaint)
|
|
88
|
+
for (x in listOf(cropRect.left, cropRect.right)) canvas.drawLine(x, cropRect.centerY() - edge, x, cropRect.centerY() + edge, handlePaint)
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
92
|
+
if (imageBounds.isEmpty) return false
|
|
87
93
|
when (event.actionMasked) {
|
|
88
94
|
MotionEvent.ACTION_DOWN -> {
|
|
89
|
-
activeHandle = hitTest(event.x, event.y)
|
|
90
|
-
lastX = event.x
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
activeHandle = hitTest(event.x, event.y); movingMedia = activeHandle == null
|
|
96
|
+
lastX = event.x; lastY = event.y; lastDistance = 0f
|
|
97
|
+
parent?.requestDisallowInterceptTouchEvent(true)
|
|
98
|
+
}
|
|
99
|
+
MotionEvent.ACTION_POINTER_DOWN -> {
|
|
100
|
+
activeHandle = null; movingMedia = true
|
|
101
|
+
lastX = (event.getX(0) + event.getX(1)) / 2; lastY = (event.getY(0) + event.getY(1)) / 2
|
|
102
|
+
lastDistance = hypot(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1))
|
|
93
103
|
}
|
|
94
104
|
MotionEvent.ACTION_MOVE -> {
|
|
95
|
-
|
|
96
|
-
val
|
|
97
|
-
val
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
105
|
+
val multi = event.pointerCount > 1
|
|
106
|
+
val x = if (multi) (event.getX(0) + event.getX(1)) / 2 else event.x
|
|
107
|
+
val y = if (multi) (event.getY(0) + event.getY(1)) / 2 else event.y
|
|
108
|
+
val distance = if (multi) hypot(event.getX(0) - event.getX(1), event.getY(0) - event.getY(1)) else 0f
|
|
109
|
+
val handle = activeHandle
|
|
110
|
+
if (handle != null) {
|
|
111
|
+
val valid = RectF(imageBounds).apply { intersect(0f, 0f, width.toFloat(), height.toFloat()) }
|
|
112
|
+
cropRect = CropGeometry.resize(cropRect.geometry(), valid.geometry(), handle, x - lastX, y - lastY, aspectRatio, 48 * density).native()
|
|
113
|
+
} else if (movingMedia) {
|
|
114
|
+
moveMedia(x - lastX, y - lastY, if (lastDistance > 0 && distance > 0) distance / lastDistance else 1f, x, y)
|
|
115
|
+
}
|
|
116
|
+
lastX = x; lastY = y; lastDistance = distance
|
|
117
|
+
reportCrop(); invalidate()
|
|
104
118
|
}
|
|
105
|
-
MotionEvent.
|
|
106
|
-
val
|
|
107
|
-
|
|
108
|
-
return handled
|
|
119
|
+
MotionEvent.ACTION_POINTER_UP -> {
|
|
120
|
+
val index = if (event.actionIndex == 0) 1 else 0
|
|
121
|
+
lastX = event.getX(index); lastY = event.getY(index); lastDistance = 0f
|
|
109
122
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
private fun hitTest(x: Float, y: Float): Handle {
|
|
115
|
-
fun near(px: Float, py: Float) = abs(x - px) < handleTouchRadius && abs(y - py) < handleTouchRadius
|
|
116
|
-
return when {
|
|
117
|
-
near(cropRect.left, cropRect.top) -> Handle.TOP_LEFT
|
|
118
|
-
near(cropRect.right, cropRect.top) -> Handle.TOP_RIGHT
|
|
119
|
-
near(cropRect.left, cropRect.bottom) -> Handle.BOTTOM_LEFT
|
|
120
|
-
near(cropRect.right, cropRect.bottom) -> Handle.BOTTOM_RIGHT
|
|
121
|
-
cropRect.contains(x, y) -> Handle.MOVE
|
|
122
|
-
else -> Handle.NONE
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private fun applyDrag(handle: Handle, dx: Float, dy: Float) {
|
|
127
|
-
val proposed = RectF(cropRect)
|
|
128
|
-
when (handle) {
|
|
129
|
-
Handle.MOVE -> proposed.offset(dx, dy)
|
|
130
|
-
Handle.TOP_LEFT -> { proposed.left += dx; proposed.top += dy }
|
|
131
|
-
Handle.TOP_RIGHT -> { proposed.right += dx; proposed.top += dy }
|
|
132
|
-
Handle.BOTTOM_LEFT -> { proposed.left += dx; proposed.bottom += dy }
|
|
133
|
-
Handle.BOTTOM_RIGHT -> { proposed.right += dx; proposed.bottom += dy }
|
|
134
|
-
Handle.NONE -> return
|
|
135
|
-
}
|
|
136
|
-
if (proposed.width() < minSizePx || proposed.height() < minSizePx) return
|
|
137
|
-
aspectRatio?.let { ratio ->
|
|
138
|
-
when (handle) {
|
|
139
|
-
Handle.TOP_LEFT, Handle.BOTTOM_RIGHT -> proposed.bottom = proposed.top + proposed.width() / ratio
|
|
140
|
-
Handle.TOP_RIGHT -> proposed.top = proposed.bottom - proposed.width() / ratio
|
|
141
|
-
Handle.BOTTOM_LEFT -> proposed.bottom = proposed.top + proposed.width() / ratio
|
|
142
|
-
else -> {}
|
|
123
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
124
|
+
activeHandle = null; movingMedia = false; lastDistance = 0f
|
|
125
|
+
parent?.requestDisallowInterceptTouchEvent(false)
|
|
143
126
|
}
|
|
144
127
|
}
|
|
145
|
-
|
|
146
|
-
clampToImageBounds()
|
|
128
|
+
return true
|
|
147
129
|
}
|
|
148
130
|
|
|
149
|
-
private fun
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
131
|
+
private fun moveMedia(dx: Float, dy: Float, factor: Float, focusX: Float, focusY: Float) {
|
|
132
|
+
val minimumZoom = maxOf(1f, cropRect.width() / fittedBounds.width(), cropRect.height() / fittedBounds.height())
|
|
133
|
+
val zoom = imageBounds.width() / fittedBounds.width()
|
|
134
|
+
val scale = (zoom * factor).coerceIn(minimumZoom, maxOf(8f, minimumZoom)) / zoom
|
|
135
|
+
val w = imageBounds.width() * scale; val h = imageBounds.height() * scale
|
|
136
|
+
val left = (focusX + (imageBounds.left - focusX) * scale + dx).coerceIn(cropRect.right - w, cropRect.left)
|
|
137
|
+
val top = (focusY + (imageBounds.top - focusY) * scale + dy).coerceIn(cropRect.bottom - h, cropRect.top)
|
|
138
|
+
imageBounds = RectF(left, top, left + w, top + h)
|
|
139
|
+
onMediaBoundsChanged?.invoke(RectF(imageBounds))
|
|
140
|
+
onViewportChanged?.invoke(w / fittedBounds.width(), (imageBounds.centerX() - fittedBounds.centerX()) / fittedBounds.width(), (imageBounds.centerY() - fittedBounds.centerY()) / fittedBounds.height())
|
|
158
141
|
}
|
|
159
142
|
|
|
160
|
-
private fun
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
143
|
+
private fun hitTest(x: Float, y: Float): CropGeometry.Handle? {
|
|
144
|
+
val points = listOf(
|
|
145
|
+
Triple(cropRect.left, cropRect.top, CropGeometry.Handle.TOP_LEFT), Triple(cropRect.right, cropRect.top, CropGeometry.Handle.TOP_RIGHT),
|
|
146
|
+
Triple(cropRect.left, cropRect.bottom, CropGeometry.Handle.BOTTOM_LEFT), Triple(cropRect.right, cropRect.bottom, CropGeometry.Handle.BOTTOM_RIGHT),
|
|
147
|
+
Triple(cropRect.centerX(), cropRect.top, CropGeometry.Handle.TOP), Triple(cropRect.right, cropRect.centerY(), CropGeometry.Handle.RIGHT),
|
|
148
|
+
Triple(cropRect.centerX(), cropRect.bottom, CropGeometry.Handle.BOTTOM), Triple(cropRect.left, cropRect.centerY(), CropGeometry.Handle.LEFT))
|
|
149
|
+
val nearest = points.minByOrNull { hypot(x - it.first, y - it.second) } ?: return null
|
|
150
|
+
return nearest.third.takeIf { hypot(x - nearest.first, y - nearest.second) <= 24 * density }
|
|
167
151
|
}
|
|
152
|
+
private fun normalized(): FloatArray = if (imageBounds.isEmpty) floatArrayOf(0f, 0f, 1f, 1f) else floatArrayOf(
|
|
153
|
+
(cropRect.left - imageBounds.left) / imageBounds.width(), (cropRect.top - imageBounds.top) / imageBounds.height(),
|
|
154
|
+
(cropRect.right - imageBounds.left) / imageBounds.width(), (cropRect.bottom - imageBounds.top) / imageBounds.height())
|
|
155
|
+
private fun reportCrop() { val r = normalized(); onCropChanged?.invoke(r[0].coerceIn(0f, 1f), r[1].coerceIn(0f, 1f), r[2].coerceIn(0f, 1f), r[3].coerceIn(0f, 1f)) }
|
|
156
|
+
private fun RectF.geometry() = CropGeometry.Rect(left, top, right, bottom)
|
|
157
|
+
private fun CropGeometry.Rect.native() = RectF(left, top, right, bottom)
|
|
168
158
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
package com.photovideoeditor.photo.ui
|
|
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.Path
|
|
8
|
+
import android.graphics.RectF
|
|
9
|
+
import android.view.MotionEvent
|
|
10
|
+
import android.view.View
|
|
11
|
+
|
|
12
|
+
data class DrawStroke(val points: MutableList<Pair<Float, Float>>, val color: Int, val widthPx: Float)
|
|
13
|
+
|
|
14
|
+
/** Captures freehand strokes over the visible image while Draw mode is active. */
|
|
15
|
+
class DrawOverlayView(context: Context) : View(context) {
|
|
16
|
+
private var imageBounds = RectF()
|
|
17
|
+
var strokeColor: Int = Color.RED
|
|
18
|
+
var strokeWidthPx: Float = 12f
|
|
19
|
+
private val strokes = mutableListOf<DrawStroke>()
|
|
20
|
+
private var current: DrawStroke? = null
|
|
21
|
+
var onStrokesChanged: ((List<DrawStroke>) -> Unit)? = null
|
|
22
|
+
private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
23
|
+
style = Paint.Style.STROKE
|
|
24
|
+
strokeCap = Paint.Cap.ROUND
|
|
25
|
+
strokeJoin = Paint.Join.ROUND
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fun setImageBounds(bounds: RectF) { imageBounds = RectF(bounds) }
|
|
29
|
+
fun clearStrokes() { strokes.clear(); current = null; invalidate(); onStrokesChanged?.invoke(strokes) }
|
|
30
|
+
fun undoLastStroke() { if (strokes.isNotEmpty()) strokes.removeAt(strokes.lastIndex); invalidate(); onStrokesChanged?.invoke(strokes) }
|
|
31
|
+
val hasStrokes: Boolean get() = strokes.isNotEmpty()
|
|
32
|
+
|
|
33
|
+
fun normalizedStrokes(): List<Pair<List<Pair<Float, Float>>, DrawStroke>> {
|
|
34
|
+
if (imageBounds.width() <= 0 || imageBounds.height() <= 0) return emptyList()
|
|
35
|
+
return strokes.map { stroke ->
|
|
36
|
+
stroke.points.map { (x, y) ->
|
|
37
|
+
(x - imageBounds.left) / imageBounds.width() to (y - imageBounds.top) / imageBounds.height()
|
|
38
|
+
} to stroke
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
override fun onDraw(canvas: Canvas) {
|
|
43
|
+
strokes.forEach { stroke ->
|
|
44
|
+
if (stroke.points.size < 2) return@forEach
|
|
45
|
+
paint.color = stroke.color; paint.strokeWidth = stroke.widthPx
|
|
46
|
+
val path = Path()
|
|
47
|
+
stroke.points.forEachIndexed { index, point -> if (index == 0) path.moveTo(point.first, point.second) else path.lineTo(point.first, point.second) }
|
|
48
|
+
canvas.drawPath(path, paint)
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
53
|
+
if (!imageBounds.contains(event.x, event.y) && event.actionMasked == MotionEvent.ACTION_DOWN) return false
|
|
54
|
+
when (event.actionMasked) {
|
|
55
|
+
MotionEvent.ACTION_DOWN -> {
|
|
56
|
+
current = DrawStroke(mutableListOf(event.x to event.y), strokeColor, strokeWidthPx).also(strokes::add)
|
|
57
|
+
parent?.requestDisallowInterceptTouchEvent(true); invalidate(); return true
|
|
58
|
+
}
|
|
59
|
+
MotionEvent.ACTION_MOVE -> { current?.points?.add(event.x to event.y); invalidate(); return true }
|
|
60
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
61
|
+
current = null; parent?.requestDisallowInterceptTouchEvent(false); onStrokesChanged?.invoke(strokes); return true
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return false
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -3,12 +3,13 @@ package com.photovideoeditor.photo.ui
|
|
|
3
3
|
import android.content.Context
|
|
4
4
|
import android.graphics.Canvas
|
|
5
5
|
import android.graphics.Color
|
|
6
|
-
import android.graphics.DashPathEffect
|
|
7
6
|
import android.graphics.Paint
|
|
8
7
|
import android.graphics.RectF
|
|
9
8
|
import android.view.MotionEvent
|
|
10
9
|
import android.view.View
|
|
11
10
|
import android.text.TextPaint
|
|
11
|
+
import com.photovideoeditor.R
|
|
12
|
+
import com.photovideoeditor.photo.render.OverlayGeometry
|
|
12
13
|
import com.photovideoeditor.photo.render.PhotoLayer
|
|
13
14
|
import com.photovideoeditor.photo.render.LayerType
|
|
14
15
|
import kotlin.math.atan2
|
|
@@ -27,7 +28,9 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
27
28
|
set(value) { field = value; invalidate() }
|
|
28
29
|
var selectedLayerId: String? = null
|
|
29
30
|
set(value) { field = value; invalidate() }
|
|
31
|
+
var onLayerDelete: ((String) -> Unit)? = null
|
|
30
32
|
var onLayerTapped: ((String?) -> Unit)? = null
|
|
33
|
+
var onLayerDoubleTapped: ((String) -> Unit)? = null
|
|
31
34
|
var onLayerTransformChanged: ((id: String, x: Float, y: Float, scale: Float, rotationDegrees: Float) -> Unit)? = null
|
|
32
35
|
var onLayerTransformEnded: ((id: String) -> Unit)? = null
|
|
33
36
|
|
|
@@ -40,17 +43,17 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
40
43
|
private var startAngle = 0f
|
|
41
44
|
private var startMidX = 0f
|
|
42
45
|
private var startMidY = 0f
|
|
46
|
+
private var movedDuringGesture = false
|
|
47
|
+
private var lastTapTimeMs = 0L
|
|
48
|
+
private var lastTapLayerId: String? = null
|
|
43
49
|
private val density = resources.displayMetrics.density
|
|
44
50
|
private val handleRadius = 11f * density
|
|
45
51
|
private val handleTouchRadius = 30f * density
|
|
46
52
|
private val selectionPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
47
|
-
color = Color.WHITE; style = Paint.Style.STROKE; strokeWidth =
|
|
48
|
-
pathEffect = DashPathEffect(floatArrayOf(8f * density, 5f * density), 0f)
|
|
53
|
+
color = Color.WHITE; style = Paint.Style.STROKE; strokeWidth = 1f * density
|
|
49
54
|
}
|
|
50
55
|
private val handlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { color = Color.WHITE }
|
|
51
|
-
|
|
52
|
-
color = Color.rgb(87, 55, 245); style = Paint.Style.STROKE; strokeWidth = 2f * density
|
|
53
|
-
}
|
|
56
|
+
|
|
54
57
|
|
|
55
58
|
fun setImageBounds(bounds: RectF) { imageBounds = RectF(bounds); invalidate() }
|
|
56
59
|
|
|
@@ -63,22 +66,42 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
63
66
|
canvas.translate(center.first, center.second)
|
|
64
67
|
canvas.rotate(selected.rotationDegrees)
|
|
65
68
|
canvas.drawRect(-half.first, -half.second, half.first, half.second, selectionPaint)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
+
for ((x, y) in listOf(Pair(-half.first, -half.second), Pair(half.first, -half.second), Pair(-half.first, half.second), Pair(half.first, half.second))) {
|
|
70
|
+
canvas.drawCircle(x, y, 4f * density, handlePaint)
|
|
71
|
+
}
|
|
72
|
+
drawControl(canvas, -half.first, -half.second, R.drawable.ic_close, Color.rgb(255, 100, 115))
|
|
73
|
+
drawControl(canvas, half.first, half.second, R.drawable.ic_fullscreen, Color.rgb(167, 139, 250))
|
|
69
74
|
canvas.restore()
|
|
70
75
|
}
|
|
71
76
|
|
|
77
|
+
private fun drawControl(canvas: Canvas, x: Float, y: Float, icon: Int, tint: Int) {
|
|
78
|
+
canvas.drawCircle(x, y, handleRadius, handlePaint)
|
|
79
|
+
context.getDrawable(icon)?.mutate()?.let { drawable ->
|
|
80
|
+
drawable.setTint(tint)
|
|
81
|
+
val size = (handleRadius * 0.7f).toInt()
|
|
82
|
+
drawable.setBounds(x.toInt() - size, y.toInt() - size, x.toInt() + size, y.toInt() + size)
|
|
83
|
+
drawable.draw(canvas)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
72
87
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
73
88
|
if (imageBounds.width() <= 0 || imageBounds.height() <= 0) return false
|
|
74
89
|
when (event.actionMasked) {
|
|
75
90
|
MotionEvent.ACTION_DOWN -> {
|
|
76
91
|
val selected = selectedLayer()
|
|
92
|
+
if (selected != null && !selected.locked) {
|
|
93
|
+
val handle = handlePoint(selected)
|
|
94
|
+
val center = centerOf(selected)
|
|
95
|
+
if (hypot(event.x - (2 * center.first - handle.first), event.y - (2 * center.second - handle.second)) <= handleTouchRadius) {
|
|
96
|
+
onLayerDelete?.invoke(selected.id); resetGesture(); return true
|
|
97
|
+
}
|
|
98
|
+
}
|
|
77
99
|
val handleHit = selected?.takeIf { !it.locked && isHandleHit(it, event.x, event.y) }
|
|
78
100
|
val hit = handleHit ?: hitTest(event.x, event.y)
|
|
79
101
|
onLayerTapped?.invoke(hit?.id)
|
|
80
102
|
if (hit == null || hit.locked) { resetGesture(); return false }
|
|
81
103
|
gestureId = hit.id
|
|
104
|
+
movedDuringGesture = false
|
|
82
105
|
startLayer = hit.copy()
|
|
83
106
|
startPointX = event.x; startPointY = event.y
|
|
84
107
|
if (handleHit != null) {
|
|
@@ -97,12 +120,14 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
97
120
|
val metrics = multiMetrics(event)
|
|
98
121
|
startDistance = metrics.distance.coerceAtLeast(1f); startAngle = metrics.angle
|
|
99
122
|
startMidX = metrics.midX; startMidY = metrics.midY
|
|
123
|
+
movedDuringGesture = true
|
|
100
124
|
gesture = Gesture.MULTI
|
|
101
125
|
return true
|
|
102
126
|
}
|
|
103
127
|
}
|
|
104
128
|
MotionEvent.ACTION_MOVE -> {
|
|
105
129
|
val initial = startLayer ?: return false
|
|
130
|
+
if (hypot(event.x - startPointX, event.y - startPointY) > handleRadius) movedDuringGesture = true
|
|
106
131
|
when (gesture) {
|
|
107
132
|
Gesture.DRAG -> {
|
|
108
133
|
val x = (initial.x + (event.x - startPointX) / imageBounds.width()).coerceIn(0f, 1f)
|
|
@@ -127,8 +152,27 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
127
152
|
}
|
|
128
153
|
return true
|
|
129
154
|
}
|
|
130
|
-
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
131
|
-
|
|
155
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
156
|
+
val tappedId = gestureId
|
|
157
|
+
if (event.actionMasked == MotionEvent.ACTION_UP && !movedDuringGesture && tappedId != null) {
|
|
158
|
+
val now = android.os.SystemClock.uptimeMillis()
|
|
159
|
+
if (lastTapLayerId == tappedId && now - lastTapTimeMs <= 350L) {
|
|
160
|
+
onLayerDoubleTapped?.invoke(tappedId); lastTapLayerId = null; lastTapTimeMs = 0L
|
|
161
|
+
} else { lastTapLayerId = tappedId; lastTapTimeMs = now }
|
|
162
|
+
}
|
|
163
|
+
finishGesture(); return true
|
|
164
|
+
}
|
|
165
|
+
MotionEvent.ACTION_POINTER_UP -> {
|
|
166
|
+
// Rebase to the remaining finger so lifting a pinch finger does not jump/freeze the layer.
|
|
167
|
+
val remaining = if (event.actionIndex == 0) 1 else 0
|
|
168
|
+
if (remaining < event.pointerCount) {
|
|
169
|
+
startLayer = currentGestureLayer()?.copy()
|
|
170
|
+
startPointX = event.getX(remaining); startPointY = event.getY(remaining)
|
|
171
|
+
gesture = Gesture.DRAG
|
|
172
|
+
}
|
|
173
|
+
movedDuringGesture = true
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
132
176
|
}
|
|
133
177
|
return false
|
|
134
178
|
}
|
|
@@ -158,16 +202,21 @@ class LayerOverlayView(context: Context) : View(context) {
|
|
|
158
202
|
Pair((lines.maxOfOrNull { paint.measureText(it) } ?: paint.textSize) / 2f + 8f * density, paint.fontSpacing * lines.size / 2f + 6f * density)
|
|
159
203
|
}
|
|
160
204
|
LayerType.STICKER -> {
|
|
161
|
-
val
|
|
162
|
-
Pair(
|
|
205
|
+
val (width, height) = OverlayGeometry.stickerSize(shortSide, layer.overlayAspectRatio ?: 1f)
|
|
206
|
+
Pair(width / 2f + 4f * density, height / 2f + 4f * density)
|
|
163
207
|
}
|
|
164
208
|
LayerType.OVERLAY -> {
|
|
165
209
|
val aspect = layer.overlayAspectRatio ?: 1f
|
|
166
210
|
val baseSize = shortSide * 0.6f
|
|
167
211
|
if (aspect >= 1f) Pair(baseSize / 2f, (baseSize / aspect) / 2f) else Pair((baseSize * aspect) / 2f, baseSize / 2f)
|
|
168
212
|
}
|
|
213
|
+
LayerType.DRAWING -> {
|
|
214
|
+
val maxX = layer.drawPoints.maxOfOrNull { kotlin.math.abs(it.first) } ?: 0.08f
|
|
215
|
+
val maxY = layer.drawPoints.maxOfOrNull { kotlin.math.abs(it.second) } ?: 0.08f
|
|
216
|
+
Pair(maxX * imageBounds.width() + handleRadius, maxY * imageBounds.height() + handleRadius)
|
|
217
|
+
}
|
|
169
218
|
}
|
|
170
|
-
return Pair((base.first * layer.scale).coerceAtLeast(
|
|
219
|
+
return Pair((base.first * layer.scale).coerceAtLeast(24f * density), (base.second * layer.scale).coerceAtLeast(24f * density))
|
|
171
220
|
}
|
|
172
221
|
|
|
173
222
|
private fun handlePoint(layer: PhotoLayer): Pair<Float, Float> {
|
|
@@ -59,6 +59,16 @@ class ZoomableImageView(context: Context) : ImageView(context) {
|
|
|
59
59
|
onBoundsChanged?.invoke(currentImageBounds())
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
63
|
+
super.onSizeChanged(w, h, oldw, oldh)
|
|
64
|
+
if (w != oldw || h != oldh) resetToFit()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fun setRenderedBounds(bounds: RectF) {
|
|
68
|
+
val image = drawable ?: return
|
|
69
|
+
imageMatrix = Matrix().apply { setRectToRect(RectF(0f, 0f, image.intrinsicWidth.toFloat(), image.intrinsicHeight.toFloat()), bounds, Matrix.ScaleToFit.FILL) }
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
fun currentImageBounds(): RectF {
|
|
63
73
|
val bitmapDrawable = drawable ?: return RectF(0f, 0f, width.toFloat(), height.toFloat())
|
|
64
74
|
val rect = RectF(0f, 0f, bitmapDrawable.intrinsicWidth.toFloat(), bitmapDrawable.intrinsicHeight.toFloat())
|