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.
Files changed (233) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/PhotoVideoEditor.podspec +1 -0
  3. package/README.md +10 -6
  4. package/THIRD_PARTY_NOTICES.md +20 -0
  5. package/android/build.gradle +1 -1
  6. package/android/src/main/AndroidManifest.xml +1 -1
  7. package/android/src/main/assets/Stickers/AUTUMN_1.png +0 -0
  8. package/android/src/main/assets/Stickers/AUTUMN_10.png +0 -0
  9. package/android/src/main/assets/Stickers/AUTUMN_11.png +0 -0
  10. package/android/src/main/assets/Stickers/AUTUMN_12.png +0 -0
  11. package/android/src/main/assets/Stickers/AUTUMN_13.png +0 -0
  12. package/android/src/main/assets/Stickers/AUTUMN_14.png +0 -0
  13. package/android/src/main/assets/Stickers/AUTUMN_15.png +0 -0
  14. package/android/src/main/assets/Stickers/AUTUMN_16.png +0 -0
  15. package/android/src/main/assets/Stickers/AUTUMN_17.png +0 -0
  16. package/android/src/main/assets/Stickers/AUTUMN_18.png +0 -0
  17. package/android/src/main/assets/Stickers/AUTUMN_2.png +0 -0
  18. package/android/src/main/assets/Stickers/AUTUMN_3.png +0 -0
  19. package/android/src/main/assets/Stickers/AUTUMN_4.png +0 -0
  20. package/android/src/main/assets/Stickers/AUTUMN_5.png +0 -0
  21. package/android/src/main/assets/Stickers/AUTUMN_6.png +0 -0
  22. package/android/src/main/assets/Stickers/AUTUMN_7.png +0 -0
  23. package/android/src/main/assets/Stickers/AUTUMN_8.png +0 -0
  24. package/android/src/main/assets/Stickers/AUTUMN_9.png +0 -0
  25. package/android/src/main/assets/Stickers/LOL_1.png +0 -0
  26. package/android/src/main/assets/Stickers/LOL_10.png +0 -0
  27. package/android/src/main/assets/Stickers/LOL_11.png +0 -0
  28. package/android/src/main/assets/Stickers/LOL_12.png +0 -0
  29. package/android/src/main/assets/Stickers/LOL_13.png +0 -0
  30. package/android/src/main/assets/Stickers/LOL_14.png +0 -0
  31. package/android/src/main/assets/Stickers/LOL_15.png +0 -0
  32. package/android/src/main/assets/Stickers/LOL_16.png +0 -0
  33. package/android/src/main/assets/Stickers/LOL_17.png +0 -0
  34. package/android/src/main/assets/Stickers/LOL_18.png +0 -0
  35. package/android/src/main/assets/Stickers/LOL_19.png +0 -0
  36. package/android/src/main/assets/Stickers/LOL_2.png +0 -0
  37. package/android/src/main/assets/Stickers/LOL_3.png +0 -0
  38. package/android/src/main/assets/Stickers/LOL_4.png +0 -0
  39. package/android/src/main/assets/Stickers/LOL_5.png +0 -0
  40. package/android/src/main/assets/Stickers/LOL_6.png +0 -0
  41. package/android/src/main/assets/Stickers/LOL_7.png +0 -0
  42. package/android/src/main/assets/Stickers/LOL_8.png +0 -0
  43. package/android/src/main/assets/Stickers/LOL_9.png +0 -0
  44. package/android/src/main/assets/Stickers/SPRING_1.png +0 -0
  45. package/android/src/main/assets/Stickers/SPRING_2.png +0 -0
  46. package/android/src/main/assets/Stickers/SPRING_3.png +0 -0
  47. package/android/src/main/assets/Stickers/SPRING_4.png +0 -0
  48. package/android/src/main/assets/Stickers/SPRING_5.png +0 -0
  49. package/android/src/main/assets/Stickers/SPRING_6.png +0 -0
  50. package/android/src/main/assets/Stickers/SUMMER_1.png +0 -0
  51. package/android/src/main/assets/Stickers/SUMMER_10.png +0 -0
  52. package/android/src/main/assets/Stickers/SUMMER_11.png +0 -0
  53. package/android/src/main/assets/Stickers/SUMMER_12.png +0 -0
  54. package/android/src/main/assets/Stickers/SUMMER_13.png +0 -0
  55. package/android/src/main/assets/Stickers/SUMMER_14.png +0 -0
  56. package/android/src/main/assets/Stickers/SUMMER_15.png +0 -0
  57. package/android/src/main/assets/Stickers/SUMMER_16.png +0 -0
  58. package/android/src/main/assets/Stickers/SUMMER_17.png +0 -0
  59. package/android/src/main/assets/Stickers/SUMMER_18.png +0 -0
  60. package/android/src/main/assets/Stickers/SUMMER_19.png +0 -0
  61. package/android/src/main/assets/Stickers/SUMMER_2.png +0 -0
  62. package/android/src/main/assets/Stickers/SUMMER_20.png +0 -0
  63. package/android/src/main/assets/Stickers/SUMMER_21.png +0 -0
  64. package/android/src/main/assets/Stickers/SUMMER_3.png +0 -0
  65. package/android/src/main/assets/Stickers/SUMMER_4.png +0 -0
  66. package/android/src/main/assets/Stickers/SUMMER_5.png +0 -0
  67. package/android/src/main/assets/Stickers/SUMMER_6.png +0 -0
  68. package/android/src/main/assets/Stickers/SUMMER_7.png +0 -0
  69. package/android/src/main/assets/Stickers/SUMMER_8.png +0 -0
  70. package/android/src/main/assets/Stickers/SUMMER_9.png +0 -0
  71. package/android/src/main/assets/Stickers/TEXT_1.png +0 -0
  72. package/android/src/main/assets/Stickers/TEXT_12.png +0 -0
  73. package/android/src/main/assets/Stickers/TEXT_13.png +0 -0
  74. package/android/src/main/assets/Stickers/TEXT_14.png +0 -0
  75. package/android/src/main/assets/Stickers/TEXT_15.png +0 -0
  76. package/android/src/main/assets/Stickers/TEXT_16.png +0 -0
  77. package/android/src/main/assets/Stickers/TEXT_2.png +0 -0
  78. package/android/src/main/assets/Stickers/TEXT_25.png +0 -0
  79. package/android/src/main/assets/Stickers/TEXT_26.png +0 -0
  80. package/android/src/main/assets/Stickers/TEXT_27.png +0 -0
  81. package/android/src/main/assets/Stickers/TEXT_28.png +0 -0
  82. package/android/src/main/assets/Stickers/TEXT_29.png +0 -0
  83. package/android/src/main/assets/Stickers/TEXT_3.png +0 -0
  84. package/android/src/main/assets/Stickers/TEXT_30.png +0 -0
  85. package/android/src/main/assets/Stickers/TEXT_31.png +0 -0
  86. package/android/src/main/assets/Stickers/TEXT_32.png +0 -0
  87. package/android/src/main/assets/Stickers/TEXT_7.png +0 -0
  88. package/android/src/main/assets/Stickers/TEXT_8.png +0 -0
  89. package/android/src/main/assets/Stickers/TEXT_9.png +0 -0
  90. package/android/src/main/java/com/photovideoeditor/editor/PhotoVideoEditorActivity.kt +901 -651
  91. package/android/src/main/java/com/photovideoeditor/photo/export/PhotoExporter.kt +8 -23
  92. package/android/src/main/java/com/photovideoeditor/photo/render/CropGeometry.kt +71 -0
  93. package/android/src/main/java/com/photovideoeditor/photo/render/OverlayGeometry.kt +36 -0
  94. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoEditSession.kt +5 -7
  95. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoFilterPresets.kt +71 -41
  96. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayer.kt +5 -1
  97. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoLayerRenderer.kt +24 -1
  98. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformRenderer.kt +33 -0
  99. package/android/src/main/java/com/photovideoeditor/photo/render/PhotoTransformState.kt +6 -1
  100. package/android/src/main/java/com/photovideoeditor/photo/ui/CropOverlayView.kt +112 -122
  101. package/android/src/main/java/com/photovideoeditor/photo/ui/DrawOverlayView.kt +66 -0
  102. package/android/src/main/java/com/photovideoeditor/photo/ui/LayerOverlayView.kt +63 -14
  103. package/android/src/main/java/com/photovideoeditor/photo/ui/ZoomableImageView.kt +10 -0
  104. package/android/src/main/java/com/photovideoeditor/stickers/OnlineStickerSheet.kt +135 -104
  105. package/android/src/main/java/com/photovideoeditor/ui/components/CropPanel.kt +122 -0
  106. package/android/src/main/java/com/photovideoeditor/ui/components/EditorPanel.kt +250 -0
  107. package/android/src/main/java/com/photovideoeditor/ui/components/EditorToolSlider.kt +103 -0
  108. package/android/src/main/java/com/photovideoeditor/video/export/VideoExporter.kt +38 -11
  109. package/android/src/main/java/com/photovideoeditor/video/render/VideoCropEffects.kt +35 -0
  110. package/android/src/main/java/com/photovideoeditor/video/render/VideoEditSession.kt +10 -0
  111. package/android/src/main/java/com/photovideoeditor/video/render/VideoTransformState.kt +1 -0
  112. package/android/src/main/res/drawable/ic_draw.xml +6 -0
  113. package/android/src/main/res/drawable/ic_pause.xml +7 -0
  114. package/android/src/main/res/drawable/ic_play.xml +7 -0
  115. package/docs/api.md +1 -1
  116. package/docs/editor-ui-validation.md +22 -0
  117. package/docs/photo-editor.md +5 -6
  118. package/ios/Editor/PhotoVideoEditorViewController.swift +918 -494
  119. package/ios/Editor/UI/CropPanel.swift +97 -0
  120. package/ios/Editor/UI/EditorPanel.swift +334 -0
  121. package/ios/Editor/UI/EditorToolSlider.swift +22 -0
  122. package/ios/Editor/UI/TextEditorSheet.swift +84 -0
  123. package/ios/Photo/Export/PhotoExporter.swift +3 -3
  124. package/ios/Photo/Rendering/CropGeometry.swift +48 -0
  125. package/ios/Photo/Rendering/PhotoEditSession.swift +21 -10
  126. package/ios/Photo/Rendering/PhotoFilterPresets.swift +53 -23
  127. package/ios/Photo/Rendering/PhotoLayer.swift +14 -2
  128. package/ios/Photo/Rendering/PhotoLayerRenderer.swift +16 -1
  129. package/ios/Photo/Rendering/PhotoTransformState.swift +5 -0
  130. package/ios/Photo/UI/CropOverlayView.swift +98 -134
  131. package/ios/Photo/UI/DrawOverlayView.swift +48 -0
  132. package/ios/Photo/UI/FilterThumbnailLoader.swift +62 -0
  133. package/ios/Photo/UI/LayerOverlayView.swift +36 -11
  134. package/ios/Photo/UI/ZoomableImageView.swift +19 -1
  135. package/ios/Resources/Stickers/AUTUMN_1.png +0 -0
  136. package/ios/Resources/Stickers/AUTUMN_10.png +0 -0
  137. package/ios/Resources/Stickers/AUTUMN_11.png +0 -0
  138. package/ios/Resources/Stickers/AUTUMN_12.png +0 -0
  139. package/ios/Resources/Stickers/AUTUMN_13.png +0 -0
  140. package/ios/Resources/Stickers/AUTUMN_14.png +0 -0
  141. package/ios/Resources/Stickers/AUTUMN_15.png +0 -0
  142. package/ios/Resources/Stickers/AUTUMN_16.png +0 -0
  143. package/ios/Resources/Stickers/AUTUMN_17.png +0 -0
  144. package/ios/Resources/Stickers/AUTUMN_18.png +0 -0
  145. package/ios/Resources/Stickers/AUTUMN_2.png +0 -0
  146. package/ios/Resources/Stickers/AUTUMN_3.png +0 -0
  147. package/ios/Resources/Stickers/AUTUMN_4.png +0 -0
  148. package/ios/Resources/Stickers/AUTUMN_5.png +0 -0
  149. package/ios/Resources/Stickers/AUTUMN_6.png +0 -0
  150. package/ios/Resources/Stickers/AUTUMN_7.png +0 -0
  151. package/ios/Resources/Stickers/AUTUMN_8.png +0 -0
  152. package/ios/Resources/Stickers/AUTUMN_9.png +0 -0
  153. package/ios/Resources/Stickers/LOL_1.png +0 -0
  154. package/ios/Resources/Stickers/LOL_10.png +0 -0
  155. package/ios/Resources/Stickers/LOL_11.png +0 -0
  156. package/ios/Resources/Stickers/LOL_12.png +0 -0
  157. package/ios/Resources/Stickers/LOL_13.png +0 -0
  158. package/ios/Resources/Stickers/LOL_14.png +0 -0
  159. package/ios/Resources/Stickers/LOL_15.png +0 -0
  160. package/ios/Resources/Stickers/LOL_16.png +0 -0
  161. package/ios/Resources/Stickers/LOL_17.png +0 -0
  162. package/ios/Resources/Stickers/LOL_18.png +0 -0
  163. package/ios/Resources/Stickers/LOL_19.png +0 -0
  164. package/ios/Resources/Stickers/LOL_2.png +0 -0
  165. package/ios/Resources/Stickers/LOL_3.png +0 -0
  166. package/ios/Resources/Stickers/LOL_4.png +0 -0
  167. package/ios/Resources/Stickers/LOL_5.png +0 -0
  168. package/ios/Resources/Stickers/LOL_6.png +0 -0
  169. package/ios/Resources/Stickers/LOL_7.png +0 -0
  170. package/ios/Resources/Stickers/LOL_8.png +0 -0
  171. package/ios/Resources/Stickers/LOL_9.png +0 -0
  172. package/ios/Resources/Stickers/SPRING_1.png +0 -0
  173. package/ios/Resources/Stickers/SPRING_2.png +0 -0
  174. package/ios/Resources/Stickers/SPRING_3.png +0 -0
  175. package/ios/Resources/Stickers/SPRING_4.png +0 -0
  176. package/ios/Resources/Stickers/SPRING_5.png +0 -0
  177. package/ios/Resources/Stickers/SPRING_6.png +0 -0
  178. package/ios/Resources/Stickers/SUMMER_1.png +0 -0
  179. package/ios/Resources/Stickers/SUMMER_10.png +0 -0
  180. package/ios/Resources/Stickers/SUMMER_11.png +0 -0
  181. package/ios/Resources/Stickers/SUMMER_12.png +0 -0
  182. package/ios/Resources/Stickers/SUMMER_13.png +0 -0
  183. package/ios/Resources/Stickers/SUMMER_14.png +0 -0
  184. package/ios/Resources/Stickers/SUMMER_15.png +0 -0
  185. package/ios/Resources/Stickers/SUMMER_16.png +0 -0
  186. package/ios/Resources/Stickers/SUMMER_17.png +0 -0
  187. package/ios/Resources/Stickers/SUMMER_18.png +0 -0
  188. package/ios/Resources/Stickers/SUMMER_19.png +0 -0
  189. package/ios/Resources/Stickers/SUMMER_2.png +0 -0
  190. package/ios/Resources/Stickers/SUMMER_20.png +0 -0
  191. package/ios/Resources/Stickers/SUMMER_21.png +0 -0
  192. package/ios/Resources/Stickers/SUMMER_3.png +0 -0
  193. package/ios/Resources/Stickers/SUMMER_4.png +0 -0
  194. package/ios/Resources/Stickers/SUMMER_5.png +0 -0
  195. package/ios/Resources/Stickers/SUMMER_6.png +0 -0
  196. package/ios/Resources/Stickers/SUMMER_7.png +0 -0
  197. package/ios/Resources/Stickers/SUMMER_8.png +0 -0
  198. package/ios/Resources/Stickers/SUMMER_9.png +0 -0
  199. package/ios/Resources/Stickers/TEXT_1.png +0 -0
  200. package/ios/Resources/Stickers/TEXT_12.png +0 -0
  201. package/ios/Resources/Stickers/TEXT_13.png +0 -0
  202. package/ios/Resources/Stickers/TEXT_14.png +0 -0
  203. package/ios/Resources/Stickers/TEXT_15.png +0 -0
  204. package/ios/Resources/Stickers/TEXT_16.png +0 -0
  205. package/ios/Resources/Stickers/TEXT_2.png +0 -0
  206. package/ios/Resources/Stickers/TEXT_25.png +0 -0
  207. package/ios/Resources/Stickers/TEXT_26.png +0 -0
  208. package/ios/Resources/Stickers/TEXT_27.png +0 -0
  209. package/ios/Resources/Stickers/TEXT_28.png +0 -0
  210. package/ios/Resources/Stickers/TEXT_29.png +0 -0
  211. package/ios/Resources/Stickers/TEXT_3.png +0 -0
  212. package/ios/Resources/Stickers/TEXT_30.png +0 -0
  213. package/ios/Resources/Stickers/TEXT_31.png +0 -0
  214. package/ios/Resources/Stickers/TEXT_32.png +0 -0
  215. package/ios/Resources/Stickers/TEXT_7.png +0 -0
  216. package/ios/Resources/Stickers/TEXT_8.png +0 -0
  217. package/ios/Resources/Stickers/TEXT_9.png +0 -0
  218. package/ios/Resources/Stickers/a_pety_icon.png +0 -0
  219. package/ios/Stickers/OnlineStickerSheetViewController.swift +102 -159
  220. package/ios/Video/Export/VideoExporter.swift +10 -1
  221. package/ios/Video/Render/VideoCropGeometry.swift +48 -0
  222. package/ios/Video/Render/VideoTransformState.swift +1 -0
  223. package/lib/module/index.js +2 -2
  224. package/lib/module/index.js.map +1 -1
  225. package/lib/typescript/src/types.d.ts +2 -2
  226. package/lib/typescript/src/types.d.ts.map +1 -1
  227. package/package.json +2 -1
  228. package/src/index.ts +1 -1
  229. package/src/types.ts +2 -2
  230. package/android/src/main/java/com/photovideoeditor/stickers/OpenMojiCatalog.kt +0 -172
  231. package/android/src/main/java/com/photovideoeditor/stickers/RemoteImageLoader.kt +0 -110
  232. package/ios/Stickers/OpenMojiCatalog.swift +0 -188
  233. 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: ((left: Float, top: Float, right: Float, bottom: Float) -> Unit)? = null
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
- private val dimPaint = Paint().apply { color = Color.argb(153, 0, 0, 0) }
24
- private val borderPaint = Paint().apply { color = Color.WHITE; style = Paint.Style.STROKE; strokeWidth = 3f }
25
- private val gridPaint = Paint().apply { color = Color.argb(120, 255, 255, 255); style = Paint.Style.STROKE; strokeWidth = 1f }
26
- private val handlePaint = Paint().apply { color = Color.WHITE; style = Paint.Style.FILL }
27
-
28
- private val handleTouchRadius = 56f
29
- private val minSizePx = 96f
30
- private var activeHandle = Handle.NONE
31
- private var lastX = 0f
32
- private var lastY = 0f
33
-
34
- private enum class Handle { NONE, MOVE, TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT }
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
- imageBounds = RectF(bounds)
39
- if (resetCrop || cropRect.width() <= 0f || cropRect.height() <= 0f) {
40
- cropRect = RectF(imageBounds)
41
- reportCrop()
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
- if (ratio != null && imageBounds.width() > 0 && imageBounds.height() > 0) {
49
- val centerX = cropRect.centerX()
50
- val centerY = cropRect.centerY()
51
- var width = cropRect.width()
52
- var height = width / ratio
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
- super.onDraw(canvas)
66
- if (imageBounds.width() <= 0 || imageBounds.height() <= 0) return
67
- canvas.drawRect(imageBounds.left, imageBounds.top, imageBounds.right, cropRect.top, dimPaint)
68
- canvas.drawRect(imageBounds.left, cropRect.bottom, imageBounds.right, imageBounds.bottom, dimPaint)
69
- canvas.drawRect(imageBounds.left, cropRect.top, cropRect.left, cropRect.bottom, dimPaint)
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
- canvas.drawLine(cropRect.left + thirdWidth * i, cropRect.top, cropRect.left + thirdWidth * i, cropRect.bottom, gridPaint)
76
- canvas.drawLine(cropRect.left, cropRect.top + thirdHeight * i, cropRect.right, cropRect.top + thirdHeight * i, gridPaint)
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
- listOf(
79
- cropRect.left to cropRect.top,
80
- cropRect.right to cropRect.top,
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
- lastY = event.y
92
- return activeHandle != Handle.NONE
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
- if (activeHandle == Handle.NONE) return false
96
- val dx = event.x - lastX
97
- val dy = event.y - lastY
98
- lastX = event.x
99
- lastY = event.y
100
- applyDrag(activeHandle, dx, dy)
101
- invalidate()
102
- reportCrop()
103
- return true
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.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
106
- val handled = activeHandle != Handle.NONE
107
- activeHandle = Handle.NONE
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
- return super.onTouchEvent(event)
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
- cropRect = proposed
146
- clampToImageBounds()
128
+ return true
147
129
  }
148
130
 
149
- private fun clampToImageBounds() {
150
- if (cropRect.left < imageBounds.left) cropRect.offset(imageBounds.left - cropRect.left, 0f)
151
- if (cropRect.top < imageBounds.top) cropRect.offset(0f, imageBounds.top - cropRect.top)
152
- if (cropRect.right > imageBounds.right) cropRect.offset(imageBounds.right - cropRect.right, 0f)
153
- if (cropRect.bottom > imageBounds.bottom) cropRect.offset(0f, imageBounds.bottom - cropRect.bottom)
154
- cropRect.left = cropRect.left.coerceAtLeast(imageBounds.left)
155
- cropRect.top = cropRect.top.coerceAtLeast(imageBounds.top)
156
- cropRect.right = cropRect.right.coerceAtMost(imageBounds.right)
157
- cropRect.bottom = cropRect.bottom.coerceAtMost(imageBounds.bottom)
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 reportCrop() {
161
- if (imageBounds.width() <= 0 || imageBounds.height() <= 0) return
162
- val left = (cropRect.left - imageBounds.left) / imageBounds.width()
163
- val top = (cropRect.top - imageBounds.top) / imageBounds.height()
164
- val right = (cropRect.right - imageBounds.left) / imageBounds.width()
165
- val bottom = (cropRect.bottom - imageBounds.top) / imageBounds.height()
166
- onCropChanged?.invoke(left, top, right, bottom)
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 = 2f * density
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
- private val handleGlyphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
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
- canvas.drawCircle(half.first, half.second, handleRadius, handlePaint)
67
- val glyph = handleRadius * 0.52f
68
- canvas.drawLine(half.first - glyph, half.second + glyph, half.first + glyph, half.second - glyph, handleGlyphPaint)
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 -> { finishGesture(); return true }
131
- MotionEvent.ACTION_POINTER_UP -> return true
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 size = shortSide * 0.18f
162
- Pair(size / 2f, size / 2f)
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(handleRadius), (base.second * layer.scale).coerceAtLeast(handleRadius))
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())