react-native-video-trim 6.1.0 → 6.2.1
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/android/src/main/java/com/videotrim/enums/{ErrorCode.java → ErrorCode.kt} +2 -2
- package/android/src/main/java/com/videotrim/interfaces/IVideoTrimmerView.kt +5 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.kt +16 -0
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.kt +84 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.kt +129 -0
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.kt +163 -0
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.kt +1170 -0
- package/ios/VideoTrimmer.swift +102 -3
- package/ios/VideoTrimmerViewController.swift +18 -0
- package/package.json +2 -2
- package/android/src/main/java/com/videotrim/interfaces/IVideoTrimmerView.java +0 -5
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +0 -19
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +0 -92
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +0 -147
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +0 -171
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +0 -1248
|
@@ -0,0 +1,1170 @@
|
|
|
1
|
+
package com.videotrim.widgets
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.pm.ActivityInfo
|
|
5
|
+
import android.content.res.Configuration
|
|
6
|
+
import android.graphics.Color
|
|
7
|
+
import android.graphics.drawable.GradientDrawable
|
|
8
|
+
import android.media.MediaMetadataRetriever
|
|
9
|
+
import android.media.MediaPlayer
|
|
10
|
+
import android.net.Uri
|
|
11
|
+
import android.os.Build
|
|
12
|
+
import android.os.Handler
|
|
13
|
+
import android.os.VibrationEffect
|
|
14
|
+
import android.os.Vibrator
|
|
15
|
+
import android.util.AttributeSet
|
|
16
|
+
import android.util.Log
|
|
17
|
+
import android.util.TypedValue
|
|
18
|
+
import android.view.GestureDetector
|
|
19
|
+
import android.view.LayoutInflater
|
|
20
|
+
import android.view.MotionEvent
|
|
21
|
+
import android.view.View
|
|
22
|
+
import android.widget.FrameLayout
|
|
23
|
+
import android.widget.ImageView
|
|
24
|
+
import android.widget.LinearLayout
|
|
25
|
+
import android.widget.ProgressBar
|
|
26
|
+
import android.widget.RelativeLayout
|
|
27
|
+
import android.widget.TextView
|
|
28
|
+
import android.widget.VideoView
|
|
29
|
+
|
|
30
|
+
import androidx.appcompat.app.AlertDialog
|
|
31
|
+
|
|
32
|
+
import com.arthenica.ffmpegkit.FFmpegSession
|
|
33
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
34
|
+
import com.facebook.react.bridge.ReadableMap
|
|
35
|
+
import com.facebook.react.bridge.UiThreadUtil.runOnUiThread
|
|
36
|
+
import com.videotrim.R
|
|
37
|
+
import com.videotrim.enums.ErrorCode
|
|
38
|
+
import com.videotrim.interfaces.IVideoTrimmerView
|
|
39
|
+
import com.videotrim.interfaces.VideoTrimListener
|
|
40
|
+
import com.videotrim.utils.MediaMetadataUtil
|
|
41
|
+
import com.videotrim.utils.StorageUtil
|
|
42
|
+
import com.videotrim.utils.VideoTrimmerUtil
|
|
43
|
+
import com.videotrim.utils.VideoTrimmerUtil.RECYCLER_VIEW_PADDING
|
|
44
|
+
import com.videotrim.utils.VideoTrimmerUtil.VIDEO_FRAMES_WIDTH
|
|
45
|
+
|
|
46
|
+
import iknow.android.utils.DeviceUtil
|
|
47
|
+
import iknow.android.utils.thread.BackgroundExecutor
|
|
48
|
+
import iknow.android.utils.thread.UiThreadExecutor
|
|
49
|
+
|
|
50
|
+
import java.io.IOException
|
|
51
|
+
import java.util.Locale
|
|
52
|
+
import androidx.core.graphics.toColorInt
|
|
53
|
+
|
|
54
|
+
class VideoTrimmerView(
|
|
55
|
+
context: ReactApplicationContext,
|
|
56
|
+
config: ReadableMap?,
|
|
57
|
+
attrs: AttributeSet? = null,
|
|
58
|
+
defStyleAttr: Int = 0
|
|
59
|
+
) : FrameLayout(context, attrs, defStyleAttr), IVideoTrimmerView {
|
|
60
|
+
|
|
61
|
+
companion object {
|
|
62
|
+
private val TAG: String = VideoTrimmerView::class.java.simpleName
|
|
63
|
+
private const val TIMING_UPDATE_INTERVAL = 30L
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private var mContext: ReactApplicationContext = context
|
|
67
|
+
private lateinit var mVideoView: VideoView
|
|
68
|
+
|
|
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
|
+
private var mediaPlayer: MediaPlayer? = null
|
|
73
|
+
private lateinit var mPlayView: ImageView
|
|
74
|
+
private lateinit var mThumbnailContainer: LinearLayout
|
|
75
|
+
private var mSourceUri: Uri? = null
|
|
76
|
+
private lateinit var mOnTrimVideoListener: VideoTrimListener
|
|
77
|
+
private var mDuration = 0
|
|
78
|
+
private var mMaxDuration = Long.MAX_VALUE
|
|
79
|
+
private var mMinDuration = VideoTrimmerUtil.MIN_SHOOT_DURATION
|
|
80
|
+
|
|
81
|
+
private val mTimingHandler = Handler()
|
|
82
|
+
private var mTimingRunnable: Runnable? = null
|
|
83
|
+
private lateinit var currentTimeText: TextView
|
|
84
|
+
private lateinit var startTimeText: TextView
|
|
85
|
+
private lateinit var endTimeText: TextView
|
|
86
|
+
private lateinit var progressIndicator: View
|
|
87
|
+
private lateinit var trimmerContainer: View
|
|
88
|
+
// background of the trimmer container, its width never changes
|
|
89
|
+
// this is to make sure when we calculate position of the progress indicator, we don't need to consider the width of the trimmer container
|
|
90
|
+
private lateinit var trimmerContainerBg: View
|
|
91
|
+
private lateinit var leadingHandle: FrameLayout
|
|
92
|
+
private lateinit var trailingHandle: View
|
|
93
|
+
private lateinit var leadingOverlay: View
|
|
94
|
+
private lateinit var trailingOverlay: View
|
|
95
|
+
private lateinit var trimmerContainerWrapper: RelativeLayout
|
|
96
|
+
|
|
97
|
+
private var startTime = 0L
|
|
98
|
+
private var endTime = 0L
|
|
99
|
+
private var enableRotation = false
|
|
100
|
+
private var rotationAngle = 0.0
|
|
101
|
+
private var zoomOnWaitingDuration = 5000L
|
|
102
|
+
|
|
103
|
+
private var vibrator: Vibrator? = null
|
|
104
|
+
private var didClampWhilePanning = false
|
|
105
|
+
|
|
106
|
+
// zoom
|
|
107
|
+
private var isZoomedIn = false
|
|
108
|
+
private val zoomWaitTimer = Handler()
|
|
109
|
+
private var zoomRunnable: Runnable? = null
|
|
110
|
+
private var zoomedInRangeStart = 0L
|
|
111
|
+
private var zoomedInRangeDuration = 0L
|
|
112
|
+
private var isTrimmingLeading = false
|
|
113
|
+
|
|
114
|
+
// range drag
|
|
115
|
+
private var isRangeDragging = false
|
|
116
|
+
private var rangeDragInitialRawX = 0f
|
|
117
|
+
private var rangeDragInitialStartTime = 0L
|
|
118
|
+
private var rangeDragInitialEndTime = 0L
|
|
119
|
+
private lateinit var rangeDragGestureDetector: GestureDetector
|
|
120
|
+
|
|
121
|
+
// thumbnail caching for zoom functionality
|
|
122
|
+
private val cachedFullViewThumbnails = mutableListOf<ImageView>()
|
|
123
|
+
@Volatile
|
|
124
|
+
private var isGeneratingThumbnails = false
|
|
125
|
+
|
|
126
|
+
private var mediaMetadataRetriever: MediaMetadataRetriever? = null
|
|
127
|
+
private lateinit var loadingIndicator: ProgressBar
|
|
128
|
+
private lateinit var saveBtn: TextView
|
|
129
|
+
private lateinit var cancelBtn: TextView
|
|
130
|
+
private lateinit var audioBannerView: FrameLayout
|
|
131
|
+
private var isVideoType = true
|
|
132
|
+
private lateinit var failToLoadBtn: ImageView
|
|
133
|
+
|
|
134
|
+
private var mOutputExt = "mp4"
|
|
135
|
+
private var enableHapticFeedback = true
|
|
136
|
+
private var autoplay = false
|
|
137
|
+
private var jumpToPositionOnLoad = 0L
|
|
138
|
+
private lateinit var headerView: FrameLayout
|
|
139
|
+
private lateinit var headerText: TextView
|
|
140
|
+
private var ffmpegSession: FFmpegSession? = null
|
|
141
|
+
private var alertOnFailToLoad = true
|
|
142
|
+
private var alertOnFailTitle = "Error"
|
|
143
|
+
private var alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection"
|
|
144
|
+
private var alertOnFailCloseText = "Close"
|
|
145
|
+
private var currentSelectedhandle: View? = null
|
|
146
|
+
|
|
147
|
+
private lateinit var trimmerView: RelativeLayout
|
|
148
|
+
|
|
149
|
+
private var trimmerColor = context.getString(R.string.trim_color).toColorInt()
|
|
150
|
+
private var handleIconColor = Color.BLACK
|
|
151
|
+
private lateinit var leadingChevron: ImageView
|
|
152
|
+
private lateinit var trailingChevron: ImageView
|
|
153
|
+
|
|
154
|
+
init {
|
|
155
|
+
init(context, config)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private fun init(context: ReactApplicationContext, config: ReadableMap?) {
|
|
159
|
+
mContext = context
|
|
160
|
+
|
|
161
|
+
context.currentActivity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
162
|
+
LayoutInflater.from(context).inflate(R.layout.video_trimmer_view, this, true)
|
|
163
|
+
vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator
|
|
164
|
+
|
|
165
|
+
initializeViews()
|
|
166
|
+
if (config != null) configure(config)
|
|
167
|
+
setUpListeners()
|
|
168
|
+
initRangeDragDetector()
|
|
169
|
+
setProgressIndicatorTouchListener()
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun initRangeDragDetector() {
|
|
173
|
+
rangeDragGestureDetector = GestureDetector(mContext, object : GestureDetector.SimpleOnGestureListener() {
|
|
174
|
+
override fun onLongPress(e: MotionEvent) {
|
|
175
|
+
isRangeDragging = true
|
|
176
|
+
rangeDragInitialRawX = e.rawX
|
|
177
|
+
rangeDragInitialStartTime = startTime
|
|
178
|
+
rangeDragInitialEndTime = endTime
|
|
179
|
+
playHapticFeedback(true)
|
|
180
|
+
fadeOutProgressIndicator()
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
private fun initializeViews() {
|
|
186
|
+
mThumbnailContainer = findViewById(R.id.thumbnailContainer)
|
|
187
|
+
mVideoView = findViewById(R.id.video_loader)
|
|
188
|
+
mPlayView = findViewById(R.id.icon_video_play)
|
|
189
|
+
startTimeText = findViewById(R.id.startTime)
|
|
190
|
+
currentTimeText = findViewById(R.id.currentTime)
|
|
191
|
+
endTimeText = findViewById(R.id.endTime)
|
|
192
|
+
progressIndicator = findViewById(R.id.progressIndicator)
|
|
193
|
+
trimmerContainer = findViewById(R.id.trimmerContainer)
|
|
194
|
+
trimmerContainerBg = findViewById(R.id.trimmerContainerBg)
|
|
195
|
+
leadingHandle = findViewById(R.id.leadingHandle)
|
|
196
|
+
trailingHandle = findViewById(R.id.trailingHandle)
|
|
197
|
+
leadingOverlay = findViewById(R.id.leadingOverlay)
|
|
198
|
+
trailingOverlay = findViewById(R.id.trailingOverlay)
|
|
199
|
+
|
|
200
|
+
trimmerContainerWrapper = findViewById(R.id.trimmerContainerWrapper)
|
|
201
|
+
trimmerContainerWrapper.visibility = View.INVISIBLE
|
|
202
|
+
trimmerContainerWrapper.alpha = 0f
|
|
203
|
+
|
|
204
|
+
loadingIndicator = findViewById(R.id.loadingIndicator)
|
|
205
|
+
saveBtn = findViewById(R.id.saveBtn)
|
|
206
|
+
cancelBtn = findViewById(R.id.cancelBtn)
|
|
207
|
+
audioBannerView = findViewById(R.id.audioBannerView)
|
|
208
|
+
failToLoadBtn = findViewById(R.id.failToLoadBtn)
|
|
209
|
+
|
|
210
|
+
headerView = findViewById(R.id.headerView)
|
|
211
|
+
headerText = findViewById(R.id.headerText)
|
|
212
|
+
|
|
213
|
+
trimmerView = findViewById(R.id.trimmerView)
|
|
214
|
+
|
|
215
|
+
leadingChevron = findViewById(R.id.leadingChevron)
|
|
216
|
+
trailingChevron = findViewById(R.id.trailingChevron)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
fun initByURI(videoURI: Uri) {
|
|
220
|
+
mSourceUri = videoURI
|
|
221
|
+
|
|
222
|
+
if (isVideoType) {
|
|
223
|
+
mVideoView.setVideoURI(videoURI)
|
|
224
|
+
mVideoView.requestFocus()
|
|
225
|
+
|
|
226
|
+
mVideoView.setOnPreparedListener { mp ->
|
|
227
|
+
mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT)
|
|
228
|
+
mediaPlayer = mp
|
|
229
|
+
mediaPrepared()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
mVideoView.setOnErrorListener { mp, what, extra -> onFailToLoadMedia(mp, what, extra) }
|
|
233
|
+
mVideoView.setOnCompletionListener { mediaCompleted() }
|
|
234
|
+
} else {
|
|
235
|
+
mVideoView.visibility = View.GONE
|
|
236
|
+
audioBannerView.alpha = 0f
|
|
237
|
+
audioBannerView.visibility = View.VISIBLE
|
|
238
|
+
audioBannerView.animate().alpha(1f).setDuration(500).start()
|
|
239
|
+
|
|
240
|
+
mediaPlayer = MediaPlayer()
|
|
241
|
+
try {
|
|
242
|
+
mediaPlayer!!.setDataSource(videoURI.toString())
|
|
243
|
+
mediaPlayer!!.setOnPreparedListener { mediaPrepared() }
|
|
244
|
+
mediaPlayer!!.setOnCompletionListener { mediaCompleted() }
|
|
245
|
+
mediaPlayer!!.setOnErrorListener { mp, what, extra -> onFailToLoadMedia(mp, what, extra) }
|
|
246
|
+
mediaPlayer!!.prepareAsync()
|
|
247
|
+
} catch (e: IOException) {
|
|
248
|
+
e.printStackTrace()
|
|
249
|
+
mediaFailed()
|
|
250
|
+
mOnTrimVideoListener.onError("Error initializing audio player. Please try again.", ErrorCode.FAIL_TO_INITIALIZE_AUDIO_PLAYER)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private fun onFailToLoadMedia(mp: MediaPlayer, what: Int, extra: Int): Boolean {
|
|
256
|
+
mediaFailed()
|
|
257
|
+
mOnTrimVideoListener.onError("Error loading media file. Please try again.", ErrorCode.FAIL_TO_LOAD_MEDIA)
|
|
258
|
+
if (alertOnFailToLoad) {
|
|
259
|
+
val builder = AlertDialog.Builder(mContext.currentActivity!!)
|
|
260
|
+
builder.setMessage(alertOnFailMessage)
|
|
261
|
+
builder.setTitle(alertOnFailTitle)
|
|
262
|
+
builder.setCancelable(false)
|
|
263
|
+
builder.setPositiveButton(alertOnFailCloseText) { dialog, _ -> dialog.cancel() }
|
|
264
|
+
|
|
265
|
+
val alertDialog = builder.create()
|
|
266
|
+
alertDialog.show()
|
|
267
|
+
}
|
|
268
|
+
return true
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private fun startShootVideoThumbs(context: Context, totalThumbsCount: Int, startPosition: Long, endPosition: Long) {
|
|
272
|
+
mThumbnailContainer.removeAllViews()
|
|
273
|
+
cachedFullViewThumbnails.clear()
|
|
274
|
+
|
|
275
|
+
val containerContentWidth = mThumbnailContainer.width - mThumbnailContainer.paddingLeft - mThumbnailContainer.paddingRight
|
|
276
|
+
val effectiveWidth = if (containerContentWidth > 0) containerContentWidth else VideoTrimmerUtil.VIDEO_FRAMES_WIDTH
|
|
277
|
+
val baseThumbWidth = effectiveWidth / totalThumbsCount
|
|
278
|
+
val remainder = effectiveWidth % totalThumbsCount
|
|
279
|
+
|
|
280
|
+
VideoTrimmerUtil.shootVideoThumbInBackground(mediaMetadataRetriever!!, totalThumbsCount, startPosition, endPosition) { bitmap, interval ->
|
|
281
|
+
if (bitmap != null) {
|
|
282
|
+
runOnUiThread {
|
|
283
|
+
val index = mThumbnailContainer.childCount
|
|
284
|
+
val width = if (index < remainder) baseThumbWidth + 1 else baseThumbWidth
|
|
285
|
+
val layoutParams = LinearLayout.LayoutParams(width, LayoutParams.MATCH_PARENT)
|
|
286
|
+
|
|
287
|
+
val thumbImageView = ImageView(context)
|
|
288
|
+
thumbImageView.setImageBitmap(bitmap)
|
|
289
|
+
thumbImageView.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
290
|
+
thumbImageView.layoutParams = layoutParams
|
|
291
|
+
mThumbnailContainer.addView(thumbImageView)
|
|
292
|
+
|
|
293
|
+
val cachedView = ImageView(context)
|
|
294
|
+
cachedView.setImageBitmap(bitmap)
|
|
295
|
+
cachedView.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
296
|
+
cachedView.layoutParams = LinearLayout.LayoutParams(width, LayoutParams.MATCH_PARENT)
|
|
297
|
+
cachedFullViewThumbnails.add(cachedView)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private fun mediaPrepared() {
|
|
304
|
+
mDuration = mediaPlayer!!.duration
|
|
305
|
+
mMaxDuration = mMaxDuration.coerceAtMost(mDuration.toLong())
|
|
306
|
+
|
|
307
|
+
if (isVideoType) {
|
|
308
|
+
mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString())
|
|
309
|
+
if (mediaMetadataRetriever == null) {
|
|
310
|
+
mOnTrimVideoListener.onError("Error when retrieving video info. Please try again.", ErrorCode.FAIL_TO_GET_VIDEO_INFO)
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
val bitmap = mediaMetadataRetriever!!.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
315
|
+
|
|
316
|
+
if (bitmap != null) {
|
|
317
|
+
val bitmapHeight = if (bitmap.height > 0) bitmap.height else VideoTrimmerUtil.THUMB_HEIGHT
|
|
318
|
+
val bitmapWidth = if (bitmap.width > 0) bitmap.width else VideoTrimmerUtil.THUMB_WIDTH
|
|
319
|
+
VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmapWidth / bitmapHeight
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
VideoTrimmerUtil.SCREEN_WIDTH_FULL = getScreenWidthInPortraitMode()
|
|
323
|
+
VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2
|
|
324
|
+
VideoTrimmerUtil.MAX_COUNT_RANGE = if (VideoTrimmerUtil.mThumbWidth != 0)
|
|
325
|
+
maxOf(VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth, VideoTrimmerUtil.MAX_COUNT_RANGE)
|
|
326
|
+
else
|
|
327
|
+
VideoTrimmerUtil.MAX_COUNT_RANGE
|
|
328
|
+
|
|
329
|
+
startShootVideoThumbs(mContext, VideoTrimmerUtil.MAX_COUNT_RANGE, 0, mDuration.toLong())
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
endTime = if (mMaxDuration < mDuration) mMaxDuration else mDuration.toLong()
|
|
333
|
+
updateHandlePositions()
|
|
334
|
+
|
|
335
|
+
loadingIndicator.visibility = View.GONE
|
|
336
|
+
mPlayView.visibility = View.VISIBLE
|
|
337
|
+
saveBtn.visibility = View.VISIBLE
|
|
338
|
+
|
|
339
|
+
if (jumpToPositionOnLoad > 0) {
|
|
340
|
+
seekTo(if (jumpToPositionOnLoad > mDuration) mDuration.toLong() else jumpToPositionOnLoad, true)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (autoplay) {
|
|
344
|
+
playOrPause()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
mOnTrimVideoListener.onLoad(mDuration)
|
|
348
|
+
ignoreSystemGestureForView(trimmerView)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private fun updateGradientColors(startColor: Int, endColor: Int) {
|
|
352
|
+
val gradientDrawable = GradientDrawable().apply {
|
|
353
|
+
shape = GradientDrawable.RECTANGLE
|
|
354
|
+
cornerRadius = 6f
|
|
355
|
+
colors = intArrayOf(startColor, endColor)
|
|
356
|
+
orientation = GradientDrawable.Orientation.LEFT_RIGHT
|
|
357
|
+
}
|
|
358
|
+
mThumbnailContainer.background = gradientDrawable
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private fun mediaFailed() {
|
|
362
|
+
loadingIndicator.visibility = View.GONE
|
|
363
|
+
failToLoadBtn.visibility = View.VISIBLE
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
private fun updateHandlePositions() {
|
|
367
|
+
val leadingHandleX = positionForTime(startTime)
|
|
368
|
+
val trailingHandleX = positionForTime(endTime)
|
|
369
|
+
|
|
370
|
+
leadingHandle.x = leadingHandleX
|
|
371
|
+
trailingHandle.x = trailingHandleX + trailingHandle.width
|
|
372
|
+
|
|
373
|
+
updateTrimmerContainerWidth()
|
|
374
|
+
updateCurrentTime(false)
|
|
375
|
+
|
|
376
|
+
trimmerContainerWrapper.visibility = View.VISIBLE
|
|
377
|
+
trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start()
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
private fun mediaCompleted() {
|
|
381
|
+
onMediaPause()
|
|
382
|
+
// when mediaCompleted is called, the endTime may not be exactly at the end of the video (can be slightly before), therefore we should seek to exact position on ended
|
|
383
|
+
seekTo(endTime, true)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
private fun playOrPause() {
|
|
387
|
+
val player = mediaPlayer ?: return
|
|
388
|
+
if (player.isPlaying) {
|
|
389
|
+
onMediaPause()
|
|
390
|
+
} else {
|
|
391
|
+
if (player.currentPosition >= endTime) {
|
|
392
|
+
seekTo(startTime, true)
|
|
393
|
+
}
|
|
394
|
+
player.start()
|
|
395
|
+
startTimingRunnable()
|
|
396
|
+
}
|
|
397
|
+
setPlayPauseViewIcon(player.isPlaying)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
fun onMediaPause() {
|
|
401
|
+
mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
|
|
402
|
+
val player = mediaPlayer ?: return
|
|
403
|
+
if (player.isPlaying) {
|
|
404
|
+
player.pause()
|
|
405
|
+
}
|
|
406
|
+
setPlayPauseViewIcon(false)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
fun setOnTrimVideoListener(onTrimVideoListener: VideoTrimListener) {
|
|
410
|
+
mOnTrimVideoListener = onTrimVideoListener
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private fun setUpListeners() {
|
|
414
|
+
cancelBtn.setOnClickListener { mOnTrimVideoListener.onCancel() }
|
|
415
|
+
saveBtn.setOnClickListener { mOnTrimVideoListener.onSave() }
|
|
416
|
+
mPlayView.setOnClickListener { playOrPause() }
|
|
417
|
+
setHandleTouchListener(leadingHandle, true)
|
|
418
|
+
setHandleTouchListener(trailingHandle, false)
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
fun onSaveClicked() {
|
|
422
|
+
onMediaPause()
|
|
423
|
+
ffmpegSession = VideoTrimmerUtil.trim(
|
|
424
|
+
mSourceUri.toString(),
|
|
425
|
+
StorageUtil.getOutputPath(mContext, mOutputExt),
|
|
426
|
+
mDuration,
|
|
427
|
+
startTime,
|
|
428
|
+
endTime,
|
|
429
|
+
enableRotation,
|
|
430
|
+
rotationAngle,
|
|
431
|
+
mOnTrimVideoListener
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
fun onCancelTrimClicked() {
|
|
436
|
+
if (ffmpegSession != null) {
|
|
437
|
+
ffmpegSession!!.cancel()
|
|
438
|
+
} else {
|
|
439
|
+
mOnTrimVideoListener.onCancelTrim()
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
private fun seekTo(msec: Long, needUpdateProgress: Boolean) {
|
|
444
|
+
val player = mediaPlayer ?: return
|
|
445
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
446
|
+
player.seekTo(msec, MediaPlayer.SEEK_CLOSEST)
|
|
447
|
+
} else {
|
|
448
|
+
player.seekTo(msec.toInt())
|
|
449
|
+
}
|
|
450
|
+
updateCurrentTime(needUpdateProgress)
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
private fun setPlayPauseViewIcon(isPlaying: Boolean) {
|
|
454
|
+
mPlayView.setImageResource(if (isPlaying) R.drawable.pause_fill else R.drawable.play_fill)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
override fun onDetachedFromWindow() {
|
|
458
|
+
super.onDetachedFromWindow()
|
|
459
|
+
mContext.currentActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
override fun onDestroy() {
|
|
463
|
+
isGeneratingThumbnails = false
|
|
464
|
+
BackgroundExecutor.cancelAll("", true)
|
|
465
|
+
UiThreadExecutor.cancelAll("")
|
|
466
|
+
mContext.currentActivity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
467
|
+
mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
|
|
468
|
+
zoomRunnable?.let { zoomWaitTimer.removeCallbacks(it) }
|
|
469
|
+
|
|
470
|
+
cachedFullViewThumbnails.clear()
|
|
471
|
+
|
|
472
|
+
try {
|
|
473
|
+
mediaMetadataRetriever?.release()
|
|
474
|
+
} catch (e: Exception) {
|
|
475
|
+
e.printStackTrace()
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
try {
|
|
479
|
+
mediaPlayer?.stop()
|
|
480
|
+
mediaPlayer?.release()
|
|
481
|
+
} catch (e: IllegalStateException) {
|
|
482
|
+
e.printStackTrace()
|
|
483
|
+
Log.d(TAG, "onDestroy mediaPlayer is already released")
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
private fun getScreenWidthInPortraitMode(): Int {
|
|
488
|
+
val screenWidth = DeviceUtil.getDeviceWidth()
|
|
489
|
+
val screenHeight = DeviceUtil.getDeviceHeight()
|
|
490
|
+
val currentOrientation = resources.configuration.orientation
|
|
491
|
+
return if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) screenHeight else screenWidth
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
private fun configure(config: ReadableMap) {
|
|
495
|
+
if (config.hasKey("maxDuration") && config.getDouble("maxDuration") > 0) {
|
|
496
|
+
mMaxDuration = maxOf(0, config.getDouble("maxDuration").toLong())
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (config.hasKey("minDuration") && config.getDouble("minDuration") > 0) {
|
|
500
|
+
mMinDuration = maxOf(1000L, config.getDouble("minDuration").toLong())
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
cancelBtn.text = config.getString("cancelButtonText")
|
|
504
|
+
saveBtn.text = config.getString("saveButtonText")
|
|
505
|
+
isVideoType = config.hasKey("type") && config.getString("type") == "video"
|
|
506
|
+
println("isVideoType: $isVideoType")
|
|
507
|
+
|
|
508
|
+
mOutputExt = if (config.hasKey("outputExt")) config.getString("outputExt") ?: "mp4" else "mp4"
|
|
509
|
+
if (!isVideoType) {
|
|
510
|
+
mOutputExt = "wav"
|
|
511
|
+
}
|
|
512
|
+
enableHapticFeedback = config.hasKey("enableHapticFeedback") && config.getBoolean("enableHapticFeedback")
|
|
513
|
+
autoplay = config.hasKey("autoplay") && config.getBoolean("autoplay")
|
|
514
|
+
|
|
515
|
+
if (config.hasKey("jumpToPositionOnLoad") && config.getDouble("jumpToPositionOnLoad") > 0) {
|
|
516
|
+
jumpToPositionOnLoad = maxOf(0, (config.getDouble("jumpToPositionOnLoad") * 1000L).toLong())
|
|
517
|
+
}
|
|
518
|
+
headerText.text = if (config.hasKey("headerText")) config.getString("headerText") ?: "" else ""
|
|
519
|
+
|
|
520
|
+
var textSize = if (config.hasKey("headerTextSize")) config.getInt("headerTextSize") else 16
|
|
521
|
+
if (textSize < 0) {
|
|
522
|
+
textSize = 16
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
headerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize.toFloat())
|
|
526
|
+
headerText.setTextColor(if (config.hasKey("headerTextColor")) config.getInt("headerTextColor") else Color.BLACK)
|
|
527
|
+
|
|
528
|
+
headerView.visibility = View.VISIBLE
|
|
529
|
+
alertOnFailToLoad = config.hasKey("alertOnFailToLoad") && config.getBoolean("alertOnFailToLoad")
|
|
530
|
+
alertOnFailTitle = if (config.hasKey("alertOnFailTitle")) config.getString("alertOnFailTitle") ?: "Error" else "Error"
|
|
531
|
+
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
|
+
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
|
+
|
|
536
|
+
if (config.hasKey("zoomOnWaitingDuration") && config.getDouble("zoomOnWaitingDuration") > 0) {
|
|
537
|
+
zoomOnWaitingDuration = config.getDouble("zoomOnWaitingDuration").toLong()
|
|
538
|
+
Log.d(TAG, "Configured zoom on waiting duration: ${zoomOnWaitingDuration / 1000.0} seconds")
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
trimmerColor = if (config.hasKey("trimmerColor")) config.getInt("trimmerColor") else context.getString(
|
|
542
|
+
R.string.trim_color
|
|
543
|
+
).toColorInt()
|
|
544
|
+
handleIconColor = if (config.hasKey("handleIconColor")) config.getInt("handleIconColor") else Color.BLACK
|
|
545
|
+
|
|
546
|
+
applyTrimmerColor()
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
private fun applyTrimmerColor() {
|
|
550
|
+
val borderDrawable = GradientDrawable().apply {
|
|
551
|
+
shape = GradientDrawable.RECTANGLE
|
|
552
|
+
setColor(Color.TRANSPARENT)
|
|
553
|
+
setStroke(dpToPx(4), trimmerColor)
|
|
554
|
+
}
|
|
555
|
+
trimmerContainer.background = borderDrawable
|
|
556
|
+
|
|
557
|
+
val leadingHandleDrawable = GradientDrawable().apply {
|
|
558
|
+
shape = GradientDrawable.RECTANGLE
|
|
559
|
+
setColor(trimmerColor)
|
|
560
|
+
cornerRadii = floatArrayOf(dpToPx(6).toFloat(), dpToPx(6).toFloat(), 0f, 0f, 0f, 0f, dpToPx(6).toFloat(), dpToPx(6).toFloat())
|
|
561
|
+
}
|
|
562
|
+
leadingHandle.background = leadingHandleDrawable
|
|
563
|
+
|
|
564
|
+
val trailingHandleDrawable = GradientDrawable().apply {
|
|
565
|
+
shape = GradientDrawable.RECTANGLE
|
|
566
|
+
setColor(trimmerColor)
|
|
567
|
+
cornerRadii = floatArrayOf(0f, 0f, dpToPx(6).toFloat(), dpToPx(6).toFloat(), dpToPx(6).toFloat(), dpToPx(6).toFloat(), 0f, 0f)
|
|
568
|
+
}
|
|
569
|
+
trailingHandle.background = trailingHandleDrawable
|
|
570
|
+
|
|
571
|
+
leadingChevron.setColorFilter(handleIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
572
|
+
trailingChevron.setColorFilter(handleIconColor, android.graphics.PorterDuff.Mode.SRC_IN)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private fun dpToPx(dp: Int): Int {
|
|
576
|
+
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), resources.displayMetrics).toInt()
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
private fun startTimingRunnable() {
|
|
580
|
+
mTimingRunnable = object : Runnable {
|
|
581
|
+
override fun run() {
|
|
582
|
+
try {
|
|
583
|
+
val currentPosition = mediaPlayer!!.currentPosition
|
|
584
|
+
if (currentPosition >= endTime) {
|
|
585
|
+
onMediaPause()
|
|
586
|
+
seekTo(endTime, true)
|
|
587
|
+
} else {
|
|
588
|
+
updateCurrentTime(true)
|
|
589
|
+
mTimingHandler.postDelayed(this, TIMING_UPDATE_INTERVAL)
|
|
590
|
+
}
|
|
591
|
+
} catch (e: IllegalStateException) {
|
|
592
|
+
e.printStackTrace()
|
|
593
|
+
mTimingRunnable?.let { mTimingHandler.removeCallbacks(it) }
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
mTimingHandler.postDelayed(mTimingRunnable!!, TIMING_UPDATE_INTERVAL)
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
private fun updateCurrentTime(needUpdateProgress: Boolean) {
|
|
601
|
+
var currentPosition = mediaPlayer!!.currentPosition
|
|
602
|
+
val duration = mDuration
|
|
603
|
+
|
|
604
|
+
when {
|
|
605
|
+
currentPosition >= duration - 100 -> currentPosition = duration
|
|
606
|
+
currentPosition >= endTime - 100 -> currentPosition = endTime.toInt()
|
|
607
|
+
currentPosition <= startTime + 100 -> currentPosition = startTime.toInt()
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
currentTimeText.text = formatTime(currentPosition)
|
|
611
|
+
startTimeText.text = formatTime(startTime.toInt())
|
|
612
|
+
endTimeText.text = formatTime(endTime.toInt())
|
|
613
|
+
|
|
614
|
+
if (needUpdateProgress) {
|
|
615
|
+
val indicatorPosition: Float
|
|
616
|
+
|
|
617
|
+
if (isZoomedIn) {
|
|
618
|
+
val visibleRangeStart = getVisibleRangeStart()
|
|
619
|
+
val visibleRangeDuration = getVisibleRangeDuration()
|
|
620
|
+
|
|
621
|
+
var clampedPosition = currentPosition
|
|
622
|
+
if (clampedPosition < visibleRangeStart || clampedPosition > visibleRangeStart + visibleRangeDuration) {
|
|
623
|
+
clampedPosition = maxOf(visibleRangeStart, minOf(visibleRangeStart + visibleRangeDuration, currentPosition.toLong())).toInt()
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
val ratio = if (visibleRangeDuration > 0) (clampedPosition - visibleRangeStart).toFloat() / visibleRangeDuration else 0f
|
|
627
|
+
indicatorPosition = ratio * (trimmerContainerBg.width - progressIndicator.width) + leadingHandle.width
|
|
628
|
+
} else {
|
|
629
|
+
indicatorPosition = if (mDuration > 0)
|
|
630
|
+
currentPosition.toFloat() / mDuration * (trimmerContainerBg.width - progressIndicator.width) + leadingHandle.width
|
|
631
|
+
else
|
|
632
|
+
leadingHandle.width.toFloat()
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
636
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
637
|
+
val boundedPosition = maxOf(leftBoundary, minOf(rightBoundary, indicatorPosition))
|
|
638
|
+
|
|
639
|
+
when (currentSelectedhandle) {
|
|
640
|
+
leadingHandle -> progressIndicator.x = maxOf(leftBoundary, boundedPosition)
|
|
641
|
+
trailingHandle -> progressIndicator.x = minOf(rightBoundary, boundedPosition)
|
|
642
|
+
else -> progressIndicator.x = boundedPosition
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
private fun formatTime(milliseconds: Int): String {
|
|
648
|
+
val totalSeconds = milliseconds / 1000
|
|
649
|
+
val minutes = totalSeconds / 60
|
|
650
|
+
val seconds = totalSeconds % 60
|
|
651
|
+
val millis = milliseconds % 1000
|
|
652
|
+
return String.format(Locale.getDefault(), "%d:%02d.%03d", minutes, seconds, millis)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
@Suppress("ClickableViewAccessibility")
|
|
656
|
+
private fun setProgressIndicatorTouchListener() {
|
|
657
|
+
trimmerContainerBg.setOnTouchListener { view, event ->
|
|
658
|
+
rangeDragGestureDetector.onTouchEvent(event)
|
|
659
|
+
|
|
660
|
+
when (event.action) {
|
|
661
|
+
MotionEvent.ACTION_DOWN -> {
|
|
662
|
+
isRangeDragging = false
|
|
663
|
+
didClampWhilePanning = false
|
|
664
|
+
onMediaPause()
|
|
665
|
+
onTrimmerContainerPanned(event)
|
|
666
|
+
playHapticFeedback(true)
|
|
667
|
+
}
|
|
668
|
+
MotionEvent.ACTION_MOVE -> {
|
|
669
|
+
if (isRangeDragging) {
|
|
670
|
+
onRangeDrag(event)
|
|
671
|
+
} else {
|
|
672
|
+
onTrimmerContainerPanned(event)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
MotionEvent.ACTION_UP -> {
|
|
676
|
+
if (isRangeDragging) {
|
|
677
|
+
isRangeDragging = false
|
|
678
|
+
fadeInProgressIndicator()
|
|
679
|
+
updateCurrentTime(true)
|
|
680
|
+
}
|
|
681
|
+
view.performClick()
|
|
682
|
+
}
|
|
683
|
+
else -> return@setOnTouchListener false
|
|
684
|
+
}
|
|
685
|
+
true
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
private fun onTrimmerContainerPanned(event: MotionEvent) {
|
|
690
|
+
var newX = event.rawX
|
|
691
|
+
var didClamp = false
|
|
692
|
+
|
|
693
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
694
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
695
|
+
|
|
696
|
+
newX = maxOf(leftBoundary, newX)
|
|
697
|
+
newX = minOf(rightBoundary, newX)
|
|
698
|
+
|
|
699
|
+
if (newX <= leftBoundary || newX >= rightBoundary) {
|
|
700
|
+
didClamp = true
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (didClamp && !didClampWhilePanning) {
|
|
704
|
+
playHapticFeedback(false)
|
|
705
|
+
}
|
|
706
|
+
didClampWhilePanning = didClamp
|
|
707
|
+
|
|
708
|
+
progressIndicator.x = newX
|
|
709
|
+
|
|
710
|
+
val indicatorPosition = newX - trimmerContainerBg.x
|
|
711
|
+
|
|
712
|
+
val indicatorPositionPercent: Float
|
|
713
|
+
val newVideoPosition: Long
|
|
714
|
+
|
|
715
|
+
if (isZoomedIn) {
|
|
716
|
+
indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.width - progressIndicator.width)
|
|
717
|
+
val visibleStart = getVisibleRangeStart()
|
|
718
|
+
val visibleDuration = getVisibleRangeDuration()
|
|
719
|
+
newVideoPosition = visibleStart + (indicatorPositionPercent * visibleDuration).toLong()
|
|
720
|
+
} else {
|
|
721
|
+
indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.width - progressIndicator.width)
|
|
722
|
+
newVideoPosition = (indicatorPositionPercent * mDuration).toLong()
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
seekTo(newVideoPosition, false)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
private fun onRangeDrag(event: MotionEvent) {
|
|
729
|
+
val deltaX = event.rawX - rangeDragInitialRawX
|
|
730
|
+
val containerWidth = trimmerContainerBg.width.toFloat()
|
|
731
|
+
if (containerWidth <= 0) return
|
|
732
|
+
|
|
733
|
+
val rangeDuration = rangeDragInitialEndTime - rangeDragInitialStartTime
|
|
734
|
+
val deltaTime = if (isZoomedIn) {
|
|
735
|
+
(deltaX / containerWidth * zoomedInRangeDuration).toLong()
|
|
736
|
+
} else {
|
|
737
|
+
(deltaX / containerWidth * mDuration).toLong()
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
var newStart = rangeDragInitialStartTime + deltaTime
|
|
741
|
+
var newEnd = newStart + rangeDuration
|
|
742
|
+
|
|
743
|
+
var didClamp = false
|
|
744
|
+
if (newStart < 0) {
|
|
745
|
+
newStart = 0
|
|
746
|
+
newEnd = rangeDuration
|
|
747
|
+
didClamp = true
|
|
748
|
+
}
|
|
749
|
+
if (newEnd > mDuration) {
|
|
750
|
+
newEnd = mDuration.toLong()
|
|
751
|
+
newStart = newEnd - rangeDuration
|
|
752
|
+
didClamp = true
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (didClamp && !didClampWhilePanning) {
|
|
756
|
+
playHapticFeedback(false)
|
|
757
|
+
}
|
|
758
|
+
didClampWhilePanning = didClamp
|
|
759
|
+
|
|
760
|
+
startTime = newStart
|
|
761
|
+
endTime = newEnd
|
|
762
|
+
updateHandlePositions()
|
|
763
|
+
seekTo(startTime, false)
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
@Suppress("ClickableViewAccessibility")
|
|
767
|
+
private fun setHandleTouchListener(handle: View, isLeading: Boolean) {
|
|
768
|
+
handle.setOnTouchListener { view, event ->
|
|
769
|
+
val draggingDisabled = mDuration < mMinDuration
|
|
770
|
+
when (event.action) {
|
|
771
|
+
MotionEvent.ACTION_DOWN -> {
|
|
772
|
+
currentSelectedhandle = handle
|
|
773
|
+
didClampWhilePanning = false
|
|
774
|
+
onMediaPause()
|
|
775
|
+
fadeOutProgressIndicator()
|
|
776
|
+
seekTo(if (isLeading) startTime else endTime, true)
|
|
777
|
+
playHapticFeedback(true)
|
|
778
|
+
isTrimmingLeading = isLeading
|
|
779
|
+
}
|
|
780
|
+
MotionEvent.ACTION_MOVE -> {
|
|
781
|
+
if (draggingDisabled) return@setOnTouchListener false
|
|
782
|
+
|
|
783
|
+
var didClamp = false
|
|
784
|
+
var newX = event.rawX - view.width.toFloat() / 2
|
|
785
|
+
|
|
786
|
+
if (isLeading) {
|
|
787
|
+
newX = maxOf(0f, minOf(newX, trailingHandle.x - view.width))
|
|
788
|
+
} else {
|
|
789
|
+
newX = minOf(trimmerContainerBg.width.toFloat() + view.width, maxOf(newX, leadingHandle.x + view.width))
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
view.x = newX
|
|
793
|
+
|
|
794
|
+
if (isLeading) {
|
|
795
|
+
val newStartTime = timeForPosition(newX)
|
|
796
|
+
val duration = endTime - newStartTime
|
|
797
|
+
when {
|
|
798
|
+
duration in mMinDuration..mMaxDuration -> {
|
|
799
|
+
startTime = newStartTime
|
|
800
|
+
val indicatorX = newX + view.width
|
|
801
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
802
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
803
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
804
|
+
}
|
|
805
|
+
duration < mMinDuration -> {
|
|
806
|
+
didClamp = true
|
|
807
|
+
startTime = endTime - mMinDuration
|
|
808
|
+
if (isZoomedIn) {
|
|
809
|
+
val indicatorX = newX + view.width
|
|
810
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
811
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
812
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
813
|
+
} else {
|
|
814
|
+
view.x = positionForTime(startTime)
|
|
815
|
+
val indicatorX = view.x + view.width
|
|
816
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
817
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
818
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
else -> {
|
|
822
|
+
didClamp = true
|
|
823
|
+
startTime = endTime - mMaxDuration
|
|
824
|
+
if (isZoomedIn) {
|
|
825
|
+
val indicatorX = newX + view.width
|
|
826
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
827
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
828
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
829
|
+
} else {
|
|
830
|
+
view.x = positionForTime(startTime)
|
|
831
|
+
val indicatorX = view.x + view.width
|
|
832
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
833
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
834
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
} else {
|
|
839
|
+
val newEndTime = timeForPosition(newX - view.width)
|
|
840
|
+
val duration = newEndTime - startTime
|
|
841
|
+
when {
|
|
842
|
+
duration in mMinDuration..mMaxDuration -> {
|
|
843
|
+
endTime = newEndTime
|
|
844
|
+
val indicatorX = newX - progressIndicator.width
|
|
845
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
846
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
847
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
848
|
+
}
|
|
849
|
+
duration < mMinDuration -> {
|
|
850
|
+
didClamp = true
|
|
851
|
+
endTime = startTime + mMinDuration
|
|
852
|
+
if (isZoomedIn) {
|
|
853
|
+
val indicatorX = newX - progressIndicator.width
|
|
854
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
855
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
856
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
857
|
+
} else {
|
|
858
|
+
view.x = positionForTime(endTime) + view.width
|
|
859
|
+
val indicatorX = view.x - progressIndicator.width
|
|
860
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
861
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
862
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
else -> {
|
|
866
|
+
didClamp = true
|
|
867
|
+
endTime = startTime + mMaxDuration
|
|
868
|
+
if (isZoomedIn) {
|
|
869
|
+
val indicatorX = newX - progressIndicator.width
|
|
870
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
871
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
872
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
873
|
+
} else {
|
|
874
|
+
view.x = positionForTime(endTime) + view.width
|
|
875
|
+
val indicatorX = view.x - progressIndicator.width
|
|
876
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
877
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
878
|
+
progressIndicator.x = maxOf(leftBoundary, minOf(rightBoundary, indicatorX))
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
if (didClamp && !didClampWhilePanning) {
|
|
885
|
+
playHapticFeedback(false)
|
|
886
|
+
}
|
|
887
|
+
didClampWhilePanning = didClamp
|
|
888
|
+
|
|
889
|
+
updateTrimmerContainerWidth()
|
|
890
|
+
seekTo(if (isLeading) startTime else endTime, false)
|
|
891
|
+
|
|
892
|
+
startZoomWaitTimer()
|
|
893
|
+
}
|
|
894
|
+
MotionEvent.ACTION_UP -> {
|
|
895
|
+
stopZoomIfNeeded()
|
|
896
|
+
fadeInProgressIndicator()
|
|
897
|
+
view.performClick()
|
|
898
|
+
}
|
|
899
|
+
else -> return@setOnTouchListener false
|
|
900
|
+
}
|
|
901
|
+
true
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
private fun fadeOutProgressIndicator() {
|
|
906
|
+
progressIndicator.animate().alpha(0f).setDuration(250).withEndAction { progressIndicator.visibility = View.INVISIBLE }.start()
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
private fun fadeInProgressIndicator() {
|
|
910
|
+
progressIndicator.visibility = View.VISIBLE
|
|
911
|
+
progressIndicator.animate().alpha(1f).setDuration(250).start()
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private fun updateTrimmerContainerWidth() {
|
|
915
|
+
val left = (leadingHandle.x + leadingHandle.width).toInt()
|
|
916
|
+
val right = trimmerContainerBg.width - kotlin.math.ceil(trailingHandle.x.toDouble()).toInt() + 2 * trailingHandle.width
|
|
917
|
+
|
|
918
|
+
val leadingOverlayParams = leadingOverlay.layoutParams as RelativeLayout.LayoutParams
|
|
919
|
+
leadingOverlayParams.width = left
|
|
920
|
+
leadingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT
|
|
921
|
+
leadingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_START)
|
|
922
|
+
leadingOverlay.layoutParams = leadingOverlayParams
|
|
923
|
+
|
|
924
|
+
val trailingOverlayParams = trailingOverlay.layoutParams as RelativeLayout.LayoutParams
|
|
925
|
+
trailingOverlayParams.width = right
|
|
926
|
+
trailingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT
|
|
927
|
+
trailingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_END)
|
|
928
|
+
trailingOverlay.layoutParams = trailingOverlayParams
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
private fun playHapticFeedback(isLight: Boolean) {
|
|
932
|
+
if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && enableHapticFeedback) {
|
|
933
|
+
vibrator!!.vibrate(VibrationEffect.createOneShot(if (isLight) 10L else 25L, VibrationEffect.DEFAULT_AMPLITUDE))
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
private fun startZoomWaitTimer() {
|
|
938
|
+
stopZoomWaitTimer()
|
|
939
|
+
if (isZoomedIn) return
|
|
940
|
+
|
|
941
|
+
zoomRunnable = Runnable {
|
|
942
|
+
stopZoomWaitTimer()
|
|
943
|
+
zoomIfNeeded()
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
zoomWaitTimer.postDelayed(zoomRunnable!!, 500)
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
private fun stopZoomWaitTimer() {
|
|
950
|
+
zoomRunnable?.let { zoomWaitTimer.removeCallbacks(it) }
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
private fun stopZoomIfNeeded() {
|
|
954
|
+
stopZoomWaitTimer()
|
|
955
|
+
if (isZoomedIn) {
|
|
956
|
+
isGeneratingThumbnails = false
|
|
957
|
+
BackgroundExecutor.cancelAll("progressive_thumbs", true)
|
|
958
|
+
isZoomedIn = false
|
|
959
|
+
restoreCachedThumbnails()
|
|
960
|
+
animateZoomTransition {
|
|
961
|
+
updateHandlePositions()
|
|
962
|
+
updateCurrentTime(true)
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private fun zoomIfNeeded() {
|
|
968
|
+
if (isZoomedIn) return
|
|
969
|
+
|
|
970
|
+
val currentLeadingX = leadingHandle.x
|
|
971
|
+
val currentTrailingX = trailingHandle.x
|
|
972
|
+
|
|
973
|
+
var newDuration = minOf(zoomOnWaitingDuration, mDuration.toLong())
|
|
974
|
+
|
|
975
|
+
when {
|
|
976
|
+
mDuration < 2000 -> newDuration = maxOf(500L, mDuration.toLong() / 2)
|
|
977
|
+
mDuration < zoomOnWaitingDuration -> newDuration = maxOf(1000L, mDuration.toLong() / 2)
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
newDuration = minOf(newDuration, mDuration.toLong())
|
|
981
|
+
|
|
982
|
+
val rangeStart = if (isTrimmingLeading) {
|
|
983
|
+
var rs = maxOf(0L, startTime - newDuration / 2)
|
|
984
|
+
if (rs + newDuration > mDuration) rs = maxOf(0L, mDuration.toLong() - newDuration)
|
|
985
|
+
rs
|
|
986
|
+
} else {
|
|
987
|
+
var rs = maxOf(0L, endTime - newDuration / 2)
|
|
988
|
+
if (rs + newDuration > mDuration) rs = maxOf(0L, mDuration.toLong() - newDuration)
|
|
989
|
+
rs
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
zoomedInRangeStart = maxOf(0L, rangeStart)
|
|
993
|
+
zoomedInRangeDuration = minOf(newDuration, mDuration.toLong() - zoomedInRangeStart)
|
|
994
|
+
|
|
995
|
+
isZoomedIn = true
|
|
996
|
+
|
|
997
|
+
startProgressiveThumbnailGeneration()
|
|
998
|
+
updateHandlePositionsForZoom(currentLeadingX, currentTrailingX)
|
|
999
|
+
playHapticFeedback(true)
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
private fun updateHandlePositionsForZoom(previousLeadingX: Float, previousTrailingX: Float) {
|
|
1003
|
+
Log.d(TAG, "Maintaining handle positions during zoom - Leading: $previousLeadingX, Trailing: $previousTrailingX")
|
|
1004
|
+
|
|
1005
|
+
leadingHandle.x = previousLeadingX
|
|
1006
|
+
trailingHandle.x = previousTrailingX
|
|
1007
|
+
|
|
1008
|
+
updateTrimmerContainerWidth()
|
|
1009
|
+
|
|
1010
|
+
val leftBoundary = leadingHandle.x + leadingHandle.width
|
|
1011
|
+
val rightBoundary = trailingHandle.x - progressIndicator.width
|
|
1012
|
+
val currentX = progressIndicator.x
|
|
1013
|
+
|
|
1014
|
+
if (currentX < leftBoundary || currentX > rightBoundary) {
|
|
1015
|
+
updateCurrentTime(true)
|
|
1016
|
+
} else {
|
|
1017
|
+
updateCurrentTime(false)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
trimmerContainerWrapper.visibility = View.VISIBLE
|
|
1021
|
+
if (trimmerContainerWrapper.alpha == 0f) {
|
|
1022
|
+
trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start()
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
private fun startProgressiveThumbnailGeneration() {
|
|
1027
|
+
if (isGeneratingThumbnails || mediaMetadataRetriever == null) return
|
|
1028
|
+
|
|
1029
|
+
isGeneratingThumbnails = true
|
|
1030
|
+
|
|
1031
|
+
UiThreadExecutor.runTask("", {
|
|
1032
|
+
mThumbnailContainer.removeAllViews()
|
|
1033
|
+
|
|
1034
|
+
val containerContentWidth = mThumbnailContainer.width - mThumbnailContainer.paddingLeft - mThumbnailContainer.paddingRight
|
|
1035
|
+
val effectiveWidth = if (containerContentWidth > 0) containerContentWidth else VideoTrimmerUtil.VIDEO_FRAMES_WIDTH
|
|
1036
|
+
val thumbWidth = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE
|
|
1037
|
+
val numberOfThumbnails = maxOf(8, effectiveWidth / maxOf(1, thumbWidth))
|
|
1038
|
+
val baseWidth = effectiveWidth / numberOfThumbnails
|
|
1039
|
+
val remainder = effectiveWidth % numberOfThumbnails
|
|
1040
|
+
|
|
1041
|
+
for (i in 0 until numberOfThumbnails) {
|
|
1042
|
+
val placeholder = ImageView(context)
|
|
1043
|
+
val width = if (i < remainder) baseWidth + 1 else baseWidth
|
|
1044
|
+
val layoutParams = LinearLayout.LayoutParams(width, LinearLayout.LayoutParams.MATCH_PARENT)
|
|
1045
|
+
placeholder.layoutParams = layoutParams
|
|
1046
|
+
placeholder.setBackgroundColor("#F0F0F0".toColorInt())
|
|
1047
|
+
placeholder.alpha = 0.2f
|
|
1048
|
+
mThumbnailContainer.addView(placeholder)
|
|
1049
|
+
}
|
|
1050
|
+
}, 0)
|
|
1051
|
+
|
|
1052
|
+
BackgroundExecutor.execute(object : BackgroundExecutor.Task("progressive_thumbs", 0L, "") {
|
|
1053
|
+
override fun execute() {
|
|
1054
|
+
try {
|
|
1055
|
+
val thumbnailWidth = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE
|
|
1056
|
+
val numberOfThumbnails = maxOf(8, mThumbnailContainer.width / thumbnailWidth)
|
|
1057
|
+
val visibleDuration = if (isZoomedIn) zoomedInRangeDuration else mDuration.toLong()
|
|
1058
|
+
val visibleStart = if (isZoomedIn) zoomedInRangeStart else 0L
|
|
1059
|
+
val interval = if (visibleDuration > 0) visibleDuration / numberOfThumbnails else 0L
|
|
1060
|
+
|
|
1061
|
+
for (i in 0 until numberOfThumbnails) {
|
|
1062
|
+
if (!isGeneratingThumbnails || !isZoomedIn) {
|
|
1063
|
+
Log.d(TAG, "Thumbnail generation cancelled at index $i")
|
|
1064
|
+
return
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
val index = i
|
|
1068
|
+
val timeUs = (visibleStart + i * interval) * 1000
|
|
1069
|
+
val clampedTimeUs = maxOf(0L, minOf(timeUs, mDuration * 1000L))
|
|
1070
|
+
|
|
1071
|
+
try {
|
|
1072
|
+
val bitmap = mediaMetadataRetriever?.getFrameAtTime(clampedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC)
|
|
1073
|
+
if (bitmap != null && isGeneratingThumbnails && isZoomedIn) {
|
|
1074
|
+
UiThreadExecutor.runTask("", {
|
|
1075
|
+
if (isZoomedIn && index < mThumbnailContainer.childCount) {
|
|
1076
|
+
val thumbnailView = mThumbnailContainer.getChildAt(index) as? ImageView
|
|
1077
|
+
if (thumbnailView != null) {
|
|
1078
|
+
thumbnailView.setImageBitmap(bitmap)
|
|
1079
|
+
thumbnailView.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
1080
|
+
thumbnailView.background = null
|
|
1081
|
+
|
|
1082
|
+
thumbnailView.animate()
|
|
1083
|
+
.alpha(1.0f)
|
|
1084
|
+
.setDuration(150)
|
|
1085
|
+
.setStartDelay(index * 50L)
|
|
1086
|
+
.start()
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}, 0)
|
|
1090
|
+
|
|
1091
|
+
Thread.sleep(10)
|
|
1092
|
+
}
|
|
1093
|
+
} catch (e: Exception) {
|
|
1094
|
+
Log.w(TAG, "Error generating progressive thumbnail at $clampedTimeUs", e)
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
isGeneratingThumbnails = false
|
|
1099
|
+
} catch (e: Exception) {
|
|
1100
|
+
Log.e(TAG, "Error in progressive thumbnail generation", e)
|
|
1101
|
+
isGeneratingThumbnails = false
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
})
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
private fun animateZoomTransition(onComplete: Runnable?) {
|
|
1108
|
+
mThumbnailContainer.animate()
|
|
1109
|
+
.alpha(0.7f)
|
|
1110
|
+
.setDuration(200)
|
|
1111
|
+
.withEndAction {
|
|
1112
|
+
onComplete?.run()
|
|
1113
|
+
mThumbnailContainer.animate()
|
|
1114
|
+
.alpha(1.0f)
|
|
1115
|
+
.setDuration(200)
|
|
1116
|
+
.start()
|
|
1117
|
+
}
|
|
1118
|
+
.start()
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
private fun ignoreSystemGestureForView(v: View) {
|
|
1122
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1123
|
+
v.systemGestureExclusionRects = listOf(
|
|
1124
|
+
android.graphics.Rect(0, 0, DeviceUtil.getDeviceWidth(), DeviceUtil.getDeviceHeight())
|
|
1125
|
+
)
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
private fun timeForPosition(position: Float): Long {
|
|
1130
|
+
if (trimmerContainerBg.width <= 0) return 0
|
|
1131
|
+
|
|
1132
|
+
return if (isZoomedIn) {
|
|
1133
|
+
val ratio = position / trimmerContainerBg.width
|
|
1134
|
+
zoomedInRangeStart + (ratio * zoomedInRangeDuration).toLong()
|
|
1135
|
+
} else {
|
|
1136
|
+
(position / trimmerContainerBg.width * mDuration).toLong()
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
private fun positionForTime(time: Long): Float {
|
|
1141
|
+
return if (isZoomedIn) {
|
|
1142
|
+
if (zoomedInRangeDuration <= 0) return 0f
|
|
1143
|
+
val ratio = (time - zoomedInRangeStart).toFloat() / zoomedInRangeDuration
|
|
1144
|
+
maxOf(0f, minOf(trimmerContainerBg.width.toFloat(), ratio * trimmerContainerBg.width))
|
|
1145
|
+
} else {
|
|
1146
|
+
if (mDuration <= 0) return 0f
|
|
1147
|
+
maxOf(0f, minOf(trimmerContainerBg.width.toFloat(), time.toFloat() / mDuration * trimmerContainerBg.width))
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
private fun getVisibleRangeStart(): Long {
|
|
1152
|
+
return if (isZoomedIn) zoomedInRangeStart else 0
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
private fun getVisibleRangeDuration(): Long {
|
|
1156
|
+
return if (isZoomedIn) zoomedInRangeDuration else mDuration.toLong()
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
private fun restoreCachedThumbnails() {
|
|
1160
|
+
mThumbnailContainer.removeAllViews()
|
|
1161
|
+
|
|
1162
|
+
for (cachedThumbnail in cachedFullViewThumbnails) {
|
|
1163
|
+
val restoredView = ImageView(context)
|
|
1164
|
+
restoredView.setImageBitmap((cachedThumbnail.drawable as android.graphics.drawable.BitmapDrawable).bitmap)
|
|
1165
|
+
restoredView.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
1166
|
+
restoredView.layoutParams = cachedThumbnail.layoutParams
|
|
1167
|
+
mThumbnailContainer.addView(restoredView)
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|