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
|
@@ -4,6 +4,9 @@ import android.content.Context
|
|
|
4
4
|
import android.content.pm.ActivityInfo
|
|
5
5
|
import android.content.res.Configuration
|
|
6
6
|
import android.graphics.Color
|
|
7
|
+
import android.graphics.Matrix
|
|
8
|
+
import android.graphics.RectF
|
|
9
|
+
import android.graphics.SurfaceTexture
|
|
7
10
|
import android.graphics.drawable.GradientDrawable
|
|
8
11
|
import android.media.MediaMetadataRetriever
|
|
9
12
|
import android.media.MediaPlayer
|
|
@@ -17,15 +20,18 @@ import android.util.Log
|
|
|
17
20
|
import android.util.TypedValue
|
|
18
21
|
import android.view.GestureDetector
|
|
19
22
|
import android.view.LayoutInflater
|
|
23
|
+
import android.view.Gravity
|
|
20
24
|
import android.view.MotionEvent
|
|
25
|
+
import android.view.Surface
|
|
26
|
+
import android.view.TextureView
|
|
21
27
|
import android.view.View
|
|
28
|
+
import android.view.animation.DecelerateInterpolator
|
|
22
29
|
import android.widget.FrameLayout
|
|
23
30
|
import android.widget.ImageView
|
|
24
31
|
import android.widget.LinearLayout
|
|
25
32
|
import android.widget.ProgressBar
|
|
26
33
|
import android.widget.RelativeLayout
|
|
27
34
|
import android.widget.TextView
|
|
28
|
-
import android.widget.VideoView
|
|
29
35
|
|
|
30
36
|
import androidx.appcompat.app.AlertDialog
|
|
31
37
|
|
|
@@ -64,11 +70,9 @@ class VideoTrimmerView(
|
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
private var mContext: ReactApplicationContext = context
|
|
67
|
-
private lateinit var mVideoView:
|
|
73
|
+
private lateinit var mVideoView: TextureView
|
|
74
|
+
private var videoSurface: Surface? = null
|
|
68
75
|
|
|
69
|
-
// mediaPlayer is used for both video/audio
|
|
70
|
-
// the reason we use mediaPlayer for Video: https://stackoverflow.com/a/73361868/7569705
|
|
71
|
-
// the videoPlayer is to solve the issue after manually seek -> hit play -> it starts from a position slightly before with the one we just sought to
|
|
72
76
|
private var mediaPlayer: MediaPlayer? = null
|
|
73
77
|
private lateinit var mPlayView: ImageView
|
|
74
78
|
private lateinit var mThumbnailContainer: LinearLayout
|
|
@@ -96,8 +100,6 @@ class VideoTrimmerView(
|
|
|
96
100
|
|
|
97
101
|
private var startTime = 0L
|
|
98
102
|
private var endTime = 0L
|
|
99
|
-
private var enableRotation = false
|
|
100
|
-
private var rotationAngle = 0.0
|
|
101
103
|
private var zoomOnWaitingDuration = 5000L
|
|
102
104
|
|
|
103
105
|
private var vibrator: Vibrator? = null
|
|
@@ -124,6 +126,8 @@ class VideoTrimmerView(
|
|
|
124
126
|
private var isGeneratingThumbnails = false
|
|
125
127
|
|
|
126
128
|
private var mediaMetadataRetriever: MediaMetadataRetriever? = null
|
|
129
|
+
private val retrieverLock = Object()
|
|
130
|
+
@Volatile private var retrieverReleased = false
|
|
127
131
|
private lateinit var loadingIndicator: ProgressBar
|
|
128
132
|
private lateinit var saveBtn: TextView
|
|
129
133
|
private lateinit var cancelBtn: TextView
|
|
@@ -133,6 +137,7 @@ class VideoTrimmerView(
|
|
|
133
137
|
|
|
134
138
|
private var mOutputExt = "mp4"
|
|
135
139
|
private var enableHapticFeedback = true
|
|
140
|
+
private var enablePreciseTrimming = false
|
|
136
141
|
private var autoplay = false
|
|
137
142
|
private var jumpToPositionOnLoad = 0L
|
|
138
143
|
private lateinit var headerView: FrameLayout
|
|
@@ -144,6 +149,34 @@ class VideoTrimmerView(
|
|
|
144
149
|
private var alertOnFailCloseText = "Close"
|
|
145
150
|
private var currentSelectedhandle: View? = null
|
|
146
151
|
|
|
152
|
+
// Transform state
|
|
153
|
+
var rotationCount = 0
|
|
154
|
+
private set
|
|
155
|
+
var isFlipped = false
|
|
156
|
+
private set
|
|
157
|
+
private var isCropActive = false
|
|
158
|
+
private var cumulativeRotationDeg = 0f
|
|
159
|
+
|
|
160
|
+
private data class TransformSnapshot(
|
|
161
|
+
val rotationCount: Int,
|
|
162
|
+
val isFlipped: Boolean,
|
|
163
|
+
val isCropActive: Boolean,
|
|
164
|
+
val cropNormalized: RectF?,
|
|
165
|
+
val cumulativeRotationDeg: Float
|
|
166
|
+
)
|
|
167
|
+
private val undoStack = mutableListOf<TransformSnapshot>()
|
|
168
|
+
private val redoStack = mutableListOf<TransformSnapshot>()
|
|
169
|
+
private var preCropSnapshot: TransformSnapshot? = null
|
|
170
|
+
|
|
171
|
+
private lateinit var transformRow: LinearLayout
|
|
172
|
+
private lateinit var flipBtn: ImageView
|
|
173
|
+
private lateinit var rotateBtn: ImageView
|
|
174
|
+
private lateinit var cropBtn: ImageView
|
|
175
|
+
private lateinit var undoBtn: ImageView
|
|
176
|
+
private lateinit var redoBtn: ImageView
|
|
177
|
+
private lateinit var videoContainer: FrameLayout
|
|
178
|
+
private var cropOverlay: CropOverlayView? = null
|
|
179
|
+
|
|
147
180
|
private lateinit var trimmerView: RelativeLayout
|
|
148
181
|
|
|
149
182
|
private var trimmerColor = context.getString(R.string.trim_color).toColorInt()
|
|
@@ -158,7 +191,7 @@ class VideoTrimmerView(
|
|
|
158
191
|
private fun init(context: ReactApplicationContext, config: ReadableMap?) {
|
|
159
192
|
mContext = context
|
|
160
193
|
|
|
161
|
-
context.currentActivity
|
|
194
|
+
context.currentActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
162
195
|
LayoutInflater.from(context).inflate(R.layout.video_trimmer_view, this, true)
|
|
163
196
|
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
164
197
|
|
|
@@ -214,23 +247,31 @@ class VideoTrimmerView(
|
|
|
214
247
|
|
|
215
248
|
leadingChevron = findViewById(R.id.leadingChevron)
|
|
216
249
|
trailingChevron = findViewById(R.id.trailingChevron)
|
|
250
|
+
|
|
251
|
+
transformRow = findViewById(R.id.transformRow)
|
|
252
|
+
flipBtn = findViewById(R.id.flipBtn)
|
|
253
|
+
rotateBtn = findViewById(R.id.rotateBtn)
|
|
254
|
+
cropBtn = findViewById(R.id.cropBtn)
|
|
255
|
+
undoBtn = findViewById(R.id.undoBtn)
|
|
256
|
+
redoBtn = findViewById(R.id.redoBtn)
|
|
257
|
+
videoContainer = findViewById(R.id.videoContainer)
|
|
217
258
|
}
|
|
218
259
|
|
|
219
260
|
fun initByURI(videoURI: Uri) {
|
|
220
261
|
mSourceUri = videoURI
|
|
221
262
|
|
|
222
263
|
if (isVideoType) {
|
|
223
|
-
mVideoView.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
mVideoView.
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
264
|
+
if (mVideoView.isAvailable) {
|
|
265
|
+
setupVideoPlayer(mVideoView.surfaceTexture!!, videoURI)
|
|
266
|
+
}
|
|
267
|
+
mVideoView.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
|
|
268
|
+
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
|
|
269
|
+
setupVideoPlayer(st, videoURI)
|
|
270
|
+
}
|
|
271
|
+
override fun onSurfaceTextureSizeChanged(st: SurfaceTexture, w: Int, h: Int) {}
|
|
272
|
+
override fun onSurfaceTextureDestroyed(st: SurfaceTexture): Boolean = true
|
|
273
|
+
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
|
|
230
274
|
}
|
|
231
|
-
|
|
232
|
-
mVideoView.setOnErrorListener { mp, what, extra -> onFailToLoadMedia(mp, what, extra) }
|
|
233
|
-
mVideoView.setOnCompletionListener { mediaCompleted() }
|
|
234
275
|
} else {
|
|
235
276
|
mVideoView.visibility = View.GONE
|
|
236
277
|
audioBannerView.alpha = 0f
|
|
@@ -252,11 +293,58 @@ class VideoTrimmerView(
|
|
|
252
293
|
}
|
|
253
294
|
}
|
|
254
295
|
|
|
296
|
+
private fun setupVideoPlayer(surfaceTexture: SurfaceTexture, videoURI: Uri) {
|
|
297
|
+
if (mediaPlayer != null) return
|
|
298
|
+
val mp = MediaPlayer()
|
|
299
|
+
try {
|
|
300
|
+
videoSurface = Surface(surfaceTexture)
|
|
301
|
+
mp.setSurface(videoSurface)
|
|
302
|
+
mp.setDataSource(mContext, videoURI)
|
|
303
|
+
mp.setOnPreparedListener {
|
|
304
|
+
mediaPlayer = mp
|
|
305
|
+
mediaPrepared()
|
|
306
|
+
}
|
|
307
|
+
mp.setOnErrorListener { mpCb, what, extra -> onFailToLoadMedia(mpCb, what, extra) }
|
|
308
|
+
mp.setOnCompletionListener { mediaCompleted() }
|
|
309
|
+
mp.prepareAsync()
|
|
310
|
+
} catch (e: Exception) {
|
|
311
|
+
e.printStackTrace()
|
|
312
|
+
mediaFailed()
|
|
313
|
+
mOnTrimVideoListener.onError("Error initializing video player.", ErrorCode.FAIL_TO_LOAD_MEDIA)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
private fun updateVideoViewSize() {
|
|
318
|
+
val vw = mediaPlayer?.videoWidth ?: return
|
|
319
|
+
val vh = mediaPlayer?.videoHeight ?: return
|
|
320
|
+
if (vw <= 0 || vh <= 0) return
|
|
321
|
+
|
|
322
|
+
videoContainer.post {
|
|
323
|
+
val containerW = videoContainer.width
|
|
324
|
+
val containerH = videoContainer.height
|
|
325
|
+
if (containerW <= 0 || containerH <= 0) return@post
|
|
326
|
+
|
|
327
|
+
val videoAR = vw.toFloat() / vh
|
|
328
|
+
val containerAR = containerW.toFloat() / containerH
|
|
329
|
+
val newW: Int
|
|
330
|
+
val newH: Int
|
|
331
|
+
if (videoAR > containerAR) {
|
|
332
|
+
newW = containerW
|
|
333
|
+
newH = (containerW / videoAR).toInt()
|
|
334
|
+
} else {
|
|
335
|
+
newH = containerH
|
|
336
|
+
newW = (containerH * videoAR).toInt()
|
|
337
|
+
}
|
|
338
|
+
mVideoView.layoutParams = FrameLayout.LayoutParams(newW, newH, Gravity.CENTER)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
255
342
|
private fun onFailToLoadMedia(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
|
256
343
|
mediaFailed()
|
|
257
344
|
mOnTrimVideoListener.onError("Error loading media file. Please try again.", ErrorCode.FAIL_TO_LOAD_MEDIA)
|
|
258
345
|
if (alertOnFailToLoad) {
|
|
259
|
-
val
|
|
346
|
+
val activity = mContext.currentActivity ?: return true
|
|
347
|
+
val builder = AlertDialog.Builder(activity)
|
|
260
348
|
builder.setMessage(alertOnFailMessage)
|
|
261
349
|
builder.setTitle(alertOnFailTitle)
|
|
262
350
|
builder.setCancelable(false)
|
|
@@ -305,6 +393,8 @@ class VideoTrimmerView(
|
|
|
305
393
|
mMaxDuration = mMaxDuration.coerceAtMost(mDuration.toLong())
|
|
306
394
|
|
|
307
395
|
if (isVideoType) {
|
|
396
|
+
updateVideoViewSize()
|
|
397
|
+
|
|
308
398
|
mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString())
|
|
309
399
|
if (mediaMetadataRetriever == null) {
|
|
310
400
|
mOnTrimVideoListener.onError("Error when retrieving video info. Please try again.", ErrorCode.FAIL_TO_GET_VIDEO_INFO)
|
|
@@ -344,6 +434,13 @@ class VideoTrimmerView(
|
|
|
344
434
|
playOrPause()
|
|
345
435
|
}
|
|
346
436
|
|
|
437
|
+
if (isVideoType) {
|
|
438
|
+
transformRow.alpha = 0f
|
|
439
|
+
transformRow.visibility = View.VISIBLE
|
|
440
|
+
transformRow.animate().alpha(1f).setDuration(250).start()
|
|
441
|
+
updateUndoRedoButtons()
|
|
442
|
+
}
|
|
443
|
+
|
|
347
444
|
mOnTrimVideoListener.onLoad(mDuration)
|
|
348
445
|
ignoreSystemGestureForView(trimmerView)
|
|
349
446
|
}
|
|
@@ -416,18 +513,37 @@ class VideoTrimmerView(
|
|
|
416
513
|
mPlayView.setOnClickListener { playOrPause() }
|
|
417
514
|
setHandleTouchListener(leadingHandle, true)
|
|
418
515
|
setHandleTouchListener(trailingHandle, false)
|
|
516
|
+
|
|
517
|
+
flipBtn.setOnClickListener { onFlipTapped() }
|
|
518
|
+
rotateBtn.setOnClickListener { onRotateTapped() }
|
|
519
|
+
cropBtn.setOnClickListener { onCropTapped() }
|
|
520
|
+
undoBtn.setOnClickListener { onUndoTapped() }
|
|
521
|
+
redoBtn.setOnClickListener { onRedoTapped() }
|
|
419
522
|
}
|
|
420
523
|
|
|
421
524
|
fun onSaveClicked() {
|
|
422
525
|
onMediaPause()
|
|
526
|
+
val vw = mediaPlayer?.videoWidth ?: 0
|
|
527
|
+
val vh = mediaPlayer?.videoHeight ?: 0
|
|
528
|
+
val bitrate = synchronized(retrieverLock) {
|
|
529
|
+
if (retrieverReleased) 0L
|
|
530
|
+
else mediaMetadataRetriever
|
|
531
|
+
?.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE)
|
|
532
|
+
?.toLongOrNull() ?: 0L
|
|
533
|
+
}
|
|
423
534
|
ffmpegSession = VideoTrimmerUtil.trim(
|
|
424
535
|
mSourceUri.toString(),
|
|
425
536
|
StorageUtil.getOutputPath(mContext, mOutputExt),
|
|
426
537
|
mDuration,
|
|
427
538
|
startTime,
|
|
428
539
|
endTime,
|
|
429
|
-
|
|
430
|
-
|
|
540
|
+
rotationCount,
|
|
541
|
+
isFlipped,
|
|
542
|
+
getCropNormalizedRect(),
|
|
543
|
+
vw,
|
|
544
|
+
vh,
|
|
545
|
+
bitrate,
|
|
546
|
+
enablePreciseTrimming,
|
|
431
547
|
mOnTrimVideoListener
|
|
432
548
|
)
|
|
433
549
|
}
|
|
@@ -462,6 +578,7 @@ class VideoTrimmerView(
|
|
|
462
578
|
override fun onDestroy() {
|
|
463
579
|
isGeneratingThumbnails = false
|
|
464
580
|
BackgroundExecutor.cancelAll("", true)
|
|
581
|
+
BackgroundExecutor.cancelAll("progressive_thumbs", true)
|
|
465
582
|
UiThreadExecutor.cancelAll("")
|
|
466
583
|
mContext.currentActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
467
584
|
mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
|
|
@@ -469,10 +586,21 @@ class VideoTrimmerView(
|
|
|
469
586
|
|
|
470
587
|
cachedFullViewThumbnails.clear()
|
|
471
588
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
589
|
+
cropOverlay?.onCropBegan = null
|
|
590
|
+
cropOverlay?.onCropEnded = null
|
|
591
|
+
cropOverlay?.onCropChanged = null
|
|
592
|
+
cropOverlay = null
|
|
593
|
+
undoStack.clear()
|
|
594
|
+
redoStack.clear()
|
|
595
|
+
cumulativeRotationDeg = 0f
|
|
596
|
+
|
|
597
|
+
synchronized(retrieverLock) {
|
|
598
|
+
retrieverReleased = true
|
|
599
|
+
try {
|
|
600
|
+
mediaMetadataRetriever?.release()
|
|
601
|
+
} catch (e: Exception) {
|
|
602
|
+
e.printStackTrace()
|
|
603
|
+
}
|
|
476
604
|
}
|
|
477
605
|
|
|
478
606
|
try {
|
|
@@ -482,6 +610,11 @@ class VideoTrimmerView(
|
|
|
482
610
|
e.printStackTrace()
|
|
483
611
|
Log.d(TAG, "onDestroy mediaPlayer is already released")
|
|
484
612
|
}
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
videoSurface?.release()
|
|
616
|
+
} catch (_: Exception) {}
|
|
617
|
+
videoSurface = null
|
|
485
618
|
}
|
|
486
619
|
|
|
487
620
|
private fun getScreenWidthInPortraitMode(): Int {
|
|
@@ -510,6 +643,7 @@ class VideoTrimmerView(
|
|
|
510
643
|
mOutputExt = "wav"
|
|
511
644
|
}
|
|
512
645
|
enableHapticFeedback = config.hasKey("enableHapticFeedback") && config.getBoolean("enableHapticFeedback")
|
|
646
|
+
enablePreciseTrimming = config.hasKey("enablePreciseTrimming") && config.getBoolean("enablePreciseTrimming")
|
|
513
647
|
autoplay = config.hasKey("autoplay") && config.getBoolean("autoplay")
|
|
514
648
|
|
|
515
649
|
if (config.hasKey("jumpToPositionOnLoad") && config.getDouble("jumpToPositionOnLoad") > 0) {
|
|
@@ -530,8 +664,6 @@ class VideoTrimmerView(
|
|
|
530
664
|
alertOnFailTitle = if (config.hasKey("alertOnFailTitle")) config.getString("alertOnFailTitle") ?: "Error" else "Error"
|
|
531
665
|
alertOnFailMessage = if (config.hasKey("alertOnFailMessage")) config.getString("alertOnFailMessage") ?: "Fail to load media. Possibly invalid file or no network connection" else "Fail to load media. Possibly invalid file or no network connection"
|
|
532
666
|
alertOnFailCloseText = if (config.hasKey("alertOnFailCloseText")) config.getString("alertOnFailCloseText") ?: "Close" else "Close"
|
|
533
|
-
enableRotation = config.hasKey("enableRotation") && config.getBoolean("enableRotation")
|
|
534
|
-
rotationAngle = if (config.hasKey("rotationAngle")) config.getDouble("rotationAngle") else 0.0
|
|
535
667
|
|
|
536
668
|
if (config.hasKey("zoomOnWaitingDuration") && config.getDouble("zoomOnWaitingDuration") > 0) {
|
|
537
669
|
zoomOnWaitingDuration = config.getDouble("zoomOnWaitingDuration").toLong()
|
|
@@ -1069,7 +1201,9 @@ class VideoTrimmerView(
|
|
|
1069
1201
|
val clampedTimeUs = maxOf(0L, minOf(timeUs, mDuration * 1000L))
|
|
1070
1202
|
|
|
1071
1203
|
try {
|
|
1072
|
-
val bitmap =
|
|
1204
|
+
val bitmap = synchronized(retrieverLock) {
|
|
1205
|
+
if (retrieverReleased) null else mediaMetadataRetriever?.getFrameAtTime(clampedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
1206
|
+
}
|
|
1073
1207
|
if (bitmap != null && isGeneratingThumbnails && isZoomedIn) {
|
|
1074
1208
|
UiThreadExecutor.runTask("", {
|
|
1075
1209
|
if (isZoomedIn && index < mThumbnailContainer.childCount) {
|
|
@@ -1167,4 +1301,398 @@ class VideoTrimmerView(
|
|
|
1167
1301
|
mThumbnailContainer.addView(restoredView)
|
|
1168
1302
|
}
|
|
1169
1303
|
}
|
|
1304
|
+
|
|
1305
|
+
// region Transform
|
|
1306
|
+
|
|
1307
|
+
private fun onFlipTapped() {
|
|
1308
|
+
pushUndo()
|
|
1309
|
+
isFlipped = !isFlipped
|
|
1310
|
+
val newCumDeg = -cumulativeRotationDeg
|
|
1311
|
+
val fitScale = if (rotationCount % 2 != 0) {
|
|
1312
|
+
val cw = videoContainer.width.toFloat()
|
|
1313
|
+
val ch = videoContainer.height.toFloat()
|
|
1314
|
+
if (cw > 0 && ch > 0) minOf(cw / ch, ch / cw) else 1f
|
|
1315
|
+
} else {
|
|
1316
|
+
1f
|
|
1317
|
+
}
|
|
1318
|
+
val targetSx = (if (isFlipped) -1f else 1f) * fitScale
|
|
1319
|
+
val oddRotation = rotationCount % 2 != 0
|
|
1320
|
+
|
|
1321
|
+
mVideoView.animate().cancel()
|
|
1322
|
+
if (oddRotation) {
|
|
1323
|
+
mVideoView.animate()
|
|
1324
|
+
.scaleY(0f)
|
|
1325
|
+
.setDuration(125)
|
|
1326
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1327
|
+
.withEndAction {
|
|
1328
|
+
cumulativeRotationDeg = newCumDeg
|
|
1329
|
+
mVideoView.rotation = newCumDeg
|
|
1330
|
+
mVideoView.scaleX = targetSx
|
|
1331
|
+
mVideoView.animate()
|
|
1332
|
+
.scaleY(fitScale)
|
|
1333
|
+
.setDuration(125)
|
|
1334
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1335
|
+
.withEndAction {
|
|
1336
|
+
if (isCropActive) {
|
|
1337
|
+
updateCropAllowedRect()
|
|
1338
|
+
cropOverlay?.resetCrop()
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
.start()
|
|
1342
|
+
}
|
|
1343
|
+
.start()
|
|
1344
|
+
} else {
|
|
1345
|
+
mVideoView.animate()
|
|
1346
|
+
.scaleX(0f)
|
|
1347
|
+
.setDuration(125)
|
|
1348
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1349
|
+
.withEndAction {
|
|
1350
|
+
cumulativeRotationDeg = newCumDeg
|
|
1351
|
+
mVideoView.rotation = newCumDeg
|
|
1352
|
+
mVideoView.animate()
|
|
1353
|
+
.scaleX(targetSx)
|
|
1354
|
+
.setDuration(125)
|
|
1355
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1356
|
+
.withEndAction {
|
|
1357
|
+
if (isCropActive) {
|
|
1358
|
+
updateCropAllowedRect()
|
|
1359
|
+
cropOverlay?.resetCrop()
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
.start()
|
|
1363
|
+
}
|
|
1364
|
+
.start()
|
|
1365
|
+
}
|
|
1366
|
+
playHapticFeedback(true)
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
private fun onRotateTapped() {
|
|
1370
|
+
pushUndo()
|
|
1371
|
+
if (isFlipped) {
|
|
1372
|
+
rotationCount = (rotationCount - 1 + 4) % 4
|
|
1373
|
+
} else {
|
|
1374
|
+
rotationCount = (rotationCount + 1) % 4
|
|
1375
|
+
}
|
|
1376
|
+
cumulativeRotationDeg -= 90f
|
|
1377
|
+
updateVideoTransform(resetCrop = true)
|
|
1378
|
+
playHapticFeedback(true)
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
private fun updateVideoTransform(resetCrop: Boolean = false) {
|
|
1382
|
+
val containerW = videoContainer.width.toFloat()
|
|
1383
|
+
val containerH = videoContainer.height.toFloat()
|
|
1384
|
+
if (containerW <= 0 || containerH <= 0) return
|
|
1385
|
+
|
|
1386
|
+
val fitScale = if (rotationCount % 2 != 0) {
|
|
1387
|
+
minOf(containerW / containerH, containerH / containerW)
|
|
1388
|
+
} else {
|
|
1389
|
+
1f
|
|
1390
|
+
}
|
|
1391
|
+
val flipMul = if (isFlipped) -1f else 1f
|
|
1392
|
+
|
|
1393
|
+
mVideoView.animate()
|
|
1394
|
+
.scaleX(flipMul * fitScale)
|
|
1395
|
+
.scaleY(fitScale)
|
|
1396
|
+
.rotation(cumulativeRotationDeg)
|
|
1397
|
+
.setDuration(250)
|
|
1398
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1399
|
+
.withEndAction {
|
|
1400
|
+
if (resetCrop && isCropActive) {
|
|
1401
|
+
updateCropAllowedRect()
|
|
1402
|
+
cropOverlay?.resetCrop()
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
.start()
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// endregion
|
|
1409
|
+
|
|
1410
|
+
// region Crop
|
|
1411
|
+
|
|
1412
|
+
private fun onCropTapped() {
|
|
1413
|
+
isCropActive = !isCropActive
|
|
1414
|
+
cropBtn.setColorFilter(
|
|
1415
|
+
if (isCropActive) Color.WHITE else Color.argb(128, 255, 255, 255),
|
|
1416
|
+
android.graphics.PorterDuff.Mode.SRC_IN
|
|
1417
|
+
)
|
|
1418
|
+
playHapticFeedback(true)
|
|
1419
|
+
if (isCropActive) showCropOverlay() else hideCropOverlay()
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
private fun showCropOverlay() {
|
|
1423
|
+
hideCropOverlayImmediate()
|
|
1424
|
+
val overlay = CropOverlayView(mContext)
|
|
1425
|
+
overlay.layoutParams = FrameLayout.LayoutParams(
|
|
1426
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
1427
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
1428
|
+
)
|
|
1429
|
+
overlay.alpha = 0f
|
|
1430
|
+
overlay.onCropBegan = { preCropSnapshot = currentSnapshot() }
|
|
1431
|
+
overlay.onCropEnded = {
|
|
1432
|
+
val before = preCropSnapshot
|
|
1433
|
+
preCropSnapshot = null
|
|
1434
|
+
if (before != null && before != currentSnapshot()) {
|
|
1435
|
+
undoStack.add(before)
|
|
1436
|
+
redoStack.clear()
|
|
1437
|
+
updateUndoRedoButtons()
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
videoContainer.addView(overlay)
|
|
1441
|
+
cropOverlay = overlay
|
|
1442
|
+
videoContainer.post {
|
|
1443
|
+
updateCropAllowedRect()
|
|
1444
|
+
overlay.resetCrop()
|
|
1445
|
+
overlay.animate().alpha(1f).setDuration(200).start()
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
private fun hideCropOverlay() {
|
|
1450
|
+
val overlay = cropOverlay ?: return
|
|
1451
|
+
overlay.animate().alpha(0f).setDuration(200).withEndAction {
|
|
1452
|
+
videoContainer.removeView(overlay)
|
|
1453
|
+
}.start()
|
|
1454
|
+
cropOverlay = null
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
private fun showCropOverlayImmediate() {
|
|
1458
|
+
hideCropOverlayImmediate()
|
|
1459
|
+
val overlay = CropOverlayView(mContext)
|
|
1460
|
+
overlay.layoutParams = FrameLayout.LayoutParams(
|
|
1461
|
+
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
1462
|
+
FrameLayout.LayoutParams.MATCH_PARENT
|
|
1463
|
+
)
|
|
1464
|
+
overlay.onCropBegan = { preCropSnapshot = currentSnapshot() }
|
|
1465
|
+
overlay.onCropEnded = {
|
|
1466
|
+
val before = preCropSnapshot
|
|
1467
|
+
preCropSnapshot = null
|
|
1468
|
+
if (before != null && before != currentSnapshot()) {
|
|
1469
|
+
undoStack.add(before)
|
|
1470
|
+
redoStack.clear()
|
|
1471
|
+
updateUndoRedoButtons()
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
videoContainer.addView(overlay)
|
|
1475
|
+
cropOverlay = overlay
|
|
1476
|
+
updateCropAllowedRect()
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
private fun hideCropOverlayImmediate() {
|
|
1480
|
+
val overlay = cropOverlay ?: return
|
|
1481
|
+
videoContainer.removeView(overlay)
|
|
1482
|
+
cropOverlay = null
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
private fun updateCropAllowedRect() {
|
|
1486
|
+
cropOverlay?.allowedRect = getVideoDisplayRectInContainer()
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
private fun getVideoDisplayRectInContainer(): RectF {
|
|
1490
|
+
val containerW = videoContainer.width.toFloat()
|
|
1491
|
+
val containerH = videoContainer.height.toFloat()
|
|
1492
|
+
if (containerW <= 0 || containerH <= 0) return RectF()
|
|
1493
|
+
|
|
1494
|
+
val tvW = mVideoView.width.toFloat()
|
|
1495
|
+
val tvH = mVideoView.height.toFloat()
|
|
1496
|
+
if (tvW <= 0 || tvH <= 0) return RectF()
|
|
1497
|
+
|
|
1498
|
+
val tvX = (containerW - tvW) / 2f
|
|
1499
|
+
val tvY = (containerH - tvH) / 2f
|
|
1500
|
+
val videoRect = RectF(tvX, tvY, tvX + tvW, tvY + tvH)
|
|
1501
|
+
|
|
1502
|
+
val pivotX = containerW / 2f
|
|
1503
|
+
val pivotY = containerH / 2f
|
|
1504
|
+
val fitScale = if (rotationCount % 2 != 0) {
|
|
1505
|
+
minOf(containerW / containerH, containerH / containerW)
|
|
1506
|
+
} else {
|
|
1507
|
+
1f
|
|
1508
|
+
}
|
|
1509
|
+
val flipMul = if (isFlipped) -1f else 1f
|
|
1510
|
+
val geoRotation = if (isFlipped) rotationCount * 90f else -rotationCount * 90f
|
|
1511
|
+
val matrix = Matrix()
|
|
1512
|
+
matrix.setScale(flipMul * fitScale, fitScale, pivotX, pivotY)
|
|
1513
|
+
matrix.postRotate(geoRotation, pivotX, pivotY)
|
|
1514
|
+
|
|
1515
|
+
val pts = floatArrayOf(
|
|
1516
|
+
videoRect.left, videoRect.top,
|
|
1517
|
+
videoRect.right, videoRect.top,
|
|
1518
|
+
videoRect.right, videoRect.bottom,
|
|
1519
|
+
videoRect.left, videoRect.bottom
|
|
1520
|
+
)
|
|
1521
|
+
matrix.mapPoints(pts)
|
|
1522
|
+
|
|
1523
|
+
var minX = pts[0]; var minY = pts[1]
|
|
1524
|
+
var maxX = pts[0]; var maxY = pts[1]
|
|
1525
|
+
for (i in 1 until 4) {
|
|
1526
|
+
minX = minOf(minX, pts[i * 2])
|
|
1527
|
+
minY = minOf(minY, pts[i * 2 + 1])
|
|
1528
|
+
maxX = maxOf(maxX, pts[i * 2])
|
|
1529
|
+
maxY = maxOf(maxY, pts[i * 2 + 1])
|
|
1530
|
+
}
|
|
1531
|
+
return RectF(minX, minY, maxX, maxY)
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
fun getCropNormalizedRect(): RectF? {
|
|
1535
|
+
if (!isCropActive) return null
|
|
1536
|
+
val overlay = cropOverlay ?: return null
|
|
1537
|
+
val cr = overlay.cropRect
|
|
1538
|
+
val allowed = overlay.allowedRect
|
|
1539
|
+
if (cr.isEmpty || allowed.isEmpty) return null
|
|
1540
|
+
if (allowed.width() <= 0 || allowed.height() <= 0) return null
|
|
1541
|
+
|
|
1542
|
+
val normX = (cr.left - allowed.left) / allowed.width()
|
|
1543
|
+
val normY = (cr.top - allowed.top) / allowed.height()
|
|
1544
|
+
val normW = cr.width() / allowed.width()
|
|
1545
|
+
val normH = cr.height() / allowed.height()
|
|
1546
|
+
|
|
1547
|
+
if (normX < 0.01f && normY < 0.01f && normW > 0.98f && normH > 0.98f) return null
|
|
1548
|
+
return RectF(normX, normY, normX + normW, normY + normH)
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
private fun setCropFromNormalized(norm: RectF) {
|
|
1552
|
+
val overlay = cropOverlay ?: return
|
|
1553
|
+
val allowed = overlay.allowedRect
|
|
1554
|
+
if (allowed.isEmpty) return
|
|
1555
|
+
val x = allowed.left + norm.left * allowed.width()
|
|
1556
|
+
val y = allowed.top + norm.top * allowed.height()
|
|
1557
|
+
val w = norm.width() * allowed.width()
|
|
1558
|
+
val h = norm.height() * allowed.height()
|
|
1559
|
+
overlay.cropRect = RectF(x, y, x + w, y + h)
|
|
1560
|
+
}
|
|
1561
|
+
|
|
1562
|
+
// endregion
|
|
1563
|
+
|
|
1564
|
+
// region Undo / Redo
|
|
1565
|
+
|
|
1566
|
+
private fun currentSnapshot(): TransformSnapshot {
|
|
1567
|
+
return TransformSnapshot(
|
|
1568
|
+
rotationCount = rotationCount,
|
|
1569
|
+
isFlipped = isFlipped,
|
|
1570
|
+
isCropActive = isCropActive,
|
|
1571
|
+
cropNormalized = getCropNormalizedRect(),
|
|
1572
|
+
cumulativeRotationDeg = cumulativeRotationDeg
|
|
1573
|
+
)
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
private fun pushUndo() {
|
|
1577
|
+
undoStack.add(currentSnapshot())
|
|
1578
|
+
redoStack.clear()
|
|
1579
|
+
updateUndoRedoButtons()
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
private fun onUndoTapped() {
|
|
1583
|
+
if (undoStack.isEmpty()) return
|
|
1584
|
+
redoStack.add(currentSnapshot())
|
|
1585
|
+
val snap = undoStack.removeLast()
|
|
1586
|
+
applySnapshot(snap)
|
|
1587
|
+
updateUndoRedoButtons()
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
private fun onRedoTapped() {
|
|
1591
|
+
if (redoStack.isEmpty()) return
|
|
1592
|
+
undoStack.add(currentSnapshot())
|
|
1593
|
+
val snap = redoStack.removeLast()
|
|
1594
|
+
applySnapshot(snap)
|
|
1595
|
+
updateUndoRedoButtons()
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
private fun applySnapshot(snap: TransformSnapshot) {
|
|
1599
|
+
val flipChanging = isFlipped != snap.isFlipped
|
|
1600
|
+
val prevRotationCount = rotationCount
|
|
1601
|
+
|
|
1602
|
+
rotationCount = snap.rotationCount
|
|
1603
|
+
isFlipped = snap.isFlipped
|
|
1604
|
+
cumulativeRotationDeg = snap.cumulativeRotationDeg
|
|
1605
|
+
|
|
1606
|
+
val containerW = videoContainer.width.toFloat()
|
|
1607
|
+
val containerH = videoContainer.height.toFloat()
|
|
1608
|
+
val fitScale = if (rotationCount % 2 != 0 && containerW > 0 && containerH > 0) {
|
|
1609
|
+
minOf(containerW / containerH, containerH / containerW)
|
|
1610
|
+
} else {
|
|
1611
|
+
1f
|
|
1612
|
+
}
|
|
1613
|
+
val flipMul = if (isFlipped) -1f else 1f
|
|
1614
|
+
val targetSx = flipMul * fitScale
|
|
1615
|
+
|
|
1616
|
+
val onComplete = Runnable {
|
|
1617
|
+
if (snap.isCropActive) {
|
|
1618
|
+
isCropActive = true
|
|
1619
|
+
cropBtn.setColorFilter(Color.WHITE, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
1620
|
+
showCropOverlayImmediate()
|
|
1621
|
+
updateCropAllowedRect()
|
|
1622
|
+
val norm = snap.cropNormalized
|
|
1623
|
+
if (norm != null) setCropFromNormalized(norm) else cropOverlay?.resetCrop()
|
|
1624
|
+
} else {
|
|
1625
|
+
isCropActive = false
|
|
1626
|
+
cropBtn.setColorFilter(
|
|
1627
|
+
Color.argb(128, 255, 255, 255),
|
|
1628
|
+
android.graphics.PorterDuff.Mode.SRC_IN
|
|
1629
|
+
)
|
|
1630
|
+
hideCropOverlayImmediate()
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
mVideoView.animate().cancel()
|
|
1635
|
+
|
|
1636
|
+
if (flipChanging) {
|
|
1637
|
+
val oddRotation = prevRotationCount % 2 != 0
|
|
1638
|
+
if (oddRotation) {
|
|
1639
|
+
mVideoView.animate()
|
|
1640
|
+
.scaleY(0f)
|
|
1641
|
+
.setDuration(125)
|
|
1642
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1643
|
+
.withEndAction {
|
|
1644
|
+
mVideoView.rotation = cumulativeRotationDeg
|
|
1645
|
+
mVideoView.scaleX = targetSx
|
|
1646
|
+
mVideoView.animate()
|
|
1647
|
+
.scaleY(fitScale)
|
|
1648
|
+
.setDuration(125)
|
|
1649
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1650
|
+
.withEndAction { onComplete.run() }
|
|
1651
|
+
.start()
|
|
1652
|
+
}
|
|
1653
|
+
.start()
|
|
1654
|
+
} else {
|
|
1655
|
+
mVideoView.animate()
|
|
1656
|
+
.scaleX(0f)
|
|
1657
|
+
.setDuration(125)
|
|
1658
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1659
|
+
.withEndAction {
|
|
1660
|
+
mVideoView.rotation = cumulativeRotationDeg
|
|
1661
|
+
mVideoView.animate()
|
|
1662
|
+
.scaleX(targetSx)
|
|
1663
|
+
.setDuration(125)
|
|
1664
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1665
|
+
.withEndAction { onComplete.run() }
|
|
1666
|
+
.start()
|
|
1667
|
+
}
|
|
1668
|
+
.start()
|
|
1669
|
+
}
|
|
1670
|
+
} else {
|
|
1671
|
+
mVideoView.animate()
|
|
1672
|
+
.scaleX(targetSx)
|
|
1673
|
+
.scaleY(fitScale)
|
|
1674
|
+
.rotation(cumulativeRotationDeg)
|
|
1675
|
+
.setDuration(250)
|
|
1676
|
+
.setInterpolator(DecelerateInterpolator())
|
|
1677
|
+
.withEndAction { onComplete.run() }
|
|
1678
|
+
.start()
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
private fun updateUndoRedoButtons() {
|
|
1683
|
+
val dimmed = Color.argb(128, 255, 255, 255)
|
|
1684
|
+
val active = Color.WHITE
|
|
1685
|
+
undoBtn.isEnabled = undoStack.isNotEmpty()
|
|
1686
|
+
undoBtn.setColorFilter(
|
|
1687
|
+
if (undoStack.isNotEmpty()) active else dimmed,
|
|
1688
|
+
android.graphics.PorterDuff.Mode.SRC_IN
|
|
1689
|
+
)
|
|
1690
|
+
redoBtn.isEnabled = redoStack.isNotEmpty()
|
|
1691
|
+
redoBtn.setColorFilter(
|
|
1692
|
+
if (redoStack.isNotEmpty()) active else dimmed,
|
|
1693
|
+
android.graphics.PorterDuff.Mode.SRC_IN
|
|
1694
|
+
)
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// endregion
|
|
1170
1698
|
}
|