react-native-video-trim 6.2.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/package.json +1 -1
- 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 -1322
|
@@ -1,1322 +0,0 @@
|
|
|
1
|
-
package com.videotrim.widgets;
|
|
2
|
-
|
|
3
|
-
import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
|
|
4
|
-
import static com.videotrim.utils.VideoTrimmerUtil.RECYCLER_VIEW_PADDING;
|
|
5
|
-
import static com.videotrim.utils.VideoTrimmerUtil.VIDEO_FRAMES_WIDTH;
|
|
6
|
-
|
|
7
|
-
import android.content.Context;
|
|
8
|
-
import android.content.pm.ActivityInfo;
|
|
9
|
-
import android.content.res.Configuration;
|
|
10
|
-
import android.graphics.Bitmap;
|
|
11
|
-
import android.graphics.Color;
|
|
12
|
-
import android.graphics.drawable.GradientDrawable;
|
|
13
|
-
import android.media.MediaMetadataRetriever;
|
|
14
|
-
import android.media.MediaPlayer;
|
|
15
|
-
import android.net.Uri;
|
|
16
|
-
import android.os.Build;
|
|
17
|
-
import android.os.Handler;
|
|
18
|
-
import android.os.VibrationEffect;
|
|
19
|
-
import android.os.Vibrator;
|
|
20
|
-
import android.util.AttributeSet;
|
|
21
|
-
import android.util.Log;
|
|
22
|
-
import android.util.TypedValue;
|
|
23
|
-
import android.view.LayoutInflater;
|
|
24
|
-
import android.view.GestureDetector;
|
|
25
|
-
import android.view.MotionEvent;
|
|
26
|
-
import android.view.View;
|
|
27
|
-
import android.widget.FrameLayout;
|
|
28
|
-
import android.widget.ImageView;
|
|
29
|
-
import android.widget.LinearLayout;
|
|
30
|
-
import android.widget.ProgressBar;
|
|
31
|
-
import android.widget.RelativeLayout;
|
|
32
|
-
import android.widget.TextView;
|
|
33
|
-
import android.widget.VideoView;
|
|
34
|
-
|
|
35
|
-
import androidx.appcompat.app.AlertDialog;
|
|
36
|
-
|
|
37
|
-
import com.arthenica.ffmpegkit.FFmpegSession;
|
|
38
|
-
import com.facebook.react.bridge.ReactApplicationContext;
|
|
39
|
-
import com.facebook.react.bridge.ReadableMap;
|
|
40
|
-
import com.videotrim.R;
|
|
41
|
-
import com.videotrim.enums.ErrorCode;
|
|
42
|
-
import com.videotrim.interfaces.IVideoTrimmerView;
|
|
43
|
-
import com.videotrim.interfaces.VideoTrimListener;
|
|
44
|
-
import com.videotrim.utils.MediaMetadataUtil;
|
|
45
|
-
import com.videotrim.utils.StorageUtil;
|
|
46
|
-
import com.videotrim.utils.VideoTrimmerUtil;
|
|
47
|
-
|
|
48
|
-
import java.io.IOException;
|
|
49
|
-
import java.util.Locale;
|
|
50
|
-
import java.util.Objects;
|
|
51
|
-
|
|
52
|
-
import iknow.android.utils.DeviceUtil;
|
|
53
|
-
import iknow.android.utils.thread.BackgroundExecutor;
|
|
54
|
-
import iknow.android.utils.thread.UiThreadExecutor;
|
|
55
|
-
|
|
56
|
-
public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
|
|
57
|
-
|
|
58
|
-
private static final String TAG = VideoTrimmerView.class.getSimpleName();
|
|
59
|
-
|
|
60
|
-
private ReactApplicationContext mContext;
|
|
61
|
-
private VideoView mVideoView;
|
|
62
|
-
|
|
63
|
-
// mediaPlayer is used for both video/audio
|
|
64
|
-
// the reason we use mediaPlayer for Video: https://stackoverflow.com/a/73361868/7569705
|
|
65
|
-
// 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
|
|
66
|
-
private MediaPlayer mediaPlayer;
|
|
67
|
-
private ImageView mPlayView;
|
|
68
|
-
private LinearLayout mThumbnailContainer;
|
|
69
|
-
private Uri mSourceUri;
|
|
70
|
-
private VideoTrimListener mOnTrimVideoListener;
|
|
71
|
-
private int mDuration = 0;
|
|
72
|
-
private long mMaxDuration = (long) Double.POSITIVE_INFINITY;
|
|
73
|
-
private long mMinDuration = VideoTrimmerUtil.MIN_SHOOT_DURATION;
|
|
74
|
-
|
|
75
|
-
private final Handler mTimingHandler = new Handler();
|
|
76
|
-
private Runnable mTimingRunnable;
|
|
77
|
-
private static final long TIMING_UPDATE_INTERVAL = 30; // Update every 30 milliseconds
|
|
78
|
-
private TextView currentTimeText;
|
|
79
|
-
private TextView startTimeText;
|
|
80
|
-
private TextView endTimeText;
|
|
81
|
-
private View progressIndicator;
|
|
82
|
-
private View trimmerContainer;
|
|
83
|
-
// background of the trimmer container, its width never changes
|
|
84
|
-
// 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
|
|
85
|
-
private View trimmerContainerBg;
|
|
86
|
-
private FrameLayout leadingHandle;
|
|
87
|
-
private View trailingHandle;
|
|
88
|
-
private View leadingOverlay;
|
|
89
|
-
private View trailingOverlay;
|
|
90
|
-
private RelativeLayout trimmerContainerWrapper;
|
|
91
|
-
|
|
92
|
-
private long startTime = 0, endTime = 0;
|
|
93
|
-
private boolean enableRotation = false;
|
|
94
|
-
private double rotationAngle = 0.0;
|
|
95
|
-
private long zoomOnWaitingDuration = 5000; // Default: 5 seconds (in milliseconds)
|
|
96
|
-
|
|
97
|
-
private Vibrator vibrator;
|
|
98
|
-
private boolean didClampWhilePanning = false;
|
|
99
|
-
|
|
100
|
-
// zoom
|
|
101
|
-
private boolean isZoomedIn = false;
|
|
102
|
-
private final Handler zoomWaitTimer = new Handler();
|
|
103
|
-
private Runnable zoomRunnable;
|
|
104
|
-
private long zoomedInRangeStart = 0;
|
|
105
|
-
private long zoomedInRangeDuration = 0;
|
|
106
|
-
private boolean isTrimmingLeading = false;
|
|
107
|
-
|
|
108
|
-
// range drag
|
|
109
|
-
private boolean isRangeDragging = false;
|
|
110
|
-
private float rangeDragInitialRawX = 0;
|
|
111
|
-
private long rangeDragInitialStartTime = 0;
|
|
112
|
-
private long rangeDragInitialEndTime = 0;
|
|
113
|
-
private GestureDetector rangeDragGestureDetector;
|
|
114
|
-
|
|
115
|
-
// thumbnail caching for zoom functionality
|
|
116
|
-
private final java.util.List<ImageView> cachedFullViewThumbnails = new java.util.ArrayList<>();
|
|
117
|
-
private volatile boolean isGeneratingThumbnails = false;
|
|
118
|
-
|
|
119
|
-
private MediaMetadataRetriever mediaMetadataRetriever;
|
|
120
|
-
private ProgressBar loadingIndicator;
|
|
121
|
-
private TextView saveBtn;
|
|
122
|
-
private TextView cancelBtn;
|
|
123
|
-
private FrameLayout audioBannerView;
|
|
124
|
-
private boolean isVideoType = true;
|
|
125
|
-
private ImageView failToLoadBtn;
|
|
126
|
-
|
|
127
|
-
private String mOutputExt = "mp4";
|
|
128
|
-
private boolean enableHapticFeedback = true;
|
|
129
|
-
private boolean autoplay = false;
|
|
130
|
-
private long jumpToPositionOnLoad = 0;
|
|
131
|
-
private FrameLayout headerView;
|
|
132
|
-
private TextView headerText;
|
|
133
|
-
private FFmpegSession ffmpegSession;
|
|
134
|
-
private boolean alertOnFailToLoad = true;
|
|
135
|
-
private String alertOnFailTitle = "Error";
|
|
136
|
-
private String alertOnFailMessage = "Fail to load media. Possibly invalid file or no network connection";
|
|
137
|
-
private String alertOnFailCloseText = "Close";
|
|
138
|
-
private View currentSelectedhandle;
|
|
139
|
-
|
|
140
|
-
private RelativeLayout trimmerView;
|
|
141
|
-
|
|
142
|
-
private int trimmerColor = Color.parseColor(getContext().getString(R.string.trim_color)); // Default color if not set
|
|
143
|
-
private int handleIconColor = Color.BLACK; // Default chevron color
|
|
144
|
-
private ImageView leadingChevron;
|
|
145
|
-
private ImageView trailingChevron;
|
|
146
|
-
|
|
147
|
-
public VideoTrimmerView(ReactApplicationContext context, ReadableMap config, AttributeSet attrs) {
|
|
148
|
-
this(context, attrs, 0, config);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
public VideoTrimmerView(ReactApplicationContext context, AttributeSet attrs, int defStyleAttr, ReadableMap config) {
|
|
152
|
-
super(context, attrs, defStyleAttr);
|
|
153
|
-
init(context, config);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private void init(ReactApplicationContext context, ReadableMap config) {
|
|
157
|
-
this.mContext = context;
|
|
158
|
-
|
|
159
|
-
context.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
|
160
|
-
LayoutInflater.from(context).inflate(R.layout.video_trimmer_view, this, true);
|
|
161
|
-
vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
|
|
162
|
-
|
|
163
|
-
initializeViews();
|
|
164
|
-
configure(config);
|
|
165
|
-
setUpListeners();
|
|
166
|
-
initRangeDragDetector();
|
|
167
|
-
setProgressIndicatorTouchListener();
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
private void initRangeDragDetector() {
|
|
171
|
-
rangeDragGestureDetector = new GestureDetector(mContext, new GestureDetector.SimpleOnGestureListener() {
|
|
172
|
-
@Override
|
|
173
|
-
public void onLongPress(MotionEvent e) {
|
|
174
|
-
isRangeDragging = true;
|
|
175
|
-
rangeDragInitialRawX = e.getRawX();
|
|
176
|
-
rangeDragInitialStartTime = startTime;
|
|
177
|
-
rangeDragInitialEndTime = endTime;
|
|
178
|
-
playHapticFeedback(true);
|
|
179
|
-
fadeOutProgressIndicator();
|
|
180
|
-
}
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
private void initializeViews() {
|
|
185
|
-
mThumbnailContainer = findViewById(R.id.thumbnailContainer);
|
|
186
|
-
mVideoView = findViewById(R.id.video_loader);
|
|
187
|
-
mPlayView = findViewById(R.id.icon_video_play);
|
|
188
|
-
startTimeText = findViewById(R.id.startTime);
|
|
189
|
-
currentTimeText = findViewById(R.id.currentTime);
|
|
190
|
-
endTimeText = findViewById(R.id.endTime);
|
|
191
|
-
progressIndicator = findViewById(R.id.progressIndicator);
|
|
192
|
-
trimmerContainer = findViewById(R.id.trimmerContainer);
|
|
193
|
-
trimmerContainerBg = findViewById(R.id.trimmerContainerBg);
|
|
194
|
-
leadingHandle = findViewById(R.id.leadingHandle);
|
|
195
|
-
trailingHandle = findViewById(R.id.trailingHandle);
|
|
196
|
-
leadingOverlay = findViewById(R.id.leadingOverlay);
|
|
197
|
-
trailingOverlay = findViewById(R.id.trailingOverlay);
|
|
198
|
-
|
|
199
|
-
trimmerContainerWrapper = findViewById(R.id.trimmerContainerWrapper);
|
|
200
|
-
trimmerContainerWrapper.setVisibility(View.INVISIBLE);
|
|
201
|
-
trimmerContainerWrapper.setAlpha(0f);
|
|
202
|
-
|
|
203
|
-
loadingIndicator = findViewById(R.id.loadingIndicator);
|
|
204
|
-
saveBtn = findViewById(R.id.saveBtn);
|
|
205
|
-
cancelBtn = findViewById(R.id.cancelBtn);
|
|
206
|
-
audioBannerView = findViewById(R.id.audioBannerView);
|
|
207
|
-
failToLoadBtn = findViewById(R.id.failToLoadBtn);
|
|
208
|
-
|
|
209
|
-
headerView = findViewById(R.id.headerView);
|
|
210
|
-
headerText = findViewById(R.id.headerText);
|
|
211
|
-
|
|
212
|
-
trimmerView = findViewById(R.id.trimmerView);
|
|
213
|
-
|
|
214
|
-
leadingChevron = findViewById(R.id.leadingChevron);
|
|
215
|
-
trailingChevron = findViewById(R.id.trailingChevron);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
public void initByURI(final Uri videoURI) {
|
|
219
|
-
mSourceUri = videoURI;
|
|
220
|
-
|
|
221
|
-
if (isVideoType) {
|
|
222
|
-
mVideoView.setVideoURI(videoURI);
|
|
223
|
-
mVideoView.requestFocus();
|
|
224
|
-
|
|
225
|
-
mVideoView.setOnPreparedListener(mp -> {
|
|
226
|
-
mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
|
|
227
|
-
mediaPlayer = mp;
|
|
228
|
-
mediaPrepared();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
mVideoView.setOnErrorListener(this::onFailToLoadMedia);
|
|
232
|
-
mVideoView.setOnCompletionListener(mp -> mediaCompleted());
|
|
233
|
-
} else {
|
|
234
|
-
mVideoView.setVisibility(View.GONE);
|
|
235
|
-
audioBannerView.setAlpha(0f);
|
|
236
|
-
audioBannerView.setVisibility(View.VISIBLE);
|
|
237
|
-
audioBannerView.animate().alpha(1f).setDuration(500).start();
|
|
238
|
-
|
|
239
|
-
mediaPlayer = new MediaPlayer();
|
|
240
|
-
try {
|
|
241
|
-
mediaPlayer.setDataSource(videoURI.toString());
|
|
242
|
-
mediaPlayer.setOnPreparedListener(mp -> {
|
|
243
|
-
mediaPrepared();
|
|
244
|
-
});
|
|
245
|
-
mediaPlayer.setOnCompletionListener(mp -> mediaCompleted());
|
|
246
|
-
mediaPlayer.setOnErrorListener(this::onFailToLoadMedia);
|
|
247
|
-
|
|
248
|
-
mediaPlayer.prepareAsync(); // use prepareAsync to avoid blocking the main thread
|
|
249
|
-
} catch (IOException e) {
|
|
250
|
-
e.printStackTrace();
|
|
251
|
-
mediaFailed();
|
|
252
|
-
mOnTrimVideoListener.onError("Error initializing audio player. Please try again.", ErrorCode.FAIL_TO_INITIALIZE_AUDIO_PLAYER);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
private boolean onFailToLoadMedia(MediaPlayer mp, int what, int extra) {
|
|
258
|
-
mediaFailed();
|
|
259
|
-
mOnTrimVideoListener.onError("Error loading media file. Please try again.", ErrorCode.FAIL_TO_LOAD_MEDIA);
|
|
260
|
-
if (alertOnFailToLoad) {
|
|
261
|
-
AlertDialog.Builder builder = new AlertDialog.Builder(mContext.getCurrentActivity());
|
|
262
|
-
builder.setMessage(alertOnFailMessage);
|
|
263
|
-
builder.setTitle(alertOnFailTitle);
|
|
264
|
-
builder.setCancelable(false);
|
|
265
|
-
builder.setPositiveButton(alertOnFailCloseText, (dialog, which) -> {
|
|
266
|
-
dialog.cancel();
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
AlertDialog alertDialog = builder.create();
|
|
270
|
-
alertDialog.show();
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return true;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
private void startShootVideoThumbs(final Context context, int totalThumbsCount, long startPosition, long endPosition) {
|
|
277
|
-
mThumbnailContainer.removeAllViews();
|
|
278
|
-
cachedFullViewThumbnails.clear(); // Clear previous cache
|
|
279
|
-
|
|
280
|
-
VideoTrimmerUtil.shootVideoThumbInBackground(mediaMetadataRetriever, totalThumbsCount, startPosition, endPosition,
|
|
281
|
-
(bitmap, interval) -> {
|
|
282
|
-
if (bitmap != null) {
|
|
283
|
-
runOnUiThread(() -> {
|
|
284
|
-
ImageView thumbImageView = new ImageView(context);
|
|
285
|
-
thumbImageView.setImageBitmap(bitmap);
|
|
286
|
-
thumbImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
287
|
-
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(100, LayoutParams.MATCH_PARENT);
|
|
288
|
-
layoutParams.width = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
|
|
289
|
-
thumbImageView.setLayoutParams(layoutParams);
|
|
290
|
-
mThumbnailContainer.addView(thumbImageView);
|
|
291
|
-
|
|
292
|
-
// Cache the thumbnail for zoom functionality
|
|
293
|
-
ImageView cachedView = new ImageView(context);
|
|
294
|
-
cachedView.setImageBitmap(bitmap);
|
|
295
|
-
cachedView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
296
|
-
cachedView.setLayoutParams(layoutParams);
|
|
297
|
-
cachedFullViewThumbnails.add(cachedView);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
private void mediaPrepared() {
|
|
304
|
-
mDuration = mediaPlayer.getDuration();
|
|
305
|
-
mMaxDuration = Math.min(mMaxDuration, mDuration);
|
|
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
|
-
// take first frame
|
|
315
|
-
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
|
316
|
-
|
|
317
|
-
if (bitmap != null) {
|
|
318
|
-
int bitmapHeight = bitmap.getHeight() > 0 ? bitmap.getHeight() : VideoTrimmerUtil.THUMB_HEIGHT;
|
|
319
|
-
int bitmapWidth = bitmap.getWidth() > 0 ? bitmap.getWidth() : VideoTrimmerUtil.THUMB_WIDTH;
|
|
320
|
-
VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmapWidth / bitmapHeight;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
|
|
324
|
-
VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
|
|
325
|
-
VideoTrimmerUtil.MAX_COUNT_RANGE = VideoTrimmerUtil.mThumbWidth != 0 ? Math.max((VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE) : VideoTrimmerUtil.MAX_COUNT_RANGE;
|
|
326
|
-
|
|
327
|
-
startShootVideoThumbs(mContext, VideoTrimmerUtil.MAX_COUNT_RANGE, 0, mDuration);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Set initial handle positions if mMaxDuration < video duration
|
|
331
|
-
if (mMaxDuration < mDuration) {
|
|
332
|
-
endTime = mMaxDuration;
|
|
333
|
-
} else {
|
|
334
|
-
endTime = mDuration;
|
|
335
|
-
}
|
|
336
|
-
updateHandlePositions();
|
|
337
|
-
|
|
338
|
-
loadingIndicator.setVisibility(View.GONE);
|
|
339
|
-
mPlayView.setVisibility(View.VISIBLE);
|
|
340
|
-
saveBtn.setVisibility(View.VISIBLE);
|
|
341
|
-
|
|
342
|
-
if (jumpToPositionOnLoad > 0) {
|
|
343
|
-
seekTo(jumpToPositionOnLoad > mDuration ? mDuration : jumpToPositionOnLoad, true);
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
if (autoplay) {
|
|
347
|
-
playOrPause();
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
mOnTrimVideoListener.onLoad(mDuration);
|
|
351
|
-
ignoreSystemGestureForView(trimmerView);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
private void updateGradientColors(int startColor, int endColor) {
|
|
355
|
-
GradientDrawable gradientDrawable = new GradientDrawable();
|
|
356
|
-
gradientDrawable.setShape(GradientDrawable.RECTANGLE);
|
|
357
|
-
gradientDrawable.setCornerRadius(6f); // Adjust corner radius as needed
|
|
358
|
-
gradientDrawable.setColors(new int[]{startColor, endColor});
|
|
359
|
-
gradientDrawable.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
|
|
360
|
-
|
|
361
|
-
mThumbnailContainer.setBackground(gradientDrawable);
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
private void mediaFailed() {
|
|
365
|
-
loadingIndicator.setVisibility(View.GONE);
|
|
366
|
-
failToLoadBtn.setVisibility(View.VISIBLE);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private void updateHandlePositions() {
|
|
370
|
-
// Use zoom-aware position calculation
|
|
371
|
-
float leadingHandleX = positionForTime(startTime);
|
|
372
|
-
float trailingHandleX = positionForTime(endTime);
|
|
373
|
-
|
|
374
|
-
leadingHandle.setX(leadingHandleX);
|
|
375
|
-
trailingHandle.setX(trailingHandleX + trailingHandle.getWidth());
|
|
376
|
-
|
|
377
|
-
updateTrimmerContainerWidth();
|
|
378
|
-
updateCurrentTime(false);
|
|
379
|
-
|
|
380
|
-
trimmerContainerWrapper.setVisibility(View.VISIBLE);
|
|
381
|
-
trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start();
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
private void mediaCompleted() {
|
|
385
|
-
onMediaPause();
|
|
386
|
-
|
|
387
|
-
// 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
|
|
388
|
-
seekTo(endTime, true);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
private void playOrPause() {
|
|
392
|
-
if (mediaPlayer.isPlaying()) {
|
|
393
|
-
onMediaPause();
|
|
394
|
-
} else {
|
|
395
|
-
if (mediaPlayer.getCurrentPosition() >= endTime) {
|
|
396
|
-
seekTo(startTime, true);
|
|
397
|
-
}
|
|
398
|
-
mediaPlayer.start();
|
|
399
|
-
startTimingRunnable();
|
|
400
|
-
}
|
|
401
|
-
setPlayPauseViewIcon(mediaPlayer.isPlaying());
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
public void onMediaPause() {
|
|
405
|
-
mTimingHandler.removeCallbacks(mTimingRunnable);
|
|
406
|
-
if (mediaPlayer.isPlaying()) {
|
|
407
|
-
mediaPlayer.pause();
|
|
408
|
-
}
|
|
409
|
-
setPlayPauseViewIcon(false);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
public void setOnTrimVideoListener(VideoTrimListener onTrimVideoListener) {
|
|
413
|
-
mOnTrimVideoListener = onTrimVideoListener;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
private void setUpListeners() {
|
|
417
|
-
cancelBtn.setOnClickListener(view -> mOnTrimVideoListener.onCancel());
|
|
418
|
-
saveBtn.setOnClickListener(view -> mOnTrimVideoListener.onSave());
|
|
419
|
-
mPlayView.setOnClickListener(view -> playOrPause());
|
|
420
|
-
setHandleTouchListener(leadingHandle, true);
|
|
421
|
-
setHandleTouchListener(trailingHandle, false);
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
public void onSaveClicked() {
|
|
425
|
-
onMediaPause();
|
|
426
|
-
ffmpegSession = VideoTrimmerUtil.trim(
|
|
427
|
-
mSourceUri.toString(),
|
|
428
|
-
StorageUtil.getOutputPath(mContext, mOutputExt),
|
|
429
|
-
mDuration,
|
|
430
|
-
startTime,
|
|
431
|
-
endTime,
|
|
432
|
-
enableRotation,
|
|
433
|
-
rotationAngle,
|
|
434
|
-
mOnTrimVideoListener
|
|
435
|
-
);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
public void onCancelTrimClicked() {
|
|
439
|
-
if (ffmpegSession != null) {
|
|
440
|
-
ffmpegSession.cancel();
|
|
441
|
-
} else {
|
|
442
|
-
mOnTrimVideoListener.onCancelTrim();
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
private void seekTo(long msec, boolean needUpdateProgress) {
|
|
447
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
448
|
-
mediaPlayer.seekTo((int) msec, MediaPlayer.SEEK_CLOSEST);
|
|
449
|
-
} else {
|
|
450
|
-
mediaPlayer.seekTo((int) msec);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
updateCurrentTime(needUpdateProgress);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
private void setPlayPauseViewIcon(boolean isPlaying) {
|
|
457
|
-
// note: icons imported from SF symbols have 0.85 opacity we should change to 1 here
|
|
458
|
-
mPlayView.setImageResource(isPlaying ? R.drawable.pause_fill : R.drawable.play_fill);
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
@Override
|
|
462
|
-
protected void onDetachedFromWindow() {
|
|
463
|
-
super.onDetachedFromWindow();
|
|
464
|
-
mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
@Override
|
|
468
|
-
public void onDestroy() {
|
|
469
|
-
// Stop any ongoing operations
|
|
470
|
-
isGeneratingThumbnails = false;
|
|
471
|
-
BackgroundExecutor.cancelAll("", true);
|
|
472
|
-
UiThreadExecutor.cancelAll("");
|
|
473
|
-
mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
|
474
|
-
mTimingHandler.removeCallbacks(mTimingRunnable);
|
|
475
|
-
zoomWaitTimer.removeCallbacks(zoomRunnable);
|
|
476
|
-
|
|
477
|
-
// Clear cached thumbnails to prevent memory leaks
|
|
478
|
-
cachedFullViewThumbnails.clear();
|
|
479
|
-
|
|
480
|
-
try {
|
|
481
|
-
if (mediaMetadataRetriever != null) {
|
|
482
|
-
mediaMetadataRetriever.release();
|
|
483
|
-
}
|
|
484
|
-
} catch (Exception e) {
|
|
485
|
-
e.printStackTrace();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
try {
|
|
489
|
-
if (mediaPlayer != null) {
|
|
490
|
-
mediaPlayer.stop();
|
|
491
|
-
mediaPlayer.release();
|
|
492
|
-
}
|
|
493
|
-
} catch (IllegalStateException e) {
|
|
494
|
-
// if it's video, resource is released with the view, and here we also call .release which will throw exception
|
|
495
|
-
e.printStackTrace();
|
|
496
|
-
Log.d(TAG, "onDestroy mediaPlayer is already released");
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
private int getScreenWidthInPortraitMode() {
|
|
501
|
-
int screenWidth = DeviceUtil.getDeviceWidth();
|
|
502
|
-
int screenHeight = DeviceUtil.getDeviceHeight();
|
|
503
|
-
int currentOrientation = getResources().getConfiguration().orientation;
|
|
504
|
-
if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
505
|
-
return screenHeight;
|
|
506
|
-
}
|
|
507
|
-
return screenWidth;
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
private void configure(ReadableMap config) {
|
|
511
|
-
if (config.hasKey("maxDuration") && config.getDouble("maxDuration") > 0) {
|
|
512
|
-
mMaxDuration = (long) Math.max(0, config.getDouble("maxDuration"));
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
if (config.hasKey("minDuration") && config.getDouble("minDuration") > 0) {
|
|
516
|
-
mMinDuration = (long) Math.max(1000L, config.getDouble("minDuration"));
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
cancelBtn.setText(config.getString("cancelButtonText"));
|
|
520
|
-
saveBtn.setText(config.getString("saveButtonText"));
|
|
521
|
-
isVideoType = config.hasKey("type") && Objects.equals(config.getString("type"), "video");
|
|
522
|
-
System.out.println("isVideoType: " + isVideoType);
|
|
523
|
-
|
|
524
|
-
mOutputExt = config.hasKey("outputExt") ? config.getString("outputExt") : "mp4";
|
|
525
|
-
if (!isVideoType) {
|
|
526
|
-
mOutputExt = "wav";
|
|
527
|
-
}
|
|
528
|
-
enableHapticFeedback = config.hasKey("enableHapticFeedback") && config.getBoolean("enableHapticFeedback");
|
|
529
|
-
autoplay = config.hasKey("autoplay") && config.getBoolean("autoplay");
|
|
530
|
-
|
|
531
|
-
if (config.hasKey("jumpToPositionOnLoad") && config.getDouble("jumpToPositionOnLoad") > 0) {
|
|
532
|
-
jumpToPositionOnLoad = (long) Math.max(0, config.getDouble("jumpToPositionOnLoad") * 1000L);
|
|
533
|
-
}
|
|
534
|
-
headerText.setText(config.hasKey("headerText") ? config.getString("headerText") : "");
|
|
535
|
-
|
|
536
|
-
int textSize = config.hasKey("headerTextSize") ? config.getInt("headerTextSize") : 16;
|
|
537
|
-
if (textSize < 0) {
|
|
538
|
-
textSize = 16;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
headerText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, textSize);
|
|
542
|
-
headerText.setTextColor(config.hasKey("headerTextColor") ? config.getInt("headerTextColor") : Color.BLACK);
|
|
543
|
-
|
|
544
|
-
headerView.setVisibility(View.VISIBLE);
|
|
545
|
-
alertOnFailToLoad = config.hasKey("alertOnFailToLoad") && config.getBoolean("alertOnFailToLoad");
|
|
546
|
-
alertOnFailTitle = config.hasKey("alertOnFailTitle") ? config.getString("alertOnFailTitle") : "Error";
|
|
547
|
-
alertOnFailMessage = config.hasKey("alertOnFailMessage") ? config.getString("alertOnFailMessage") : "Fail to load media. Possibly invalid file or no network connection";
|
|
548
|
-
alertOnFailCloseText = config.hasKey("alertOnFailCloseText") ? config.getString("alertOnFailCloseText") : "Close";
|
|
549
|
-
enableRotation = config.hasKey("enableRotation") && config.getBoolean("enableRotation");
|
|
550
|
-
rotationAngle = config.hasKey("rotationAngle") ? config.getDouble("rotationAngle") : 0.0;
|
|
551
|
-
|
|
552
|
-
// Configure zoom on waiting duration (in seconds, converted to milliseconds)
|
|
553
|
-
if (config.hasKey("zoomOnWaitingDuration") && config.getDouble("zoomOnWaitingDuration") > 0) {
|
|
554
|
-
zoomOnWaitingDuration = (long) (config.getDouble("zoomOnWaitingDuration"));
|
|
555
|
-
Log.d(TAG, "Configured zoom on waiting duration: " + (zoomOnWaitingDuration / 1000.0) + " seconds");
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
trimmerColor = config.hasKey("trimmerColor") ? config.getInt("trimmerColor") : Color.parseColor(getContext().getString(R.string.trim_color));
|
|
559
|
-
handleIconColor = config.hasKey("handleIconColor") ? config.getInt("handleIconColor") : Color.BLACK;
|
|
560
|
-
|
|
561
|
-
applyTrimmerColor();
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
private void applyTrimmerColor() {
|
|
565
|
-
// Trimmer border (stroke only)
|
|
566
|
-
GradientDrawable borderDrawable = new GradientDrawable();
|
|
567
|
-
borderDrawable.setShape(GradientDrawable.RECTANGLE);
|
|
568
|
-
borderDrawable.setColor(Color.TRANSPARENT);
|
|
569
|
-
borderDrawable.setStroke(dpToPx(4), trimmerColor);
|
|
570
|
-
trimmerContainer.setBackground(borderDrawable);
|
|
571
|
-
|
|
572
|
-
// Leading handle (rounded left corners)
|
|
573
|
-
GradientDrawable leadingHandleDrawable = new GradientDrawable();
|
|
574
|
-
leadingHandleDrawable.setShape(GradientDrawable.RECTANGLE);
|
|
575
|
-
leadingHandleDrawable.setColor(trimmerColor);
|
|
576
|
-
leadingHandleDrawable.setCornerRadii(new float[]{dpToPx(6), dpToPx(6), 0, 0, 0, 0, dpToPx(6), dpToPx(6)});
|
|
577
|
-
leadingHandle.setBackground(leadingHandleDrawable);
|
|
578
|
-
|
|
579
|
-
// Trailing handle (rounded right corners)
|
|
580
|
-
GradientDrawable trailingHandleDrawable = new GradientDrawable();
|
|
581
|
-
trailingHandleDrawable.setShape(GradientDrawable.RECTANGLE);
|
|
582
|
-
trailingHandleDrawable.setColor(trimmerColor);
|
|
583
|
-
trailingHandleDrawable.setCornerRadii(new float[]{0, 0, dpToPx(6), dpToPx(6), dpToPx(6), dpToPx(6), 0, 0});
|
|
584
|
-
trailingHandle.setBackground(trailingHandleDrawable);
|
|
585
|
-
|
|
586
|
-
// Chevron colors
|
|
587
|
-
leadingChevron.setColorFilter(handleIconColor, android.graphics.PorterDuff.Mode.SRC_IN);
|
|
588
|
-
trailingChevron.setColorFilter(handleIconColor, android.graphics.PorterDuff.Mode.SRC_IN);
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
private int dpToPx(int dp) {
|
|
592
|
-
return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
private void startTimingRunnable() {
|
|
596
|
-
mTimingRunnable = new Runnable() {
|
|
597
|
-
@Override
|
|
598
|
-
public void run() {
|
|
599
|
-
try {
|
|
600
|
-
int currentPosition = mediaPlayer.getCurrentPosition();
|
|
601
|
-
|
|
602
|
-
if (currentPosition >= endTime) {
|
|
603
|
-
onMediaPause();
|
|
604
|
-
seekTo(endTime, true); // Ensure exact end time display
|
|
605
|
-
} else {
|
|
606
|
-
updateCurrentTime(true);
|
|
607
|
-
mTimingHandler.postDelayed(this, TIMING_UPDATE_INTERVAL);
|
|
608
|
-
}
|
|
609
|
-
} catch (IllegalStateException e) {
|
|
610
|
-
e.printStackTrace();
|
|
611
|
-
mTimingHandler.removeCallbacks(mTimingRunnable);
|
|
612
|
-
// this is to catch the error thrown if we close editor while playing (mediaPlayer is released)
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
};
|
|
616
|
-
mTimingHandler.postDelayed(mTimingRunnable, TIMING_UPDATE_INTERVAL);
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
private void updateCurrentTime(boolean needUpdateProgress) {
|
|
620
|
-
int currentPosition = mediaPlayer.getCurrentPosition();
|
|
621
|
-
int duration = mDuration;
|
|
622
|
-
|
|
623
|
-
if (currentPosition >= duration - 100) {
|
|
624
|
-
currentPosition = duration;
|
|
625
|
-
} else if (currentPosition >= endTime - 100) {
|
|
626
|
-
currentPosition = (int) endTime;
|
|
627
|
-
} else if (currentPosition <= startTime + 100) {
|
|
628
|
-
currentPosition = (int) startTime;
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
String currentTime = formatTime(currentPosition);
|
|
632
|
-
currentTimeText.setText(currentTime);
|
|
633
|
-
|
|
634
|
-
String startTime = formatTime((int) this.startTime);
|
|
635
|
-
startTimeText.setText(startTime);
|
|
636
|
-
|
|
637
|
-
String endTime = formatTime((int) this.endTime);
|
|
638
|
-
endTimeText.setText(endTime);
|
|
639
|
-
|
|
640
|
-
if (needUpdateProgress) {
|
|
641
|
-
// Update progressIndicator position using zoom-aware calculation
|
|
642
|
-
float indicatorPosition;
|
|
643
|
-
|
|
644
|
-
if (isZoomedIn) {
|
|
645
|
-
// Calculate position relative to zoomed range
|
|
646
|
-
long visibleRangeStart = getVisibleRangeStart();
|
|
647
|
-
long visibleRangeDuration = getVisibleRangeDuration();
|
|
648
|
-
|
|
649
|
-
// Ensure current position is within visible range for proper calculation
|
|
650
|
-
if (currentPosition < visibleRangeStart || currentPosition > visibleRangeStart + visibleRangeDuration) {
|
|
651
|
-
// If current position is outside visible range, clamp it
|
|
652
|
-
currentPosition = (int) Math.max(visibleRangeStart, Math.min(visibleRangeStart + visibleRangeDuration, currentPosition));
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
float ratio = visibleRangeDuration > 0 ? (float) (currentPosition - visibleRangeStart) / visibleRangeDuration : 0;
|
|
656
|
-
indicatorPosition = ratio * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth();
|
|
657
|
-
} else {
|
|
658
|
-
// Calculate position relative to full duration (original logic)
|
|
659
|
-
indicatorPosition = mDuration > 0 ? (float) currentPosition / mDuration * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth() : leadingHandle.getWidth();
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Ensure indicator stays within handle bounds using actual handle positions
|
|
663
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
664
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
665
|
-
|
|
666
|
-
// Apply bounds checking based on actual handle positions
|
|
667
|
-
indicatorPosition = Math.max(leftBoundary, Math.min(rightBoundary, indicatorPosition));
|
|
668
|
-
|
|
669
|
-
if (currentSelectedhandle == leadingHandle) {
|
|
670
|
-
progressIndicator.setX(Math.max(leftBoundary, indicatorPosition));
|
|
671
|
-
} else if (currentSelectedhandle == trailingHandle) {
|
|
672
|
-
progressIndicator.setX(Math.min(rightBoundary, indicatorPosition));
|
|
673
|
-
} else {
|
|
674
|
-
// Normal playback - use calculated position with handle bounds
|
|
675
|
-
progressIndicator.setX(indicatorPosition);
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
private String formatTime(int milliseconds) {
|
|
681
|
-
int totalSeconds = milliseconds / 1000;
|
|
682
|
-
int minutes = totalSeconds / 60;
|
|
683
|
-
int seconds = totalSeconds % 60;
|
|
684
|
-
int millis = milliseconds % 1000;
|
|
685
|
-
return String.format(Locale.getDefault(), "%d:%02d.%03d", minutes, seconds, millis);
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
private void setProgressIndicatorTouchListener() {
|
|
689
|
-
trimmerContainerBg.setOnTouchListener((view, event) -> {
|
|
690
|
-
rangeDragGestureDetector.onTouchEvent(event);
|
|
691
|
-
|
|
692
|
-
switch (event.getAction()) {
|
|
693
|
-
case MotionEvent.ACTION_DOWN:
|
|
694
|
-
isRangeDragging = false;
|
|
695
|
-
didClampWhilePanning = false;
|
|
696
|
-
onMediaPause();
|
|
697
|
-
onTrimmerContainerPanned(event);
|
|
698
|
-
playHapticFeedback(true);
|
|
699
|
-
break;
|
|
700
|
-
case MotionEvent.ACTION_MOVE:
|
|
701
|
-
if (isRangeDragging) {
|
|
702
|
-
onRangeDrag(event);
|
|
703
|
-
} else {
|
|
704
|
-
onTrimmerContainerPanned(event);
|
|
705
|
-
}
|
|
706
|
-
break;
|
|
707
|
-
case MotionEvent.ACTION_UP:
|
|
708
|
-
if (isRangeDragging) {
|
|
709
|
-
isRangeDragging = false;
|
|
710
|
-
fadeInProgressIndicator();
|
|
711
|
-
updateCurrentTime(true);
|
|
712
|
-
}
|
|
713
|
-
view.performClick();
|
|
714
|
-
break;
|
|
715
|
-
default:
|
|
716
|
-
return false;
|
|
717
|
-
}
|
|
718
|
-
return true;
|
|
719
|
-
});
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
private void onTrimmerContainerPanned(MotionEvent event) {
|
|
723
|
-
float newX = event.getRawX();
|
|
724
|
-
boolean didClamp = false;
|
|
725
|
-
|
|
726
|
-
// Use handle positions for boundaries instead of trimmer container
|
|
727
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
728
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
729
|
-
|
|
730
|
-
newX = Math.max(leftBoundary, newX);
|
|
731
|
-
newX = Math.min(rightBoundary, newX);
|
|
732
|
-
|
|
733
|
-
// check play haptic feedback
|
|
734
|
-
if (newX <= leftBoundary) {
|
|
735
|
-
didClamp = true;
|
|
736
|
-
} else if (newX >= rightBoundary) {
|
|
737
|
-
didClamp = true;
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
if (didClamp && !didClampWhilePanning) {
|
|
741
|
-
playHapticFeedback(false);
|
|
742
|
-
}
|
|
743
|
-
didClampWhilePanning = didClamp;
|
|
744
|
-
|
|
745
|
-
progressIndicator.setX(newX);
|
|
746
|
-
|
|
747
|
-
float indicatorPosition = newX - (trimmerContainerBg.getX());
|
|
748
|
-
|
|
749
|
-
// Calculate video position based on zoom state
|
|
750
|
-
float indicatorPositionPercent;
|
|
751
|
-
long newVideoPosition;
|
|
752
|
-
|
|
753
|
-
if (isZoomedIn) {
|
|
754
|
-
// Calculate relative to zoomed range
|
|
755
|
-
indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
|
|
756
|
-
long visibleStart = getVisibleRangeStart();
|
|
757
|
-
long visibleDuration = getVisibleRangeDuration();
|
|
758
|
-
newVideoPosition = visibleStart + (long) (indicatorPositionPercent * visibleDuration);
|
|
759
|
-
} else {
|
|
760
|
-
// Calculate relative to full duration
|
|
761
|
-
indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
|
|
762
|
-
newVideoPosition = (long) (indicatorPositionPercent * mDuration);
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
seekTo(newVideoPosition, false);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
private void onRangeDrag(MotionEvent event) {
|
|
769
|
-
float deltaX = event.getRawX() - rangeDragInitialRawX;
|
|
770
|
-
float containerWidth = trimmerContainerBg.getWidth();
|
|
771
|
-
if (containerWidth <= 0) return;
|
|
772
|
-
|
|
773
|
-
long rangeDuration = rangeDragInitialEndTime - rangeDragInitialStartTime;
|
|
774
|
-
long deltaTime;
|
|
775
|
-
if (isZoomedIn) {
|
|
776
|
-
deltaTime = (long) (deltaX / containerWidth * zoomedInRangeDuration);
|
|
777
|
-
} else {
|
|
778
|
-
deltaTime = (long) (deltaX / containerWidth * mDuration);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
long newStart = rangeDragInitialStartTime + deltaTime;
|
|
782
|
-
long newEnd = newStart + rangeDuration;
|
|
783
|
-
|
|
784
|
-
boolean didClamp = false;
|
|
785
|
-
if (newStart < 0) {
|
|
786
|
-
newStart = 0;
|
|
787
|
-
newEnd = rangeDuration;
|
|
788
|
-
didClamp = true;
|
|
789
|
-
}
|
|
790
|
-
if (newEnd > mDuration) {
|
|
791
|
-
newEnd = mDuration;
|
|
792
|
-
newStart = newEnd - rangeDuration;
|
|
793
|
-
didClamp = true;
|
|
794
|
-
}
|
|
795
|
-
|
|
796
|
-
if (didClamp && !didClampWhilePanning) {
|
|
797
|
-
playHapticFeedback(false);
|
|
798
|
-
}
|
|
799
|
-
didClampWhilePanning = didClamp;
|
|
800
|
-
|
|
801
|
-
startTime = newStart;
|
|
802
|
-
endTime = newEnd;
|
|
803
|
-
updateHandlePositions();
|
|
804
|
-
seekTo(startTime, false);
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
private void setHandleTouchListener(View handle, boolean isLeading) {
|
|
808
|
-
handle.setOnTouchListener((view, event) -> {
|
|
809
|
-
boolean draggingDisabled = mDuration < mMinDuration; // if the video is shorter than the minimum duration, disable dragging
|
|
810
|
-
switch (event.getAction()) {
|
|
811
|
-
case MotionEvent.ACTION_DOWN:
|
|
812
|
-
currentSelectedhandle = handle;
|
|
813
|
-
didClampWhilePanning = false;
|
|
814
|
-
onMediaPause();
|
|
815
|
-
fadeOutProgressIndicator();
|
|
816
|
-
seekTo(isLeading ? startTime : endTime, true);
|
|
817
|
-
playHapticFeedback(true);
|
|
818
|
-
isTrimmingLeading = isLeading;
|
|
819
|
-
break;
|
|
820
|
-
case MotionEvent.ACTION_MOVE:
|
|
821
|
-
if (draggingDisabled) {
|
|
822
|
-
return false;
|
|
823
|
-
}
|
|
824
|
-
|
|
825
|
-
boolean didClamp = false;
|
|
826
|
-
float newX = event.getRawX() - ((float) view.getWidth() / 2);
|
|
827
|
-
|
|
828
|
-
// Handle constraints need to consider zoom state
|
|
829
|
-
if (isLeading) {
|
|
830
|
-
newX = Math.max(0, Math.min(newX, trailingHandle.getX() - view.getWidth()));
|
|
831
|
-
} else {
|
|
832
|
-
newX = Math.min(trimmerContainerBg.getWidth() + view.getWidth(), Math.max(newX, leadingHandle.getX() + view.getWidth()));
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
view.setX(newX);
|
|
836
|
-
|
|
837
|
-
// Calculate new startTime or endTime based on zoom state
|
|
838
|
-
if (isLeading) {
|
|
839
|
-
// Calculate the new startTime based on the handle's new position
|
|
840
|
-
long newStartTime = timeForPosition(newX);
|
|
841
|
-
// Calculate the duration between the new startTime and the current endTime
|
|
842
|
-
long duration = endTime - newStartTime;
|
|
843
|
-
if (duration >= mMinDuration && duration <= mMaxDuration) {
|
|
844
|
-
// If the duration is within the allowed range, update startTime and move the progress indicator
|
|
845
|
-
startTime = newStartTime;
|
|
846
|
-
float indicatorX = newX + view.getWidth();
|
|
847
|
-
// Ensure progress indicator stays within handle bounds
|
|
848
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
849
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
850
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
851
|
-
} else if (duration < mMinDuration) {
|
|
852
|
-
didClamp = true;
|
|
853
|
-
// If the duration is less than the minimum, calculate maximum startTime to maintain minimum duration
|
|
854
|
-
startTime = endTime - mMinDuration;
|
|
855
|
-
|
|
856
|
-
// In zoom mode, don't recalculate position - keep handle where it is but update times
|
|
857
|
-
if (isZoomedIn) {
|
|
858
|
-
// Keep handle at current position but clamp times
|
|
859
|
-
float indicatorX = newX + view.getWidth();
|
|
860
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
861
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
862
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
863
|
-
} else {
|
|
864
|
-
// Normal mode - adjust handle position
|
|
865
|
-
view.setX(positionForTime(startTime));
|
|
866
|
-
float indicatorX = view.getX() + view.getWidth();
|
|
867
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
868
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
869
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
870
|
-
}
|
|
871
|
-
} else {
|
|
872
|
-
didClamp = true;
|
|
873
|
-
// If the duration is greater than the maximum, calculate minimum startTime to maintain maximum duration
|
|
874
|
-
startTime = endTime - mMaxDuration;
|
|
875
|
-
|
|
876
|
-
// In zoom mode, don't recalculate position - keep handle where it is but update times
|
|
877
|
-
if (isZoomedIn) {
|
|
878
|
-
// Keep handle at current position but clamp times
|
|
879
|
-
float indicatorX = newX + view.getWidth();
|
|
880
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
881
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
882
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
883
|
-
} else {
|
|
884
|
-
// Normal mode - adjust handle position
|
|
885
|
-
view.setX(positionForTime(startTime));
|
|
886
|
-
float indicatorX = view.getX() + view.getWidth();
|
|
887
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
888
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
889
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
} else {
|
|
893
|
-
// Calculate the new endTime based on the handle's new position
|
|
894
|
-
long newEndTime = timeForPosition(newX - view.getWidth());
|
|
895
|
-
// Calculate the duration between the new endTime and the current startTime
|
|
896
|
-
long duration = newEndTime - startTime;
|
|
897
|
-
if (duration >= mMinDuration && duration <= mMaxDuration) {
|
|
898
|
-
// If the duration is within the allowed range, update endTime and move the progress indicator
|
|
899
|
-
endTime = newEndTime;
|
|
900
|
-
float indicatorX = newX - progressIndicator.getWidth();
|
|
901
|
-
// Ensure progress indicator stays within handle bounds
|
|
902
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
903
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
904
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
905
|
-
} else if (duration < mMinDuration) {
|
|
906
|
-
didClamp = true;
|
|
907
|
-
// If the duration is less than the minimum, calculate minimum endTime to maintain minimum duration
|
|
908
|
-
endTime = startTime + mMinDuration;
|
|
909
|
-
|
|
910
|
-
// In zoom mode, don't recalculate position - keep handle where it is but update times
|
|
911
|
-
if (isZoomedIn) {
|
|
912
|
-
// Keep handle at current position but clamp times
|
|
913
|
-
float indicatorX = newX - progressIndicator.getWidth();
|
|
914
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
915
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
916
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
917
|
-
} else {
|
|
918
|
-
// Normal mode - adjust handle position
|
|
919
|
-
view.setX(positionForTime(endTime) + view.getWidth());
|
|
920
|
-
float indicatorX = view.getX() - progressIndicator.getWidth();
|
|
921
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
922
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
923
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
924
|
-
}
|
|
925
|
-
} else {
|
|
926
|
-
didClamp = true;
|
|
927
|
-
// If the duration is greater than the maximum, calculate maximum endTime to maintain maximum duration
|
|
928
|
-
endTime = startTime + mMaxDuration;
|
|
929
|
-
|
|
930
|
-
// In zoom mode, don't recalculate position - keep handle where it is but update times
|
|
931
|
-
if (isZoomedIn) {
|
|
932
|
-
// Keep handle at current position but clamp times
|
|
933
|
-
float indicatorX = newX - progressIndicator.getWidth();
|
|
934
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
935
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
936
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
937
|
-
} else {
|
|
938
|
-
// Normal mode - adjust handle position
|
|
939
|
-
view.setX(positionForTime(endTime) + view.getWidth());
|
|
940
|
-
float indicatorX = view.getX() - progressIndicator.getWidth();
|
|
941
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
942
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
943
|
-
progressIndicator.setX(Math.max(leftBoundary, Math.min(rightBoundary, indicatorX)));
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
|
|
948
|
-
if (didClamp && !didClampWhilePanning) {
|
|
949
|
-
playHapticFeedback(false);
|
|
950
|
-
}
|
|
951
|
-
didClampWhilePanning = didClamp;
|
|
952
|
-
|
|
953
|
-
updateTrimmerContainerWidth();
|
|
954
|
-
seekTo(isLeading ? startTime : endTime, false);
|
|
955
|
-
|
|
956
|
-
// Start zoom wait timer when dragging handles
|
|
957
|
-
startZoomWaitTimer();
|
|
958
|
-
break;
|
|
959
|
-
case MotionEvent.ACTION_UP:
|
|
960
|
-
stopZoomIfNeeded();
|
|
961
|
-
fadeInProgressIndicator();
|
|
962
|
-
view.performClick();
|
|
963
|
-
break;
|
|
964
|
-
default:
|
|
965
|
-
return false;
|
|
966
|
-
}
|
|
967
|
-
return true;
|
|
968
|
-
});
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
private void fadeOutProgressIndicator() {
|
|
972
|
-
progressIndicator.animate().alpha(0f).setDuration(250).withEndAction(() -> progressIndicator.setVisibility(View.INVISIBLE)).start();
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
private void fadeInProgressIndicator() {
|
|
976
|
-
progressIndicator.setVisibility(View.VISIBLE);
|
|
977
|
-
progressIndicator.animate().alpha(1f).setDuration(250).start();
|
|
978
|
-
}
|
|
979
|
-
|
|
980
|
-
private void updateTrimmerContainerWidth() {
|
|
981
|
-
int left = (int) leadingHandle.getX() + leadingHandle.getWidth();
|
|
982
|
-
int right = trimmerContainerBg.getWidth() - (int) trailingHandle.getX() + 2 * trailingHandle.getWidth();
|
|
983
|
-
|
|
984
|
-
RelativeLayout.LayoutParams leadingOverlayParams = (RelativeLayout.LayoutParams) leadingOverlay.getLayoutParams();
|
|
985
|
-
leadingOverlayParams.width = left;
|
|
986
|
-
leadingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
|
|
987
|
-
leadingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_START);
|
|
988
|
-
leadingOverlay.setLayoutParams(leadingOverlayParams);
|
|
989
|
-
|
|
990
|
-
RelativeLayout.LayoutParams trailingOverlayParams = (RelativeLayout.LayoutParams) trailingOverlay.getLayoutParams();
|
|
991
|
-
trailingOverlayParams.width = right;
|
|
992
|
-
trailingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
|
|
993
|
-
trailingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_END);
|
|
994
|
-
trailingOverlay.setLayoutParams(trailingOverlayParams);
|
|
995
|
-
}
|
|
996
|
-
|
|
997
|
-
private void playHapticFeedback(boolean isLight) {
|
|
998
|
-
if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && enableHapticFeedback) {
|
|
999
|
-
vibrator.vibrate(VibrationEffect.createOneShot(isLight ? 10 : 25, VibrationEffect.DEFAULT_AMPLITUDE)); // Light vibration
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
private void startZoomWaitTimer() {
|
|
1004
|
-
stopZoomWaitTimer();
|
|
1005
|
-
if (isZoomedIn) {
|
|
1006
|
-
return;
|
|
1007
|
-
}
|
|
1008
|
-
|
|
1009
|
-
zoomRunnable = () -> {
|
|
1010
|
-
stopZoomWaitTimer();
|
|
1011
|
-
zoomIfNeeded();
|
|
1012
|
-
};
|
|
1013
|
-
|
|
1014
|
-
zoomWaitTimer.postDelayed(zoomRunnable, 500);
|
|
1015
|
-
}
|
|
1016
|
-
|
|
1017
|
-
private void stopZoomWaitTimer() {
|
|
1018
|
-
if (zoomRunnable != null) {
|
|
1019
|
-
zoomWaitTimer.removeCallbacks(zoomRunnable);
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
private void stopZoomIfNeeded() {
|
|
1024
|
-
stopZoomWaitTimer();
|
|
1025
|
-
if (isZoomedIn) {
|
|
1026
|
-
// Stop any ongoing thumbnail generation immediately
|
|
1027
|
-
isGeneratingThumbnails = false;
|
|
1028
|
-
|
|
1029
|
-
// Cancel any ongoing background tasks for thumbnail generation
|
|
1030
|
-
BackgroundExecutor.cancelAll("progressive_thumbs", true);
|
|
1031
|
-
|
|
1032
|
-
isZoomedIn = false;
|
|
1033
|
-
|
|
1034
|
-
// Immediately restore cached thumbnails without waiting for animation
|
|
1035
|
-
restoreCachedThumbnails();
|
|
1036
|
-
|
|
1037
|
-
// Then apply smooth transition animation
|
|
1038
|
-
animateZoomTransition(() -> {
|
|
1039
|
-
updateHandlePositions();
|
|
1040
|
-
// Force update progress indicator position after exiting zoom
|
|
1041
|
-
updateCurrentTime(true);
|
|
1042
|
-
});
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
|
-
private void zoomIfNeeded() {
|
|
1047
|
-
if (isZoomedIn) {
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
|
|
1051
|
-
// Store current handle positions to maintain visual continuity
|
|
1052
|
-
float currentLeadingX = leadingHandle.getX();
|
|
1053
|
-
float currentTrailingX = trailingHandle.getX();
|
|
1054
|
-
|
|
1055
|
-
// Use configurable zoom duration, but ensure it's reasonable for the video
|
|
1056
|
-
long newDuration = Math.min(zoomOnWaitingDuration, mDuration);
|
|
1057
|
-
|
|
1058
|
-
// For very short videos, use a smaller zoom range
|
|
1059
|
-
if (mDuration < 2000) {
|
|
1060
|
-
newDuration = Math.max(500, mDuration / 2); // At least 0.5 seconds for very short videos
|
|
1061
|
-
} else if (mDuration < zoomOnWaitingDuration) {
|
|
1062
|
-
newDuration = Math.max(1000, mDuration / 2); // Use half duration for short videos
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
// Ensure zoom duration doesn't exceed video duration
|
|
1066
|
-
newDuration = Math.min(newDuration, mDuration);
|
|
1067
|
-
|
|
1068
|
-
long rangeStart;
|
|
1069
|
-
if (isTrimmingLeading) {
|
|
1070
|
-
// Zoom around the start time, but ensure we don't go before video start
|
|
1071
|
-
rangeStart = Math.max(0, startTime - (newDuration / 2));
|
|
1072
|
-
// If range would extend past video end, adjust start
|
|
1073
|
-
if (rangeStart + newDuration > mDuration) {
|
|
1074
|
-
rangeStart = Math.max(0, mDuration - newDuration);
|
|
1075
|
-
}
|
|
1076
|
-
} else {
|
|
1077
|
-
// Zoom around the end time
|
|
1078
|
-
rangeStart = Math.max(0, endTime - (newDuration / 2));
|
|
1079
|
-
// If range would extend past video end, adjust start
|
|
1080
|
-
if (rangeStart + newDuration > mDuration) {
|
|
1081
|
-
rangeStart = Math.max(0, mDuration - newDuration);
|
|
1082
|
-
}
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
// Final bounds check
|
|
1086
|
-
zoomedInRangeStart = Math.max(0, rangeStart);
|
|
1087
|
-
zoomedInRangeDuration = Math.min(newDuration, mDuration - zoomedInRangeStart);
|
|
1088
|
-
|
|
1089
|
-
isZoomedIn = true;
|
|
1090
|
-
|
|
1091
|
-
// Start progressive thumbnail generation immediately
|
|
1092
|
-
startProgressiveThumbnailGeneration();
|
|
1093
|
-
|
|
1094
|
-
// Update handle positions immediately without delay
|
|
1095
|
-
updateHandlePositionsForZoom(currentLeadingX, currentTrailingX);
|
|
1096
|
-
|
|
1097
|
-
// Provide haptic feedback
|
|
1098
|
-
playHapticFeedback(true);
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
private void updateHandlePositionsForZoom(float previousLeadingX, float previousTrailingX) {
|
|
1102
|
-
// During zoom, we want to keep handles at their current visual positions
|
|
1103
|
-
// Don't recalculate based on zoom range - this causes jumping
|
|
1104
|
-
|
|
1105
|
-
Log.d(TAG, "Maintaining handle positions during zoom - Leading: " + previousLeadingX + ", Trailing: " + previousTrailingX);
|
|
1106
|
-
|
|
1107
|
-
// Keep handles exactly where they were visually
|
|
1108
|
-
leadingHandle.setX(previousLeadingX);
|
|
1109
|
-
trailingHandle.setX(previousTrailingX);
|
|
1110
|
-
|
|
1111
|
-
// Don't update times here - let the individual handle drag logic handle that
|
|
1112
|
-
// This prevents unwanted changes to startTime/endTime during zoom transition
|
|
1113
|
-
|
|
1114
|
-
// Update trimmer container width based on current handle positions
|
|
1115
|
-
updateTrimmerContainerWidth();
|
|
1116
|
-
|
|
1117
|
-
// Ensure progress indicator is positioned correctly within the handle bounds
|
|
1118
|
-
float leftBoundary = leadingHandle.getX() + leadingHandle.getWidth();
|
|
1119
|
-
float rightBoundary = trailingHandle.getX() - progressIndicator.getWidth();
|
|
1120
|
-
float currentX = progressIndicator.getX();
|
|
1121
|
-
|
|
1122
|
-
// If progress indicator is out of bounds, position it properly
|
|
1123
|
-
if (currentX < leftBoundary || currentX > rightBoundary) {
|
|
1124
|
-
// Position it based on the current media position
|
|
1125
|
-
updateCurrentTime(true);
|
|
1126
|
-
} else {
|
|
1127
|
-
updateCurrentTime(false);
|
|
1128
|
-
}
|
|
1129
|
-
|
|
1130
|
-
trimmerContainerWrapper.setVisibility(View.VISIBLE);
|
|
1131
|
-
if (trimmerContainerWrapper.getAlpha() == 0f) {
|
|
1132
|
-
trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start();
|
|
1133
|
-
}
|
|
1134
|
-
}
|
|
1135
|
-
|
|
1136
|
-
private void startProgressiveThumbnailGeneration() {
|
|
1137
|
-
if (isGeneratingThumbnails || mediaMetadataRetriever == null) {
|
|
1138
|
-
return;
|
|
1139
|
-
}
|
|
1140
|
-
|
|
1141
|
-
isGeneratingThumbnails = true;
|
|
1142
|
-
|
|
1143
|
-
// Immediately create placeholder thumbnails with subtle animation
|
|
1144
|
-
UiThreadExecutor.runTask("", () -> {
|
|
1145
|
-
mThumbnailContainer.removeAllViews();
|
|
1146
|
-
|
|
1147
|
-
// Calculate proper number of thumbnails based on container width
|
|
1148
|
-
final int thumbnailWidth = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
|
|
1149
|
-
final int numberOfThumbnails = Math.max(8, mThumbnailContainer.getWidth() / thumbnailWidth);
|
|
1150
|
-
|
|
1151
|
-
// Create placeholder thumbnails first
|
|
1152
|
-
for (int i = 0; i < numberOfThumbnails; i++) {
|
|
1153
|
-
ImageView placeholder = new ImageView(getContext());
|
|
1154
|
-
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(thumbnailWidth, LinearLayout.LayoutParams.MATCH_PARENT);
|
|
1155
|
-
placeholder.setLayoutParams(layoutParams);
|
|
1156
|
-
placeholder.setBackgroundColor(Color.parseColor("#F0F0F0")); // Light gray placeholder
|
|
1157
|
-
placeholder.setAlpha(0.2f);
|
|
1158
|
-
mThumbnailContainer.addView(placeholder);
|
|
1159
|
-
}
|
|
1160
|
-
}, 0);
|
|
1161
|
-
|
|
1162
|
-
// Start background thumbnail generation
|
|
1163
|
-
BackgroundExecutor.execute(new BackgroundExecutor.Task("progressive_thumbs", 0L, "") {
|
|
1164
|
-
@Override
|
|
1165
|
-
public void execute() {
|
|
1166
|
-
try {
|
|
1167
|
-
final int thumbnailWidth = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
|
|
1168
|
-
final int numberOfThumbnails = Math.max(8, mThumbnailContainer.getWidth() / thumbnailWidth);
|
|
1169
|
-
final long visibleDuration = isZoomedIn ? zoomedInRangeDuration : mDuration;
|
|
1170
|
-
final long visibleStart = isZoomedIn ? zoomedInRangeStart : 0;
|
|
1171
|
-
final long interval = visibleDuration > 0 ? visibleDuration / numberOfThumbnails : 0;
|
|
1172
|
-
|
|
1173
|
-
// Generate thumbnails progressively
|
|
1174
|
-
for (int i = 0; i < numberOfThumbnails && isGeneratingThumbnails && isZoomedIn; i++) {
|
|
1175
|
-
// Check if we should continue generating
|
|
1176
|
-
if (!isGeneratingThumbnails || !isZoomedIn) {
|
|
1177
|
-
Log.d(TAG, "Thumbnail generation cancelled at index " + i);
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
final int index = i;
|
|
1182
|
-
final long timeUs = (visibleStart + (i * interval)) * 1000; // Convert to microseconds
|
|
1183
|
-
final long clampedTimeUs = Math.max(0, Math.min(timeUs, mDuration * 1000L));
|
|
1184
|
-
|
|
1185
|
-
try {
|
|
1186
|
-
Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(clampedTimeUs, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
|
|
1187
|
-
if (bitmap != null && isGeneratingThumbnails && isZoomedIn) {
|
|
1188
|
-
// Update UI immediately for each thumbnail
|
|
1189
|
-
UiThreadExecutor.runTask("", () -> {
|
|
1190
|
-
// Double-check zoom state before updating UI
|
|
1191
|
-
if (isZoomedIn && index < mThumbnailContainer.getChildCount()) {
|
|
1192
|
-
ImageView thumbnailView = (ImageView) mThumbnailContainer.getChildAt(index);
|
|
1193
|
-
if (thumbnailView != null) {
|
|
1194
|
-
thumbnailView.setImageBitmap(bitmap);
|
|
1195
|
-
thumbnailView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
1196
|
-
thumbnailView.setBackground(null); // Remove placeholder background
|
|
1197
|
-
|
|
1198
|
-
// Smooth fade-in animation for each thumbnail
|
|
1199
|
-
thumbnailView.animate()
|
|
1200
|
-
.alpha(1.0f)
|
|
1201
|
-
.setDuration(150)
|
|
1202
|
-
.setStartDelay(index * 50L) // Stagger animations
|
|
1203
|
-
.start();
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}, 0);
|
|
1207
|
-
|
|
1208
|
-
// Small delay between generations to avoid blocking
|
|
1209
|
-
Thread.sleep(10);
|
|
1210
|
-
}
|
|
1211
|
-
} catch (Exception e) {
|
|
1212
|
-
Log.w(TAG, "Error generating progressive thumbnail at " + clampedTimeUs, e);
|
|
1213
|
-
}
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
// Only finalize if we're still in zoom mode
|
|
1217
|
-
if (isGeneratingThumbnails && isZoomedIn) {
|
|
1218
|
-
isGeneratingThumbnails = false;
|
|
1219
|
-
} else {
|
|
1220
|
-
isGeneratingThumbnails = false;
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
} catch (Exception e) {
|
|
1224
|
-
Log.e(TAG, "Error in progressive thumbnail generation", e);
|
|
1225
|
-
isGeneratingThumbnails = false;
|
|
1226
|
-
}
|
|
1227
|
-
}
|
|
1228
|
-
});
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
private void animateZoomTransition(Runnable onComplete) {
|
|
1232
|
-
// Only animate if we're still transitioning
|
|
1233
|
-
if (mThumbnailContainer != null) {
|
|
1234
|
-
mThumbnailContainer.animate()
|
|
1235
|
-
.alpha(0.7f)
|
|
1236
|
-
.setDuration(200) // Shorter duration for better responsiveness
|
|
1237
|
-
.withEndAction(() -> {
|
|
1238
|
-
if (onComplete != null) {
|
|
1239
|
-
onComplete.run();
|
|
1240
|
-
}
|
|
1241
|
-
if (mThumbnailContainer != null) {
|
|
1242
|
-
mThumbnailContainer.animate()
|
|
1243
|
-
.alpha(1.0f)
|
|
1244
|
-
.setDuration(200)
|
|
1245
|
-
.start();
|
|
1246
|
-
}
|
|
1247
|
-
})
|
|
1248
|
-
.start();
|
|
1249
|
-
} else if (onComplete != null) {
|
|
1250
|
-
onComplete.run();
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
private void ignoreSystemGestureForView(View v) {
|
|
1255
|
-
// Android 10+
|
|
1256
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
1257
|
-
// 1. setSystemGestureExclusionRects on a rect which is entire screen size
|
|
1258
|
-
// 2. if we use the bound of the view, it's not smooth and still sometimes can trigger system gesture even our finger is within the view bound
|
|
1259
|
-
// 3. even though here the bound is set to screen size, it doesn't mean the entire screen is excluded from system gesture, it only means the area within the view bound is excluded
|
|
1260
|
-
v.setSystemGestureExclusionRects(
|
|
1261
|
-
java.util.List.of(
|
|
1262
|
-
new android.graphics.Rect(
|
|
1263
|
-
0,
|
|
1264
|
-
0,
|
|
1265
|
-
DeviceUtil.getDeviceWidth(),
|
|
1266
|
-
DeviceUtil.getDeviceHeight()
|
|
1267
|
-
)
|
|
1268
|
-
)
|
|
1269
|
-
);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// Helper methods for position/time conversion considering zoom state
|
|
1274
|
-
private long timeForPosition(float position) {
|
|
1275
|
-
if (trimmerContainerBg.getWidth() <= 0) return 0;
|
|
1276
|
-
|
|
1277
|
-
if (isZoomedIn) {
|
|
1278
|
-
// Convert position to time within zoomed range
|
|
1279
|
-
float ratio = position / trimmerContainerBg.getWidth();
|
|
1280
|
-
return zoomedInRangeStart + (long) (ratio * zoomedInRangeDuration);
|
|
1281
|
-
} else {
|
|
1282
|
-
// Convert position to time within full duration
|
|
1283
|
-
return (long) ((position / trimmerContainerBg.getWidth()) * mDuration);
|
|
1284
|
-
}
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
private float positionForTime(long time) {
|
|
1288
|
-
if (isZoomedIn) {
|
|
1289
|
-
// Convert time to position within zoomed range
|
|
1290
|
-
if (zoomedInRangeDuration <= 0) return 0;
|
|
1291
|
-
float ratio = (float) (time - zoomedInRangeStart) / zoomedInRangeDuration;
|
|
1292
|
-
return Math.max(0, Math.min(trimmerContainerBg.getWidth(), ratio * trimmerContainerBg.getWidth()));
|
|
1293
|
-
} else {
|
|
1294
|
-
// Convert time to position within full duration
|
|
1295
|
-
if (mDuration <= 0) return 0;
|
|
1296
|
-
return Math.max(0, Math.min(trimmerContainerBg.getWidth(), ((float) time / mDuration) * trimmerContainerBg.getWidth()));
|
|
1297
|
-
}
|
|
1298
|
-
}
|
|
1299
|
-
|
|
1300
|
-
private long getVisibleRangeStart() {
|
|
1301
|
-
return isZoomedIn ? zoomedInRangeStart : 0;
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
|
-
private long getVisibleRangeDuration() {
|
|
1305
|
-
return isZoomedIn ? zoomedInRangeDuration : mDuration;
|
|
1306
|
-
}
|
|
1307
|
-
|
|
1308
|
-
private void restoreCachedThumbnails() {
|
|
1309
|
-
// Clear current thumbnails
|
|
1310
|
-
mThumbnailContainer.removeAllViews();
|
|
1311
|
-
|
|
1312
|
-
// Restore cached thumbnails efficiently
|
|
1313
|
-
for (ImageView cachedThumbnail : cachedFullViewThumbnails) {
|
|
1314
|
-
// Create a new ImageView with the same bitmap to avoid view reuse issues
|
|
1315
|
-
ImageView restoredView = new ImageView(getContext());
|
|
1316
|
-
restoredView.setImageBitmap(((android.graphics.drawable.BitmapDrawable) cachedThumbnail.getDrawable()).getBitmap());
|
|
1317
|
-
restoredView.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
|
1318
|
-
restoredView.setLayoutParams(cachedThumbnail.getLayoutParams());
|
|
1319
|
-
mThumbnailContainer.addView(restoredView);
|
|
1320
|
-
}
|
|
1321
|
-
}
|
|
1322
|
-
}
|