react-native-video-trim 6.2.2 → 7.0.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/README.md +34 -9
- package/android/src/main/java/com/videotrim/BaseVideoTrimModule.kt +146 -119
- package/android/src/main/java/com/videotrim/enums/ErrorCode.kt +2 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.kt +6 -2
- package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +5 -1
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +99 -26
- package/android/src/main/java/com/videotrim/widgets/CropOverlayView.kt +293 -0
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +556 -28
- package/android/src/main/res/drawable/arrow_trianglehead_left_and_right_righttriangle_left_righttriangle_right.xml +19 -0
- package/android/src/main/res/drawable/arrow_uturn_backward.xml +15 -0
- package/android/src/main/res/drawable/arrow_uturn_forward.xml +15 -0
- package/android/src/main/res/drawable/crop.xml +15 -0
- package/android/src/main/res/drawable/rotate_left.xml +19 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +115 -37
- package/android/src/main/res/xml/file_paths.xml +1 -1
- package/ios/CropOverlayView.swift +285 -0
- package/ios/VideoTrim.mm +2 -4
- package/ios/VideoTrim.swift +198 -61
- package/ios/VideoTrimmer.swift +2 -4
- package/ios/VideoTrimmerViewController.swift +478 -56
- package/lib/module/NativeVideoTrim.js.map +1 -1
- package/lib/module/index.js +1 -2
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts +10 -4
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/NativeVideoTrim.ts +10 -4
- package/src/index.tsx +1 -2
|
@@ -2,12 +2,14 @@ package com.videotrim.utils
|
|
|
2
2
|
|
|
3
3
|
import android.annotation.SuppressLint
|
|
4
4
|
import android.graphics.Bitmap
|
|
5
|
+
import android.graphics.RectF
|
|
5
6
|
import android.media.MediaMetadataRetriever
|
|
6
7
|
import android.util.Log
|
|
7
8
|
import com.arthenica.ffmpegkit.FFmpegKit
|
|
8
9
|
import com.arthenica.ffmpegkit.FFmpegSession
|
|
9
10
|
import com.arthenica.ffmpegkit.ReturnCode
|
|
10
11
|
import com.facebook.react.bridge.Arguments
|
|
12
|
+
import com.facebook.react.bridge.UiThreadUtil
|
|
11
13
|
import com.videotrim.enums.ErrorCode
|
|
12
14
|
import com.videotrim.interfaces.VideoTrimListener
|
|
13
15
|
import iknow.android.utils.DeviceUtil
|
|
@@ -17,6 +19,7 @@ import iknow.android.utils.thread.BackgroundExecutor
|
|
|
17
19
|
import java.text.SimpleDateFormat
|
|
18
20
|
import java.util.Date
|
|
19
21
|
import java.util.TimeZone
|
|
22
|
+
import kotlin.math.roundToInt
|
|
20
23
|
|
|
21
24
|
object VideoTrimmerUtil {
|
|
22
25
|
|
|
@@ -41,8 +44,13 @@ object VideoTrimmerUtil {
|
|
|
41
44
|
videoDuration: Int,
|
|
42
45
|
startMs: Long,
|
|
43
46
|
endMs: Long,
|
|
44
|
-
|
|
45
|
-
|
|
47
|
+
userRotationCount: Int,
|
|
48
|
+
userIsFlipped: Boolean,
|
|
49
|
+
cropNormalized: RectF?,
|
|
50
|
+
videoWidth: Int,
|
|
51
|
+
videoHeight: Int,
|
|
52
|
+
videoBitrate: Long,
|
|
53
|
+
enablePreciseTrimming: Boolean,
|
|
46
54
|
callback: VideoTrimListener
|
|
47
55
|
): FFmpegSession {
|
|
48
56
|
val currentDate = Date()
|
|
@@ -57,18 +65,75 @@ object VideoTrimmerUtil {
|
|
|
57
65
|
cmds.add("-to")
|
|
58
66
|
cmds.add("${endMs}ms")
|
|
59
67
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
val hasUserTransform = userRotationCount != 0 || userIsFlipped
|
|
69
|
+
// Re-encode is required when: (1) user applied flip/rotate, (2) user cropped, or
|
|
70
|
+
// (3) enablePreciseTrimming is on. In all three cases, -c copy won't work because
|
|
71
|
+
// either we need video filters or we need frame-accurate cut points.
|
|
72
|
+
val needsReEncode = hasUserTransform || cropNormalized != null || enablePreciseTrimming
|
|
73
|
+
|
|
74
|
+
if (needsReEncode) {
|
|
75
|
+
val videoFilters = mutableListOf<String>()
|
|
76
|
+
|
|
77
|
+
when (userRotationCount) {
|
|
78
|
+
1 -> videoFilters.add("transpose=2")
|
|
79
|
+
2 -> { videoFilters.add("transpose=2"); videoFilters.add("transpose=2") }
|
|
80
|
+
3 -> videoFilters.add("transpose=1")
|
|
81
|
+
}
|
|
82
|
+
if (userIsFlipped) {
|
|
83
|
+
videoFilters.add("hflip")
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Convert normalized crop rect [0..1] to pixel coordinates in the post-rotation frame.
|
|
87
|
+
if (cropNormalized != null && videoWidth > 0 && videoHeight > 0) {
|
|
88
|
+
val postW: Int
|
|
89
|
+
val postH: Int
|
|
90
|
+
// After 90°/270° rotation the width and height are swapped.
|
|
91
|
+
if (userRotationCount % 2 != 0) {
|
|
92
|
+
postW = videoHeight; postH = videoWidth
|
|
93
|
+
} else {
|
|
94
|
+
postW = videoWidth; postH = videoHeight
|
|
95
|
+
}
|
|
96
|
+
val cx = (cropNormalized.left * postW).roundToInt()
|
|
97
|
+
val cy = (cropNormalized.top * postH).roundToInt()
|
|
98
|
+
var cw = (cropNormalized.width() * postW).roundToInt()
|
|
99
|
+
var ch = (cropNormalized.height() * postH).roundToInt()
|
|
100
|
+
// H.264 requires even dimensions; round down to nearest even number.
|
|
101
|
+
cw = cw and 1.inv()
|
|
102
|
+
ch = ch and 1.inv()
|
|
103
|
+
if (cw > 0 && ch > 0) {
|
|
104
|
+
videoFilters.add("crop=$cw:$ch:$cx:$cy")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
val filterString = videoFilters.joinToString(",")
|
|
109
|
+
// Preserve source quality by matching the original bitrate. Falls back to 10 Mbps.
|
|
110
|
+
val bitrateStr = if (videoBitrate > 0) "$videoBitrate" else "10M"
|
|
64
111
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
112
|
+
cmds.addAll(listOf("-i", inputFile))
|
|
113
|
+
// When enablePreciseTrimming is the only reason for re-encode (no transforms),
|
|
114
|
+
// videoFilters is empty — skip -vf entirely to avoid FFmpeg error on empty filter.
|
|
115
|
+
if (filterString.isNotEmpty()) {
|
|
116
|
+
cmds.addAll(listOf("-vf", filterString))
|
|
117
|
+
}
|
|
118
|
+
// h264_mediacodec: Android's hardware H.264 encoder — fast and energy-efficient.
|
|
119
|
+
// Note: Android FFmpegKit auto-rotates by default, so no -noautorotate is needed.
|
|
120
|
+
// The transpose filters above only handle user-initiated rotation, not source metadata.
|
|
121
|
+
cmds.addAll(listOf(
|
|
122
|
+
"-c:v", "h264_mediacodec",
|
|
123
|
+
"-b:v", bitrateStr,
|
|
124
|
+
"-c:a", "copy",
|
|
125
|
+
"-metadata", "creation_time=$formattedDateTime",
|
|
126
|
+
outputFile
|
|
127
|
+
))
|
|
128
|
+
} else {
|
|
129
|
+
// Stream copy: no re-encoding, extremely fast but only cuts at keyframes.
|
|
130
|
+
cmds.addAll(listOf(
|
|
131
|
+
"-i", inputFile,
|
|
132
|
+
"-c", "copy",
|
|
133
|
+
"-metadata", "creation_time=$formattedDateTime",
|
|
134
|
+
outputFile
|
|
135
|
+
))
|
|
136
|
+
}
|
|
72
137
|
|
|
73
138
|
val command = cmds.toTypedArray()
|
|
74
139
|
val cmdStr = "Command: ${command.joinToString(" ")}"
|
|
@@ -82,16 +147,18 @@ object VideoTrimmerUtil {
|
|
|
82
147
|
return FFmpegKit.executeWithArgumentsAsync(command, { session ->
|
|
83
148
|
val state = session.state
|
|
84
149
|
val returnCode = session.returnCode
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
150
|
+
UiThreadUtil.runOnUiThread {
|
|
151
|
+
when {
|
|
152
|
+
ReturnCode.isSuccess(returnCode) -> {
|
|
153
|
+
callback.onFinishTrim(outputFile, startMs, endMs, videoDuration)
|
|
154
|
+
}
|
|
155
|
+
ReturnCode.isCancel(returnCode) -> {
|
|
156
|
+
callback.onCancelTrim()
|
|
157
|
+
}
|
|
158
|
+
else -> {
|
|
159
|
+
val errorMessage = "Command failed with state $state and rc $returnCode.${session.failStackTrace}"
|
|
160
|
+
callback.onError(errorMessage, ErrorCode.TRIMMING_FAILED)
|
|
161
|
+
}
|
|
95
162
|
}
|
|
96
163
|
}
|
|
97
164
|
}, { log ->
|
|
@@ -102,12 +169,16 @@ object VideoTrimmerUtil {
|
|
|
102
169
|
map.putString("message", log.message)
|
|
103
170
|
map.putDouble("sessionId", log.sessionId.toDouble())
|
|
104
171
|
map.putString("logStr", log.toString())
|
|
105
|
-
|
|
172
|
+
UiThreadUtil.runOnUiThread {
|
|
173
|
+
callback.onLog(map)
|
|
174
|
+
}
|
|
106
175
|
}, { statistics ->
|
|
107
176
|
val timeInMilliseconds = statistics.time.toInt()
|
|
108
177
|
if (timeInMilliseconds > 0) {
|
|
109
178
|
val completePercentage = (timeInMilliseconds * 100) / videoDuration
|
|
110
|
-
|
|
179
|
+
UiThreadUtil.runOnUiThread {
|
|
180
|
+
callback.onTrimmingProgress(completePercentage.coerceIn(0, 100))
|
|
181
|
+
}
|
|
111
182
|
}
|
|
112
183
|
|
|
113
184
|
val map = Arguments.createMap()
|
|
@@ -120,7 +191,9 @@ object VideoTrimmerUtil {
|
|
|
120
191
|
map.putDouble("bitrate", statistics.bitrate.toDouble())
|
|
121
192
|
map.putDouble("speed", statistics.speed.toDouble())
|
|
122
193
|
map.putString("statisticsStr", statistics.toString())
|
|
123
|
-
|
|
194
|
+
UiThreadUtil.runOnUiThread {
|
|
195
|
+
callback.onStatistics(map)
|
|
196
|
+
}
|
|
124
197
|
})
|
|
125
198
|
}
|
|
126
199
|
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
package com.videotrim.widgets
|
|
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.graphics.Region
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.util.AttributeSet
|
|
12
|
+
import android.util.TypedValue
|
|
13
|
+
import android.view.MotionEvent
|
|
14
|
+
import android.view.ScaleGestureDetector
|
|
15
|
+
import android.view.View
|
|
16
|
+
import kotlin.math.abs
|
|
17
|
+
import kotlin.math.max
|
|
18
|
+
import kotlin.math.min
|
|
19
|
+
|
|
20
|
+
class CropOverlayView @JvmOverloads constructor(
|
|
21
|
+
context: Context,
|
|
22
|
+
attrs: AttributeSet? = null,
|
|
23
|
+
defStyleAttr: Int = 0
|
|
24
|
+
) : View(context, attrs, defStyleAttr) {
|
|
25
|
+
|
|
26
|
+
var cropRect = RectF()
|
|
27
|
+
set(value) {
|
|
28
|
+
field.set(value)
|
|
29
|
+
invalidate()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var allowedRect = RectF()
|
|
33
|
+
set(value) {
|
|
34
|
+
field.set(value)
|
|
35
|
+
if (cropRect.isEmpty) {
|
|
36
|
+
cropRect = RectF(value)
|
|
37
|
+
} else {
|
|
38
|
+
val clamped = RectF()
|
|
39
|
+
if (!clamped.setIntersect(cropRect, value) || clamped.width() < minCropSize || clamped.height() < minCropSize) {
|
|
40
|
+
cropRect = RectF(value)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
var onCropChanged: (() -> Unit)? = null
|
|
46
|
+
var onCropBegan: (() -> Unit)? = null
|
|
47
|
+
var onCropEnded: (() -> Unit)? = null
|
|
48
|
+
|
|
49
|
+
private val minCropSize = dpToPx(60f)
|
|
50
|
+
private val borderWidth = dpToPx(1f)
|
|
51
|
+
private val cornerLength = dpToPx(20f)
|
|
52
|
+
private val cornerWidth = dpToPx(4f)
|
|
53
|
+
private val gridLineWidth = 1f / resources.displayMetrics.density
|
|
54
|
+
private val edgeHitZone = dpToPx(30f)
|
|
55
|
+
|
|
56
|
+
private val borderPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
57
|
+
style = Paint.Style.STROKE
|
|
58
|
+
color = Color.argb(153, 255, 255, 255)
|
|
59
|
+
strokeWidth = borderWidth
|
|
60
|
+
}
|
|
61
|
+
private val gridPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
62
|
+
style = Paint.Style.STROKE
|
|
63
|
+
color = Color.WHITE
|
|
64
|
+
strokeWidth = gridLineWidth
|
|
65
|
+
}
|
|
66
|
+
private val cornerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
|
|
67
|
+
style = Paint.Style.STROKE
|
|
68
|
+
color = Color.WHITE
|
|
69
|
+
strokeWidth = cornerWidth
|
|
70
|
+
strokeCap = Paint.Cap.ROUND
|
|
71
|
+
}
|
|
72
|
+
private val dimmingPaint = Paint().apply {
|
|
73
|
+
color = Color.argb(140, 0, 0, 0)
|
|
74
|
+
style = Paint.Style.FILL
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private enum class DragEdge {
|
|
78
|
+
TOP, BOTTOM, LEFT, RIGHT,
|
|
79
|
+
TOP_LEFT, TOP_RIGHT, BOTTOM_LEFT, BOTTOM_RIGHT,
|
|
80
|
+
MOVE
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private var activeEdge: DragEdge? = null
|
|
84
|
+
private var dragStartX = 0f
|
|
85
|
+
private var dragStartY = 0f
|
|
86
|
+
private var dragStartRect = RectF()
|
|
87
|
+
private var gestureStarted = false
|
|
88
|
+
|
|
89
|
+
private val scaleDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
|
90
|
+
private var pinchStartRect = RectF()
|
|
91
|
+
|
|
92
|
+
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
|
93
|
+
pinchStartRect.set(cropRect)
|
|
94
|
+
if (!gestureStarted) {
|
|
95
|
+
gestureStarted = true
|
|
96
|
+
onCropBegan?.invoke()
|
|
97
|
+
}
|
|
98
|
+
return true
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
|
102
|
+
val scale = detector.scaleFactor
|
|
103
|
+
val cx = pinchStartRect.centerX()
|
|
104
|
+
val cy = pinchStartRect.centerY()
|
|
105
|
+
var newW = max(pinchStartRect.width() * scale, minCropSize)
|
|
106
|
+
var newH = max(pinchStartRect.height() * scale, minCropSize)
|
|
107
|
+
newW = min(newW, allowedRect.width())
|
|
108
|
+
newH = min(newH, allowedRect.height())
|
|
109
|
+
val r = RectF(cx - newW / 2, cy - newH / 2, cx + newW / 2, cy + newH / 2)
|
|
110
|
+
clamp(r, isMove = true)
|
|
111
|
+
cropRect = r
|
|
112
|
+
onCropChanged?.invoke()
|
|
113
|
+
return true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
|
117
|
+
if (gestureStarted) {
|
|
118
|
+
gestureStarted = false
|
|
119
|
+
onCropEnded?.invoke()
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
override fun onDraw(canvas: Canvas) {
|
|
125
|
+
super.onDraw(canvas)
|
|
126
|
+
if (cropRect.isEmpty) return
|
|
127
|
+
val cr = cropRect
|
|
128
|
+
|
|
129
|
+
drawDimming(canvas, cr)
|
|
130
|
+
|
|
131
|
+
canvas.drawRect(cr, borderPaint)
|
|
132
|
+
|
|
133
|
+
for (i in 1..2) {
|
|
134
|
+
val x = cr.left + cr.width() * i / 3f
|
|
135
|
+
canvas.drawLine(x, cr.top, x, cr.bottom, gridPaint)
|
|
136
|
+
}
|
|
137
|
+
for (i in 1..2) {
|
|
138
|
+
val y = cr.top + cr.height() * i / 3f
|
|
139
|
+
canvas.drawLine(cr.left, y, cr.right, y, gridPaint)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
val cl = cornerLength
|
|
143
|
+
val path = Path()
|
|
144
|
+
fun addCorner(sx: Float, sy: Float, cx: Float, cy: Float, ex: Float, ey: Float) {
|
|
145
|
+
path.moveTo(sx, sy); path.lineTo(cx, cy); path.lineTo(ex, ey)
|
|
146
|
+
}
|
|
147
|
+
addCorner(cr.left, cr.top + cl, cr.left, cr.top, cr.left + cl, cr.top)
|
|
148
|
+
addCorner(cr.right - cl, cr.top, cr.right, cr.top, cr.right, cr.top + cl)
|
|
149
|
+
addCorner(cr.left, cr.bottom - cl, cr.left, cr.bottom, cr.left + cl, cr.bottom)
|
|
150
|
+
addCorner(cr.right - cl, cr.bottom, cr.right, cr.bottom, cr.right, cr.bottom - cl)
|
|
151
|
+
canvas.drawPath(path, cornerPaint)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private fun drawDimming(canvas: Canvas, cr: RectF) {
|
|
155
|
+
canvas.save()
|
|
156
|
+
val fullPath = Path()
|
|
157
|
+
fullPath.addRect(0f, 0f, width.toFloat(), height.toFloat(), Path.Direction.CW)
|
|
158
|
+
val cropPath = Path()
|
|
159
|
+
cropPath.addRect(cr, Path.Direction.CW)
|
|
160
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
161
|
+
fullPath.op(cropPath, Path.Op.DIFFERENCE)
|
|
162
|
+
canvas.drawPath(fullPath, dimmingPaint)
|
|
163
|
+
} else {
|
|
164
|
+
@Suppress("DEPRECATION")
|
|
165
|
+
canvas.clipPath(cropPath, Region.Op.DIFFERENCE)
|
|
166
|
+
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), dimmingPaint)
|
|
167
|
+
}
|
|
168
|
+
canvas.restore()
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
172
|
+
if (event.pointerCount > 1) {
|
|
173
|
+
scaleDetector.onTouchEvent(event)
|
|
174
|
+
return true
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
val x = event.x
|
|
178
|
+
val y = event.y
|
|
179
|
+
|
|
180
|
+
when (event.actionMasked) {
|
|
181
|
+
MotionEvent.ACTION_DOWN -> {
|
|
182
|
+
activeEdge = detectEdge(x, y) ?: return false
|
|
183
|
+
dragStartX = x
|
|
184
|
+
dragStartY = y
|
|
185
|
+
dragStartRect.set(cropRect)
|
|
186
|
+
gestureStarted = true
|
|
187
|
+
onCropBegan?.invoke()
|
|
188
|
+
return true
|
|
189
|
+
}
|
|
190
|
+
MotionEvent.ACTION_MOVE -> {
|
|
191
|
+
val edge = activeEdge ?: return false
|
|
192
|
+
val dx = x - dragStartX
|
|
193
|
+
val dy = y - dragStartY
|
|
194
|
+
cropRect = computeNewRect(edge, dx, dy)
|
|
195
|
+
onCropChanged?.invoke()
|
|
196
|
+
return true
|
|
197
|
+
}
|
|
198
|
+
MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
|
|
199
|
+
activeEdge = null
|
|
200
|
+
if (gestureStarted) {
|
|
201
|
+
gestureStarted = false
|
|
202
|
+
onCropEnded?.invoke()
|
|
203
|
+
}
|
|
204
|
+
return true
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private fun detectEdge(px: Float, py: Float): DragEdge? {
|
|
211
|
+
val r = cropRect
|
|
212
|
+
val z = edgeHitZone
|
|
213
|
+
|
|
214
|
+
val nearT = abs(py - r.top) < z
|
|
215
|
+
val nearB = abs(py - r.bottom) < z
|
|
216
|
+
val nearL = abs(px - r.left) < z
|
|
217
|
+
val nearR = abs(px - r.right) < z
|
|
218
|
+
val inH = px > r.left - z && px < r.right + z
|
|
219
|
+
val inV = py > r.top - z && py < r.bottom + z
|
|
220
|
+
|
|
221
|
+
if (nearT && nearL) return DragEdge.TOP_LEFT
|
|
222
|
+
if (nearT && nearR) return DragEdge.TOP_RIGHT
|
|
223
|
+
if (nearB && nearL) return DragEdge.BOTTOM_LEFT
|
|
224
|
+
if (nearB && nearR) return DragEdge.BOTTOM_RIGHT
|
|
225
|
+
if (nearT && inH) return DragEdge.TOP
|
|
226
|
+
if (nearB && inH) return DragEdge.BOTTOM
|
|
227
|
+
if (nearL && inV) return DragEdge.LEFT
|
|
228
|
+
if (nearR && inV) return DragEdge.RIGHT
|
|
229
|
+
if (r.contains(px, py)) return DragEdge.MOVE
|
|
230
|
+
return null
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
private fun computeNewRect(edge: DragEdge, dx: Float, dy: Float): RectF {
|
|
234
|
+
val r = RectF(dragStartRect)
|
|
235
|
+
|
|
236
|
+
when (edge) {
|
|
237
|
+
DragEdge.TOP_LEFT -> { r.left += dx; r.top += dy }
|
|
238
|
+
DragEdge.TOP_RIGHT -> { r.right += dx; r.top += dy }
|
|
239
|
+
DragEdge.BOTTOM_LEFT -> { r.left += dx; r.bottom += dy }
|
|
240
|
+
DragEdge.BOTTOM_RIGHT -> { r.right += dx; r.bottom += dy }
|
|
241
|
+
DragEdge.TOP -> r.top += dy
|
|
242
|
+
DragEdge.BOTTOM -> r.bottom += dy
|
|
243
|
+
DragEdge.LEFT -> r.left += dx
|
|
244
|
+
DragEdge.RIGHT -> r.right += dx
|
|
245
|
+
DragEdge.MOVE -> {
|
|
246
|
+
r.offset(dx, dy)
|
|
247
|
+
clamp(r, isMove = true)
|
|
248
|
+
return r
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (r.width() < minCropSize) {
|
|
253
|
+
val anchorsRight = edge == DragEdge.LEFT || edge == DragEdge.TOP_LEFT || edge == DragEdge.BOTTOM_LEFT
|
|
254
|
+
if (anchorsRight) r.left = r.right - minCropSize else r.right = r.left + minCropSize
|
|
255
|
+
}
|
|
256
|
+
if (r.height() < minCropSize) {
|
|
257
|
+
val anchorsBottom = edge == DragEdge.TOP || edge == DragEdge.TOP_LEFT || edge == DragEdge.TOP_RIGHT
|
|
258
|
+
if (anchorsBottom) r.top = r.bottom - minCropSize else r.bottom = r.top + minCropSize
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
clamp(r, isMove = false)
|
|
262
|
+
return r
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
private fun clamp(r: RectF, isMove: Boolean) {
|
|
266
|
+
val a = allowedRect
|
|
267
|
+
if (a.isEmpty) return
|
|
268
|
+
|
|
269
|
+
if (isMove) {
|
|
270
|
+
val w = min(r.width(), a.width())
|
|
271
|
+
val h = min(r.height(), a.height())
|
|
272
|
+
r.right = r.left + w
|
|
273
|
+
r.bottom = r.top + h
|
|
274
|
+
if (r.left < a.left) r.offset(a.left - r.left, 0f)
|
|
275
|
+
if (r.top < a.top) r.offset(0f, a.top - r.top)
|
|
276
|
+
if (r.right > a.right) r.offset(a.right - r.right, 0f)
|
|
277
|
+
if (r.bottom > a.bottom) r.offset(0f, a.bottom - r.bottom)
|
|
278
|
+
} else {
|
|
279
|
+
r.left = max(r.left, a.left)
|
|
280
|
+
r.top = max(r.top, a.top)
|
|
281
|
+
if (r.right > a.right) r.right = a.right
|
|
282
|
+
if (r.bottom > a.bottom) r.bottom = a.bottom
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
fun resetCrop() {
|
|
287
|
+
cropRect = RectF(allowedRect)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private fun dpToPx(dp: Float): Float {
|
|
291
|
+
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, resources.displayMetrics)
|
|
292
|
+
}
|
|
293
|
+
}
|