react-native-video-trim 1.0.24 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +55 -9
  2. package/android/src/main/AndroidManifest.xml +15 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +191 -63
  4. package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
  5. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +6 -1
  6. package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
  7. package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
  8. package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +36 -8
  9. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +588 -308
  10. package/android/src/main/res/drawable/airpodsmax.xml +19 -0
  11. package/android/src/main/res/drawable/chevron_compact_left.xml +15 -0
  12. package/android/src/main/res/drawable/chevron_compact_right.xml +15 -0
  13. package/android/src/main/res/drawable/chevron_right_with_bg.xml +13 -0
  14. package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
  15. package/android/src/main/res/drawable/pause_fill.xml +15 -0
  16. package/android/src/main/res/drawable/play_fill.xml +15 -0
  17. package/android/src/main/res/drawable/rounded_progress_indicator.xml +16 -0
  18. package/android/src/main/res/drawable/rounded_yellow_left_background.xml +8 -0
  19. package/android/src/main/res/drawable/rounded_yellow_right_background.xml +8 -0
  20. package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
  21. package/android/src/main/res/drawable/yellow_border.xml +9 -0
  22. package/android/src/main/res/layout/video_trimmer_view.xml +194 -75
  23. package/android/src/main/res/values/colors.xml +15 -13
  24. package/android/src/main/res/xml/file_paths.xml +5 -0
  25. package/ios/AssetLoader.swift +99 -0
  26. package/ios/ErrorCode.swift +16 -0
  27. package/ios/VideoTrim.mm +4 -2
  28. package/ios/VideoTrim.swift +405 -168
  29. package/ios/VideoTrimmer.swift +16 -10
  30. package/ios/VideoTrimmerViewController.swift +79 -13
  31. package/lib/commonjs/index.js +20 -57
  32. package/lib/commonjs/index.js.map +1 -1
  33. package/lib/module/index.js +19 -57
  34. package/lib/module/index.js.map +1 -1
  35. package/lib/typescript/index.d.ts +47 -9
  36. package/lib/typescript/index.d.ts.map +1 -1
  37. package/package.json +1 -1
  38. package/src/index.tsx +56 -66
  39. package/android/src/main/java/com/videotrim/adapters/VideoTrimmerAdapter.java +0 -54
  40. package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +0 -534
  41. package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +0 -33
  42. package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +0 -48
  43. package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
  44. package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
  45. package/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
  46. package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
  47. package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
  48. package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
  49. package/android/src/main/res/layout/video_thumb_item_layout.xml +0 -16
@@ -1,90 +1,111 @@
1
1
  package com.videotrim.widgets;
2
2
 
3
+ import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
4
+ import static com.videotrim.utils.VideoTrimmerUtil.DEFAULT_AUDIO_EXTENSION;
3
5
  import static com.videotrim.utils.VideoTrimmerUtil.RECYCLER_VIEW_PADDING;
4
6
  import static com.videotrim.utils.VideoTrimmerUtil.VIDEO_FRAMES_WIDTH;
5
7
 
6
- import android.animation.ValueAnimator;
7
8
  import android.content.Context;
8
9
  import android.content.pm.ActivityInfo;
9
10
  import android.content.res.Configuration;
10
11
  import android.graphics.Bitmap;
12
+ import android.graphics.Color;
13
+ import android.graphics.drawable.Drawable;
14
+ import android.graphics.drawable.GradientDrawable;
11
15
  import android.media.MediaMetadataRetriever;
12
16
  import android.media.MediaPlayer;
13
17
  import android.net.Uri;
18
+ import android.os.Build;
14
19
  import android.os.Handler;
20
+ import android.os.VibrationEffect;
21
+ import android.os.Vibrator;
15
22
  import android.util.AttributeSet;
23
+ import android.util.Log;
16
24
  import android.view.LayoutInflater;
17
25
  import android.view.MotionEvent;
18
26
  import android.view.View;
19
27
  import android.view.ViewGroup;
20
- import android.view.animation.LinearInterpolator;
21
28
  import android.widget.FrameLayout;
22
29
  import android.widget.ImageView;
23
30
  import android.widget.LinearLayout;
31
+ import android.widget.ProgressBar;
24
32
  import android.widget.RelativeLayout;
25
33
  import android.widget.TextView;
26
- import android.widget.Toast;
34
+ import android.widget.VideoView;
27
35
 
28
- import androidx.annotation.NonNull;
29
- import androidx.recyclerview.widget.LinearLayoutManager;
30
- import androidx.recyclerview.widget.RecyclerView;
36
+ import androidx.core.content.ContextCompat;
37
+ import androidx.core.content.res.ResourcesCompat;
31
38
 
32
39
  import com.facebook.react.bridge.ReactApplicationContext;
33
40
  import com.facebook.react.bridge.ReadableMap;
34
41
  import com.videotrim.R;
35
- import com.videotrim.adapters.VideoTrimmerAdapter;
42
+ import com.videotrim.enums.ErrorCode;
36
43
  import com.videotrim.interfaces.IVideoTrimmerView;
37
44
  import com.videotrim.interfaces.VideoTrimListener;
45
+ import com.videotrim.utils.MediaMetadataUtil;
38
46
  import com.videotrim.utils.StorageUtil;
39
47
  import com.videotrim.utils.VideoTrimmerUtil;
40
48
 
49
+ import java.io.IOException;
50
+ import java.util.Locale;
51
+
41
52
  import iknow.android.utils.DeviceUtil;
42
53
  import iknow.android.utils.thread.BackgroundExecutor;
43
54
  import iknow.android.utils.thread.UiThreadExecutor;
44
55
 
45
- /**
46
- * Author:J.Chou
47
- * Date: 2016.08.01 2:23 PM
48
- * Email: who_know_me@163.com
49
- * Describe:
50
- */
51
56
  public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
52
57
 
53
58
  private static final String TAG = VideoTrimmerView.class.getSimpleName();
54
59
 
55
- private final int mMaxWidth = VIDEO_FRAMES_WIDTH;
56
60
  private ReactApplicationContext mContext;
57
- private RelativeLayout mLinearVideo;
58
- private ZVideoView mVideoView;
61
+ private VideoView mVideoView;
59
62
  private ImageView mPlayView;
60
- private RecyclerView mVideoThumbRecyclerView;
61
- private RangeSeekBarView mRangeSeekBarView;
62
- private LinearLayout mSeekBarLayout;
63
- private ImageView mRedProgressIcon;
64
- private float mAverageMsPx;//每毫秒所占的px
65
- private float averagePxMs;//每px所占用的ms毫秒
63
+ private LinearLayout mThumbnailContainer;
66
64
  private Uri mSourceUri;
67
65
  private VideoTrimListener mOnTrimVideoListener;
68
66
  private int mDuration = 0;
69
- private VideoTrimmerAdapter mVideoThumbAdapter;
70
- private boolean isFromRestore = false;
71
- //new
72
- private long mLeftProgressPos, mRightProgressPos;
73
- private long mRedProgressBarPos = 0;
74
- private long scrollPos = 0;
75
- private int mScaledTouchSlop;
76
- private int lastScrollX;
77
- private int mThumbsTotalCount;
78
- private ValueAnimator mRedProgressAnimator;
79
- private final Handler mAnimationHandler = new Handler();
80
67
  private Boolean mIsPrepared = false;
81
- private long mMaxDuration = VideoTrimmerUtil.MAX_SHOOT_DURATION;
68
+ private long mMaxDuration = (long) Double.POSITIVE_INFINITY;
82
69
  private long mMinDuration = VideoTrimmerUtil.MIN_SHOOT_DURATION;
83
70
 
71
+ private final Handler mTimingHandler = new Handler();
72
+ private Runnable mTimingRunnable;
73
+ private static final long TIMING_UPDATE_INTERVAL = 30; // Update every 30 milliseconds
74
+ private TextView currentTimeText;
75
+ private TextView startTimeText;
76
+ private TextView endTimeText;
77
+ private View progressIndicator;
78
+ private View trimmerContainer;
79
+ // background of the trimmer container, its width never changes
80
+ // 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
81
+ private View trimmerContainerBg;
82
+ private FrameLayout leadingHandle;
83
+ private View trailingHandle;
84
+ private View leadingOverlay;
85
+ private View trailingOverlay;
86
+ private RelativeLayout trimmerContainerWrapper;
87
+
88
+ private long startTime = 0, endTime = 0;
89
+ private Vibrator vibrator;
90
+ private boolean didClampWhilePanning = false;
91
+
92
+ // zoom
93
+ private boolean isZoomedIn = false;
94
+ private final Handler zoomWaitTimer = new Handler();
95
+ private Runnable zoomRunnable;
96
+
97
+ private MediaMetadataRetriever mediaMetadataRetriever;
98
+ private ProgressBar loadingIndicator;
99
+ private TextView saveBtn;
100
+ private TextView cancelBtn;
101
+ private FrameLayout audioBannerView;
102
+ private boolean isVideoType = true;
103
+ private MediaPlayer audioPlayer;
104
+ private ImageView failToLoadBtn;
105
+
106
+ private String mOutputExt = "mp4";
107
+ private boolean enableHapticFeedback = true;
84
108
 
85
- public VideoTrimmerView(ReactApplicationContext context, AttributeSet attrs) {
86
- this(context, attrs, 0, null);
87
- }
88
109
  public VideoTrimmerView(ReactApplicationContext context, ReadableMap config, AttributeSet attrs) {
89
110
  this(context, attrs, 0, config);
90
111
  }
@@ -97,139 +118,246 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
97
118
  private void init(ReactApplicationContext context, ReadableMap config) {
98
119
  this.mContext = context;
99
120
 
100
- // listen to onConfigurationChanged doesn't work for this, it runs too soon
101
121
  context.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
102
122
  LayoutInflater.from(context).inflate(R.layout.video_trimmer_view, this, true);
123
+ vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
103
124
 
104
- mLinearVideo = findViewById(R.id.layout_surface_view);
105
- mVideoView = findViewById(R.id.video_loader);
106
- mPlayView = findViewById(R.id.icon_video_play);
107
- mSeekBarLayout = findViewById(R.id.seekBarLayout);
108
- mRedProgressIcon = findViewById(R.id.positionIcon);
109
- mVideoThumbRecyclerView = findViewById(R.id.video_frames_recyclerView);
110
- mVideoThumbRecyclerView.setLayoutManager(new LinearLayoutManager(mContext, LinearLayoutManager.HORIZONTAL, false));
111
- mVideoThumbAdapter = new VideoTrimmerAdapter(mContext);
112
- mVideoThumbRecyclerView.setAdapter(mVideoThumbAdapter);
113
- mVideoThumbRecyclerView.addOnScrollListener(mOnScrollListener);
114
-
125
+ initializeViews();
115
126
  configure(config);
116
127
  setUpListeners();
128
+ setProgressIndicatorTouchListener();
117
129
  }
118
130
 
119
- private void initRangeSeekBarView() {
120
- if(mRangeSeekBarView != null) return;
121
- mLeftProgressPos = 0;
131
+ private void initializeViews() {
132
+ mThumbnailContainer = findViewById(R.id.thumbnailContainer);
133
+ mVideoView = findViewById(R.id.video_loader);
134
+ mPlayView = findViewById(R.id.icon_video_play);
135
+ startTimeText = findViewById(R.id.startTime);
136
+ currentTimeText = findViewById(R.id.currentTime);
137
+ endTimeText = findViewById(R.id.endTime);
138
+ progressIndicator = findViewById(R.id.progressIndicator);
139
+ trimmerContainer = findViewById(R.id.trimmerContainer);
140
+ trimmerContainerBg = findViewById(R.id.trimmerContainerBg);
141
+ leadingHandle = findViewById(R.id.leadingHandle);
142
+ trailingHandle = findViewById(R.id.trailingHandle);
143
+ leadingOverlay = findViewById(R.id.leadingOverlay);
144
+ trailingOverlay = findViewById(R.id.trailingOverlay);
145
+
146
+ trimmerContainerWrapper = findViewById(R.id.trimmerContainerWrapper);
147
+ trimmerContainerWrapper.setVisibility(View.INVISIBLE);
148
+ trimmerContainerWrapper.setAlpha(0f);
149
+
150
+ loadingIndicator = findViewById(R.id.loadingIndicator);
151
+ saveBtn = findViewById(R.id.saveBtn);
152
+ cancelBtn = findViewById(R.id.cancelBtn);
153
+ audioBannerView = findViewById(R.id.audioBannerView);
154
+ failToLoadBtn = findViewById(R.id.failToLoadBtn);
155
+ }
122
156
 
123
- VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
124
- VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
125
- VideoTrimmerUtil.MAX_COUNT_RANGE = Math.max(((int) VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE);
157
+ public void initByURI(final Uri videoURI) {
158
+ mSourceUri = videoURI;
126
159
 
127
- if (mDuration <= mMaxDuration) {
128
- mThumbsTotalCount = VideoTrimmerUtil.MAX_COUNT_RANGE;
129
- mRightProgressPos = mDuration;
130
- } else {
131
- mThumbsTotalCount = (int) (mDuration * 1.0f / (mMaxDuration * 1.0f) * VideoTrimmerUtil.MAX_COUNT_RANGE);
132
- mRightProgressPos = mMaxDuration;
133
- }
160
+ if (isVideoType) {
161
+ mVideoView.setVideoURI(videoURI);
162
+ mVideoView.requestFocus();
163
+
164
+ mVideoView.setOnPreparedListener(mp -> {
165
+ if (!mIsPrepared) {
166
+ mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
167
+ videoPrepared();
168
+ mIsPrepared = true;
169
+ }
170
+ });
171
+
172
+ mVideoView.setOnErrorListener((mp, what, extra) -> {
173
+ mediaFailed();
174
+ mOnTrimVideoListener.onError("Error loading video file. Please try again.", ErrorCode.FAIL_TO_LOAD_VIDEO);
175
+ return true;
176
+ });
134
177
 
135
- mVideoThumbRecyclerView.addItemDecoration(new SpacesItemDecoration2(RECYCLER_VIEW_PADDING, mThumbsTotalCount));
136
- mRangeSeekBarView = new RangeSeekBarView(mContext, mLeftProgressPos, mRightProgressPos);
137
- mRangeSeekBarView.setSelectedMinValue(mLeftProgressPos);
138
- mRangeSeekBarView.setSelectedMaxValue(mRightProgressPos);
139
- mRangeSeekBarView.setStartEndTime(mLeftProgressPos, mRightProgressPos);
140
- mRangeSeekBarView.setMinShootTime(mMinDuration);
141
- mRangeSeekBarView.setNotifyWhileDragging(true);
142
- mRangeSeekBarView.setOnRangeSeekBarChangeListener(mOnRangeSeekBarChangeListener);
143
- mSeekBarLayout.addView(mRangeSeekBarView);
144
- if(mThumbsTotalCount - VideoTrimmerUtil.MAX_COUNT_RANGE > 0) {
145
- mAverageMsPx = (mDuration - mMaxDuration) / (float) (mThumbsTotalCount - VideoTrimmerUtil.MAX_COUNT_RANGE);
178
+ mVideoView.setOnCompletionListener(mp -> mediaCompleted());
146
179
  } else {
147
- mAverageMsPx = 0f;
180
+ mVideoView.setVisibility(View.GONE);
181
+ audioBannerView.setAlpha(0f);
182
+ audioBannerView.setVisibility(View.VISIBLE);
183
+ audioBannerView.animate().alpha(1f).setDuration(500).start();
184
+
185
+ audioPlayer = new MediaPlayer();
186
+ try {
187
+ audioPlayer.setDataSource(videoURI.toString());
188
+ audioPlayer.setOnPreparedListener(mp -> {
189
+ if (!mIsPrepared) {
190
+ audioPrepared();
191
+ mIsPrepared = true;
192
+ }
193
+ });
194
+ audioPlayer.setOnCompletionListener(mp -> mediaCompleted());
195
+ audioPlayer.setOnErrorListener((mp, what, extra) -> {
196
+ mediaFailed();
197
+ mOnTrimVideoListener.onError("Error loading audio file. Please try again.", ErrorCode.FAIL_TO_LOAD_AUDIO);
198
+ return true;
199
+ });
200
+
201
+ audioPlayer.prepareAsync(); // use prepareAsync to avoid blocking the main thread
202
+ } catch (IOException e) {
203
+ e.printStackTrace();
204
+ mediaFailed();
205
+ mOnTrimVideoListener.onError("Error initializing audio player. Please try again.", ErrorCode.FAIL_TO_INITIALIZE_AUDIO_PLAYER);
206
+ }
148
207
  }
149
- averagePxMs = (mMaxWidth * 1.0f / (mRightProgressPos - mLeftProgressPos));
150
- }
151
-
152
- public void initVideoByURI(final Uri videoURI) {
153
- mSourceUri = videoURI;
154
- mVideoView.setVideoURI(videoURI);
155
- mVideoView.requestFocus();
156
208
  }
157
209
 
158
- private void startShootVideoThumbs(final Context context, final Uri videoUri, int totalThumbsCount, long startPosition, long endPosition) {
159
- VideoTrimmerUtil.shootVideoThumbInBackground(context, videoUri, totalThumbsCount, startPosition, endPosition,
210
+ private void startShootVideoThumbs(final Context context, int totalThumbsCount, long startPosition, long endPosition) {
211
+ mThumbnailContainer.removeAllViews();
212
+ VideoTrimmerUtil.shootVideoThumbInBackground(mediaMetadataRetriever, totalThumbsCount, startPosition, endPosition,
160
213
  (bitmap, interval) -> {
161
214
  if (bitmap != null) {
162
- UiThreadExecutor.runTask("", () -> mVideoThumbAdapter.addBitmaps(bitmap), 0L);
215
+ runOnUiThread(() -> {
216
+ ImageView thumbImageView = new ImageView(context);
217
+ thumbImageView.setImageBitmap(bitmap);
218
+ thumbImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
219
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(100, LayoutParams.MATCH_PARENT);
220
+ layoutParams.width = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
221
+ thumbImageView.setLayoutParams(layoutParams);
222
+ mThumbnailContainer.addView(thumbImageView);
223
+ });
163
224
  }
164
225
  });
165
226
  }
166
227
 
167
- private void videoPrepared(MediaPlayer mp) {
168
- ViewGroup.LayoutParams lp = mVideoView.getLayoutParams();
169
- int videoWidth = mp.getVideoWidth();
170
- int videoHeight = mp.getVideoHeight();
228
+ private void videoPrepared() {
229
+ mDuration = mVideoView.getDuration();
230
+ mMaxDuration = Math.min(mMaxDuration, mDuration);
231
+ mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString());
171
232
 
172
- int screenWidth = mLinearVideo.getWidth();
173
- int screenHeight = mLinearVideo.getHeight();
233
+ if (mediaMetadataRetriever == null) {
234
+ mOnTrimVideoListener.onError("Error when retrieving video info. Please try again.", ErrorCode.FAIL_TO_GET_VIDEO_INFO);
235
+ return;
236
+ }
237
+
238
+ // take first frame
239
+ Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
240
+
241
+ if (bitmap != null) {
242
+ VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
243
+ }
174
244
 
175
- if (videoHeight > videoWidth) {
176
- lp.height = screenHeight;
177
- float r = videoWidth / (float) videoHeight;
178
- lp.width = (int) (lp.height * r);
245
+ VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
246
+ VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
247
+ VideoTrimmerUtil.MAX_COUNT_RANGE = Math.max((VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE);
248
+
249
+ startShootVideoThumbs(mContext, VideoTrimmerUtil.MAX_COUNT_RANGE, 0, mDuration);
250
+
251
+ // Set initial handle positions if mMaxDuration < video duration
252
+ if (mMaxDuration < mDuration) {
253
+ endTime = mMaxDuration;
179
254
  } else {
180
- lp.width = screenWidth;
181
- float r = videoHeight / (float) videoWidth;
182
- lp.height = (int) (lp.width * r);
255
+ endTime = mDuration;
183
256
  }
184
- mVideoView.setLayoutParams(lp);
185
- mDuration = mVideoView.getDuration();
257
+ updateHandlePositions();
186
258
 
187
- mMaxDuration = Math.min(mMaxDuration, mDuration);
259
+ loadingIndicator.setVisibility(View.GONE);
260
+ mPlayView.setVisibility(View.VISIBLE);
261
+ saveBtn.setVisibility(View.VISIBLE);
262
+ }
188
263
 
189
- MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever();
190
- mediaMetadataRetriever.setDataSource(mContext, mSourceUri);
191
- // take first frame
192
- Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
193
- VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
264
+ private void audioPrepared() {
265
+ mDuration = audioPlayer.getDuration();
266
+ mMaxDuration = Math.min(mMaxDuration, mDuration);
194
267
 
195
- if (!getRestoreState()) {
196
- seekTo((int) mRedProgressBarPos);
268
+ // Set initial handle positions if mMaxDuration < video duration
269
+ if (mMaxDuration < mDuration) {
270
+ endTime = mMaxDuration;
197
271
  } else {
198
- setRestoreState(false);
199
- seekTo((int) mRedProgressBarPos);
272
+ endTime = mDuration;
200
273
  }
201
- initRangeSeekBarView();
202
- startShootVideoThumbs(mContext, mSourceUri, mThumbsTotalCount, 0, mDuration);
274
+
275
+ updateHandlePositions();
276
+ loadingIndicator.setVisibility(View.GONE);
277
+ mPlayView.setVisibility(View.VISIBLE);
278
+ saveBtn.setVisibility(View.VISIBLE);
279
+ // mThumbnailContainer.animate().alpha(1f).setDuration(250).start();
203
280
  }
204
281
 
205
- private void videoCompleted() {
206
- seekTo(mLeftProgressPos);
207
- setPlayPauseViewIcon(false);
282
+ private void updateGradientColors(int startColor, int endColor) {
283
+ GradientDrawable gradientDrawable = new GradientDrawable();
284
+ gradientDrawable.setShape(GradientDrawable.RECTANGLE);
285
+ gradientDrawable.setCornerRadius(6f); // Adjust corner radius as needed
286
+ gradientDrawable.setColors(new int[]{startColor, endColor});
287
+ gradientDrawable.setOrientation(GradientDrawable.Orientation.LEFT_RIGHT);
288
+
289
+ mThumbnailContainer.setBackground(gradientDrawable);
290
+ }
291
+
292
+ private void mediaFailed() {
293
+ loadingIndicator.setVisibility(View.GONE);
294
+ failToLoadBtn.setVisibility(View.VISIBLE);
295
+ }
296
+
297
+ private void updateHandlePositions() {
298
+ float startPercent = (float) startTime / mDuration;
299
+ float endPercent = (float) endTime / mDuration;
300
+
301
+ float containerWidth = trimmerContainerBg.getWidth();
302
+ float leadingHandleX = startPercent * containerWidth;
303
+ float trailingHandleX = endPercent * containerWidth;
304
+
305
+ leadingHandle.setX(leadingHandleX);
306
+ trailingHandle.setX(trailingHandleX + trailingHandle.getWidth());
307
+
308
+ updateTrimmerContainerWidth();
309
+ updateCurrentTime(false);
310
+
311
+ trimmerContainerWrapper.setVisibility(View.VISIBLE);
312
+ trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start();
208
313
  }
209
314
 
210
- private void onVideoReset() {
211
- mVideoView.pause();
315
+ private void mediaCompleted() {
212
316
  setPlayPauseViewIcon(false);
317
+ mTimingHandler.removeCallbacks(mTimingRunnable);
213
318
  }
214
319
 
215
- private void playVideoOrPause() {
216
- mRedProgressBarPos = mVideoView.getCurrentPosition();
217
- if (mVideoView.isPlaying()) {
218
- mVideoView.pause();
219
- pauseRedProgressAnimation();
320
+ private void playOrPause() {
321
+ if (isVideoType) {
322
+ if (mVideoView.isPlaying()) {
323
+ onMediaPause();
324
+ } else {
325
+ // if current video time >= end time, seek to start time
326
+ if (mVideoView.getCurrentPosition() >= endTime) {
327
+ seekTo(startTime, true);
328
+ }
329
+ mVideoView.start();
330
+ startTimingRunnable();
331
+ }
332
+ setPlayPauseViewIcon(mVideoView.isPlaying());
333
+
220
334
  } else {
221
- mVideoView.start();
222
- playingRedProgressAnimation();
335
+ if (audioPlayer.isPlaying()) {
336
+ onMediaPause();
337
+ } else {
338
+ if (audioPlayer.getCurrentPosition() >= endTime) {
339
+ seekTo(startTime, true);
340
+ }
341
+ audioPlayer.start();
342
+ startTimingRunnable();
343
+ }
344
+ setPlayPauseViewIcon(audioPlayer.isPlaying());
223
345
  }
224
- setPlayPauseViewIcon(mVideoView.isPlaying());
225
346
  }
226
347
 
227
- public void onVideoPause() {
228
- if (mVideoView.isPlaying()) {
229
- seekTo(mLeftProgressPos);//复位
230
- mVideoView.pause();
231
- setPlayPauseViewIcon(false);
232
- mRedProgressIcon.setVisibility(GONE);
348
+ public void onMediaPause() {
349
+ if (isVideoType) {
350
+ if (mVideoView.isPlaying()) {
351
+ mTimingHandler.removeCallbacks(mTimingRunnable);
352
+ mVideoView.pause();
353
+ setPlayPauseViewIcon(false);
354
+ }
355
+ } else {
356
+ if (audioPlayer.isPlaying()) {
357
+ mTimingHandler.removeCallbacks(mTimingRunnable);
358
+ audioPlayer.pause();
359
+ setPlayPauseViewIcon(false);
360
+ }
233
361
  }
234
362
  }
235
363
 
@@ -238,237 +366,389 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
238
366
  }
239
367
 
240
368
  private void setUpListeners() {
241
- findViewById(R.id.cancelBtn).setOnClickListener(view -> {
242
- mOnTrimVideoListener.onCancel();
243
- });
244
-
245
- findViewById(R.id.saveBtn).setOnClickListener(view -> {
246
- mOnTrimVideoListener.onSave();
247
- });
248
-
249
- mVideoView.setOnPreparedListener(mp -> {
250
- // this is called everytime activity goes active, and can fire multiple times
251
- // so that we create a flag to not run below code more than once
252
- if (mIsPrepared) {
253
- return;
254
- }
255
- mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
256
- videoPrepared(mp);
257
- mIsPrepared = true;
258
- });
259
- mVideoView.setOnCompletionListener(mp -> {
260
- videoCompleted();
261
- });
262
- mPlayView.setOnClickListener(view -> {
263
- playVideoOrPause();
264
- });
369
+ cancelBtn.setOnClickListener(view -> mOnTrimVideoListener.onCancel());
370
+ saveBtn.setOnClickListener(view -> mOnTrimVideoListener.onSave());
371
+ mPlayView.setOnClickListener(view -> playOrPause());
372
+ setHandleTouchListener(leadingHandle, true);
373
+ setHandleTouchListener(trailingHandle, false);
265
374
  }
266
375
 
267
376
  public void onSaveClicked() {
268
- if (mRightProgressPos - mLeftProgressPos < mMinDuration) {
269
- Toast.makeText(mContext, "Video is too short, can't proceed", Toast.LENGTH_SHORT).show();
377
+ onMediaPause();
378
+ VideoTrimmerUtil.trim(
379
+ isVideoType ? mSourceUri.getPath() : mSourceUri.toString(),
380
+ StorageUtil.getOutputPath(mContext, mOutputExt),
381
+ mDuration,
382
+ startTime,
383
+ endTime,
384
+ mOnTrimVideoListener);
385
+ }
386
+
387
+ private void seekTo(long msec, boolean needUpdateProgress) {
388
+ if (isVideoType) {
389
+ mVideoView.seekTo((int) msec);
270
390
  } else {
271
- mVideoView.pause();
272
- VideoTrimmerUtil.trim(
273
- mSourceUri.getPath(),
274
- StorageUtil.getOutputPath(mContext),
275
- mDuration,
276
- mLeftProgressPos,
277
- mRightProgressPos,
278
- mOnTrimVideoListener);
391
+ audioPlayer.seekTo((int) msec);
279
392
  }
280
- }
281
393
 
282
- private void seekTo(long msec) {
283
- mVideoView.seekTo((int) msec);
394
+ updateCurrentTime(needUpdateProgress);
284
395
  }
285
396
 
286
- private boolean getRestoreState() {
287
- return isFromRestore;
397
+ private void setPlayPauseViewIcon(boolean isPlaying) {
398
+ // note: icons imported from SF symbols have 0.85 opacity we should change to 1 here
399
+ mPlayView.setImageResource(isPlaying ? R.drawable.pause_fill : R.drawable.play_fill);
288
400
  }
289
401
 
290
- public void setRestoreState(boolean fromRestore) {
291
- isFromRestore = fromRestore;
402
+ @Override
403
+ protected void onDetachedFromWindow() {
404
+ super.onDetachedFromWindow();
405
+ mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
292
406
  }
293
407
 
294
- private void setPlayPauseViewIcon(boolean isPlaying) {
295
- mPlayView.setImageResource(isPlaying ? R.drawable.ic_video_pause_black : R.drawable.ic_video_play_black);
408
+ @Override
409
+ public void onDestroy() {
410
+ BackgroundExecutor.cancelAll("", true);
411
+ UiThreadExecutor.cancelAll("");
412
+ mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
413
+ mTimingHandler.removeCallbacks(mTimingRunnable);
414
+ zoomWaitTimer.removeCallbacks(zoomRunnable);
415
+
416
+ try {
417
+ if (mediaMetadataRetriever != null) {
418
+ mediaMetadataRetriever.release();
419
+ }
420
+ } catch (Exception e) {
421
+ e.printStackTrace();
422
+ }
423
+
424
+ if (audioPlayer != null) {
425
+ audioPlayer.stop();
426
+ audioPlayer.release();
427
+ }
296
428
  }
297
429
 
298
- private final RangeSeekBarView.OnRangeSeekBarChangeListener mOnRangeSeekBarChangeListener = new RangeSeekBarView.OnRangeSeekBarChangeListener() {
299
- @Override public void onRangeSeekBarValuesChanged(RangeSeekBarView bar, long minValue, long maxValue, int action, boolean isMin,
300
- RangeSeekBarView.Thumb pressedThumb) {
301
- mLeftProgressPos = minValue + scrollPos;
302
- mRedProgressBarPos = mLeftProgressPos;
430
+ private int getScreenWidthInPortraitMode() {
431
+ int screenWidth = DeviceUtil.getDeviceWidth();
432
+ int screenHeight = DeviceUtil.getDeviceHeight();
433
+ int currentOrientation = getResources().getConfiguration().orientation;
434
+ if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
435
+ return screenHeight;
436
+ }
437
+ return screenWidth;
438
+ }
303
439
 
304
- // when dragging the highlighted section mRightProgressPos in some cases can bigger than mDuration
305
- // Eg: mDuration=62006, then mRightProgressPos can be 63000
306
- mRightProgressPos = Math.min(maxValue + scrollPos, mDuration);
307
- switch (action) {
308
- case MotionEvent.ACTION_DOWN:
309
- break;
310
- case MotionEvent.ACTION_MOVE:
311
- seekTo((int) (pressedThumb == RangeSeekBarView.Thumb.MIN ? mLeftProgressPos : mRightProgressPos));
312
- break;
313
- case MotionEvent.ACTION_UP:
314
- seekTo((int) mLeftProgressPos);
315
- break;
316
- default:
317
- break;
318
- }
440
+ private void configure(ReadableMap config) {
441
+ if (config.hasKey("maxDuration")) {
442
+ mMaxDuration = Math.max(0, config.getInt("maxDuration") * 1000L);
443
+ }
444
+ if (config.hasKey("minDuration")) {
445
+ mMinDuration = Math.max(1000L, config.getInt("minDuration") * 1000L);
446
+ }
447
+ if (config.hasKey("cancelButtonText")) {
448
+ cancelBtn.setText(config.getString("cancelButtonText"));
449
+ }
450
+ if (config.hasKey("saveButtonText")) {
451
+ saveBtn.setText(config.getString("saveButtonText"));
452
+ }
319
453
 
454
+ if (config.hasKey("type")) {
455
+ isVideoType = !config.getString("type").equals("audio");
320
456
 
321
- mRangeSeekBarView.setStartEndTime(mLeftProgressPos, mRightProgressPos);
457
+ // if (!isVideoType) {
458
+ // mThumbnailContainer.setAlpha(0f);
459
+ // mThumbnailContainer.setBackground(ContextCompat.getDrawable(mContext, R.drawable.thumb_container_bg));
460
+ // }
322
461
  }
323
- };
324
462
 
325
- private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() {
326
- @Override
327
- public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
328
- super.onScrollStateChanged(recyclerView, newState);
463
+ if (config.hasKey("outputExt")) {
464
+ mOutputExt = config.getString("outputExt");
329
465
  }
330
466
 
331
- @Override
332
- public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
333
- super.onScrolled(recyclerView, dx, dy);
334
- int scrollX = calcScrollXDistance();
467
+ if (config.hasKey("enableHapticFeedback")) {
468
+ enableHapticFeedback = config.getBoolean("enableHapticFeedback");
469
+ }
470
+ }
335
471
 
336
- if (Math.abs(lastScrollX - scrollX) < mScaledTouchSlop) {
337
- return;
338
- }
339
- //初始状态,why ? 因为默认的时候有35dp的空白!
340
- if (scrollX == -RECYCLER_VIEW_PADDING) {
341
- scrollPos = 0;
342
- mLeftProgressPos = mRangeSeekBarView.getSelectedMinValue() + scrollPos;
343
-
344
- // when scrolling the highlighted section mRightProgressPos in some cases can bigger than mDuration
345
- // Eg: mDuration=62006, then mRightProgressPos can be 63000
346
- mRightProgressPos = Math.min(mRangeSeekBarView.getSelectedMaxValue() + scrollPos, mDuration);
347
- mRedProgressBarPos = mLeftProgressPos;
348
- } else {
349
- scrollPos = (long) (mAverageMsPx * (RECYCLER_VIEW_PADDING + scrollX) / VideoTrimmerUtil.mThumbWidth);
350
- mLeftProgressPos = mRangeSeekBarView.getSelectedMinValue() + scrollPos;
351
-
352
- // when scrolling the highlighted section mRightProgressPos in some cases can bigger than mDuration
353
- // Eg: mDuration=62006, then mRightProgressPos can be 63000
354
- mRightProgressPos = Math.min(mRangeSeekBarView.getSelectedMaxValue() + scrollPos, mDuration);
355
- mRedProgressBarPos = mLeftProgressPos;
356
- if (mVideoView.isPlaying()) {
357
- mVideoView.pause();
358
- setPlayPauseViewIcon(false);
472
+ private void startTimingRunnable() {
473
+ mTimingRunnable = new Runnable() {
474
+ @Override
475
+ public void run() {
476
+ int currentPosition;
477
+ if (isVideoType) {
478
+ currentPosition = mVideoView.getCurrentPosition();
479
+ } else {
480
+ currentPosition = audioPlayer.getCurrentPosition();
359
481
  }
360
- mRedProgressIcon.setVisibility(GONE);
361
- seekTo(mLeftProgressPos);
362
482
 
363
- mRangeSeekBarView.setStartEndTime(mLeftProgressPos, mRightProgressPos);
364
- mRangeSeekBarView.invalidate();
483
+ if (currentPosition >= endTime) {
484
+ onMediaPause();
485
+ seekTo(endTime, true); // Ensure exact end time display
486
+ } else {
487
+ updateCurrentTime(true);
488
+ mTimingHandler.postDelayed(this, TIMING_UPDATE_INTERVAL);
489
+ }
365
490
  }
491
+ };
492
+ mTimingHandler.postDelayed(mTimingRunnable, TIMING_UPDATE_INTERVAL);
493
+ }
494
+
495
+ private void updateCurrentTime(boolean needUpdateProgress) {
496
+ // TODO: check the case after drag the progress indicator and hit play, it'll play a little bit earlier than the progress indicator
497
+
498
+ int currentPosition;
499
+ if (isVideoType) {
500
+ currentPosition = mVideoView.getCurrentPosition();
501
+ } else {
502
+ currentPosition = audioPlayer.getCurrentPosition();
503
+ }
366
504
 
367
- lastScrollX = scrollX;
505
+ int duration = mDuration;
506
+
507
+ if (currentPosition >= duration - 100) {
508
+ currentPosition = duration;
509
+ } else if (currentPosition >= endTime - 100) {
510
+ currentPosition = (int) endTime;
368
511
  }
369
- };
370
512
 
371
- /**
372
- * 水平滑动了多少px
373
- */
374
- private int calcScrollXDistance() {
375
- LinearLayoutManager layoutManager = (LinearLayoutManager) mVideoThumbRecyclerView.getLayoutManager();
376
- int position = layoutManager.findFirstVisibleItemPosition();
377
- View firstVisibleChildView = layoutManager.findViewByPosition(position);
378
- int itemWidth = firstVisibleChildView.getWidth();
379
- return (position) * itemWidth - firstVisibleChildView.getLeft();
513
+ String currentTime = formatTime(currentPosition);
514
+ currentTimeText.setText(currentTime);
515
+
516
+ String startTime = formatTime((int) this.startTime);
517
+ startTimeText.setText(startTime);
518
+
519
+ String endTime = formatTime((int) this.endTime);
520
+ endTimeText.setText(endTime);
521
+
522
+ if (needUpdateProgress) {
523
+ // Update progressIndicator position
524
+ float indicatorPosition = (float) currentPosition / duration * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth();
525
+
526
+ float rightBoundary = trimmerContainer.getX() + trimmerContainer.getWidth() - progressIndicator.getWidth();
527
+
528
+ progressIndicator.setX(Math.min(rightBoundary, indicatorPosition));
529
+ }
380
530
  }
381
531
 
382
- private void playingRedProgressAnimation() {
383
- pauseRedProgressAnimation();
384
- playingAnimation();
385
- mAnimationHandler.post(mAnimationRunnable);
532
+ private String formatTime(int milliseconds) {
533
+ int totalSeconds = milliseconds / 1000;
534
+ int minutes = totalSeconds / 60;
535
+ int seconds = totalSeconds % 60;
536
+ int millis = milliseconds % 1000;
537
+ return String.format(Locale.getDefault(), "%d:%02d.%03d", minutes, seconds, millis);
386
538
  }
387
539
 
388
- private void playingAnimation() {
389
- if (mRedProgressIcon.getVisibility() == View.GONE) {
390
- mRedProgressIcon.setVisibility(View.VISIBLE);
391
- }
392
- final LayoutParams params = (LayoutParams) mRedProgressIcon.getLayoutParams();
393
- int start = (int) (RECYCLER_VIEW_PADDING + (mRedProgressBarPos - scrollPos) * averagePxMs);
394
- int end = (int) (RECYCLER_VIEW_PADDING + (mRightProgressPos - scrollPos) * averagePxMs);
395
- mRedProgressAnimator = ValueAnimator.ofInt(start, end).setDuration((mRightProgressPos - scrollPos) - (mRedProgressBarPos - scrollPos));
396
- mRedProgressAnimator.setInterpolator(new LinearInterpolator());
397
- mRedProgressAnimator.addUpdateListener(animation -> {
398
- params.leftMargin = (int) animation.getAnimatedValue();
399
- mRedProgressIcon.setLayoutParams(params);
540
+ private void setProgressIndicatorTouchListener() {
541
+ trimmerContainerBg.setOnTouchListener((view, event) -> {
542
+ switch (event.getAction()) {
543
+ case MotionEvent.ACTION_DOWN:
544
+ didClampWhilePanning = false;
545
+ onMediaPause();
546
+ onTrimmerContainerPanned(event);
547
+ playHapticFeedback(true);
548
+ break;
549
+ case MotionEvent.ACTION_MOVE:
550
+ onTrimmerContainerPanned(event);
551
+ break;
552
+ case MotionEvent.ACTION_UP:
553
+ view.performClick();
554
+ break;
555
+ default:
556
+ return false;
557
+ }
558
+ return true;
400
559
  });
401
- mRedProgressAnimator.start();
402
560
  }
403
561
 
404
- private void pauseRedProgressAnimation() {
405
- mRedProgressIcon.clearAnimation();
406
- if (mRedProgressAnimator != null && mRedProgressAnimator.isRunning()) {
407
- mAnimationHandler.removeCallbacks(mAnimationRunnable);
408
- mRedProgressAnimator.cancel();
562
+ private void onTrimmerContainerPanned(MotionEvent event) {
563
+ float newX = event.getRawX();
564
+ boolean didClamp = false;
565
+ // Ensure newX is within valid range
566
+ float leftBoundary = trimmerContainer.getX();
567
+ float rightBoundary = trimmerContainer.getX() + trimmerContainer.getWidth() - progressIndicator.getWidth();
568
+ newX = Math.max(leftBoundary, newX);
569
+ newX = Math.min(rightBoundary, newX);
570
+
571
+ // check play haptic feedback
572
+ if (newX <= leftBoundary) {
573
+ didClamp = true;
574
+ } else if (newX >= rightBoundary) {
575
+ didClamp = true;
409
576
  }
410
- }
411
577
 
412
- private final Runnable mAnimationRunnable = () -> updateVideoProgress();
413
-
414
- private void updateVideoProgress() {
415
- long currentPosition = mVideoView.getCurrentPosition();
416
- if (currentPosition >= (mRightProgressPos)) {
417
- mRedProgressBarPos = mLeftProgressPos;
418
- pauseRedProgressAnimation();
419
- onVideoPause();
420
- } else {
421
- mAnimationHandler.post(mAnimationRunnable);
578
+ if (didClamp && !didClampWhilePanning) {
579
+ playHapticFeedback(false);
422
580
  }
581
+ didClampWhilePanning = didClamp;
582
+
583
+ progressIndicator.setX(newX);
584
+
585
+ float indicatorPosition = newX - (trimmerContainerBg.getX());
586
+
587
+ // TODO: check this
588
+ float indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
589
+ long newVideoPosition = (long) (indicatorPositionPercent * mDuration);
590
+
591
+ seekTo(newVideoPosition, false);
423
592
  }
424
593
 
425
- @Override
426
- protected void onDetachedFromWindow() {
427
- super.onDetachedFromWindow();
428
- mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
594
+ private void setHandleTouchListener(View handle, boolean isLeading) {
595
+ handle.setOnTouchListener((view, event) -> {
596
+ switch (event.getAction()) {
597
+ case MotionEvent.ACTION_DOWN:
598
+ didClampWhilePanning = false;
599
+ onMediaPause();
600
+ fadeOutProgressIndicator();
601
+ seekTo(isLeading ? startTime : endTime, true);
602
+ playHapticFeedback(true);
603
+ break;
604
+ case MotionEvent.ACTION_MOVE:
605
+ boolean didClamp = false;
606
+ float newX = event.getRawX() - ((float) view.getWidth() / 2);
607
+ if (isLeading) {
608
+ newX = Math.max(0, Math.min(newX, trailingHandle.getX() - view.getWidth()));
609
+ } else {
610
+ newX = Math.min(trimmerContainerBg.getWidth() + view.getWidth(), Math.max(newX, leadingHandle.getX() + view.getWidth()));
611
+ }
612
+
613
+ view.setX(newX);
614
+
615
+ // Calculate new startTime or endTime
616
+ if (isLeading) {
617
+ // Calculate the new startTime based on the handle's new position
618
+ long newStartTime = (long) ((newX / trimmerContainerBg.getWidth()) * mDuration);
619
+ // Calculate the duration between the new startTime and the current endTime
620
+ long duration = endTime - newStartTime;
621
+ if (duration >= mMinDuration && duration <= mMaxDuration) {
622
+ // If the duration is within the allowed range, update startTime and move the progress indicator
623
+ startTime = newStartTime;
624
+ progressIndicator.setX(newX + view.getWidth());
625
+ } else if (duration < mMinDuration) {
626
+ didClamp = true;
627
+ // If the duration is less than the minimum, set startTime to the maximum possible to maintain the minimum duration
628
+ startTime = endTime - mMinDuration;
629
+ // Adjust the handle position accordingly
630
+ view.setX((float) startTime / mDuration * trimmerContainerBg.getWidth());
631
+ progressIndicator.setX(view.getX() + view.getWidth());
632
+ } else {
633
+ didClamp = true;
634
+ // If the duration is greater than the maximum, set startTime to the minimum possible to maintain the maximum duration
635
+ startTime = endTime - mMaxDuration;
636
+ // Adjust the handle position accordingly
637
+ view.setX((float) startTime / mDuration * trimmerContainerBg.getWidth());
638
+ progressIndicator.setX(view.getX() + view.getWidth());
639
+ }
640
+ } else {
641
+ // Calculate the new endTime based on the handle's new position
642
+ long newEndTime = (long) (((newX - view.getWidth()) / trimmerContainerBg.getWidth()) * mDuration);
643
+ // Calculate the duration between the new endTime and the current startTime
644
+ long duration = newEndTime - startTime;
645
+ if (duration >= mMinDuration && duration <= mMaxDuration) {
646
+ // If the duration is within the allowed range, update endTime and move the progress indicator
647
+ endTime = newEndTime;
648
+ progressIndicator.setX(newX - progressIndicator.getWidth());
649
+ } else if (duration < mMinDuration) {
650
+ didClamp = true;
651
+ // If the duration is less than the minimum, set endTime to the minimum possible to maintain the minimum duration
652
+ endTime = startTime + mMinDuration;
653
+ // Adjust the handle position accordingly
654
+ view.setX((float) endTime / mDuration * trimmerContainerBg.getWidth() + view.getWidth());
655
+ progressIndicator.setX(view.getX() - progressIndicator.getWidth());
656
+ } else {
657
+ didClamp = true;
658
+ // If the duration is greater than the maximum, set endTime to the maximum possible to maintain the maximum duration
659
+ endTime = startTime + mMaxDuration;
660
+ // Adjust the handle position accordingly
661
+ view.setX((float) endTime / mDuration * trimmerContainerBg.getWidth() + view.getWidth());
662
+ progressIndicator.setX(view.getX() - progressIndicator.getWidth());
663
+ }
664
+ }
665
+
666
+ if (didClamp && !didClampWhilePanning) {
667
+ playHapticFeedback(false);
668
+ }
669
+ didClampWhilePanning = didClamp;
670
+
671
+ updateTrimmerContainerWidth();
672
+ seekTo(isLeading ? startTime : endTime, false);
673
+
674
+ // TODO: create zoom feature like iOS
675
+ // startZoomWaitTimer();
676
+ break;
677
+ case MotionEvent.ACTION_UP:
678
+ // stopZoomIfNeeded();
679
+ fadeInProgressIndicator();
680
+ view.performClick();
681
+ break;
682
+ default:
683
+ return false;
684
+ }
685
+ return true;
686
+ });
429
687
  }
430
688
 
431
- /**
432
- * Cancel trim thread execute action when finish
433
- */
434
- @Override public void onDestroy() {
435
- BackgroundExecutor.cancelAll("", true);
436
- UiThreadExecutor.cancelAll("");
437
- mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
689
+ private void fadeOutProgressIndicator() {
690
+ progressIndicator.animate().alpha(0f).setDuration(250).withEndAction(() -> progressIndicator.setVisibility(View.INVISIBLE)).start();
438
691
  }
439
692
 
440
- private int getScreenWidthInPortraitMode() {
441
- int screenWidth = DeviceUtil.getDeviceWidth();
442
- int screenHeight = DeviceUtil.getDeviceHeight();
693
+ private void fadeInProgressIndicator() {
694
+ progressIndicator.setVisibility(View.VISIBLE);
695
+ progressIndicator.animate().alpha(1f).setDuration(250).start();
696
+ }
443
697
 
444
- // Check the current orientation
445
- int currentOrientation = getResources().getConfiguration().orientation;
698
+ private void updateTrimmerContainerWidth() {
699
+ int left = (int) leadingHandle.getX() + leadingHandle.getWidth();
700
+ int right = trimmerContainerBg.getWidth() - (int) trailingHandle.getX() + 2 * trailingHandle.getWidth();
701
+
702
+ RelativeLayout.LayoutParams leadingOverlayParams = (RelativeLayout.LayoutParams) leadingOverlay.getLayoutParams();
703
+ leadingOverlayParams.width = left;
704
+ leadingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
705
+ leadingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_START);
706
+ leadingOverlay.setLayoutParams(leadingOverlayParams);
707
+
708
+ RelativeLayout.LayoutParams trailingOverlayParams = (RelativeLayout.LayoutParams) trailingOverlay.getLayoutParams();
709
+ trailingOverlayParams.width = right;
710
+ trailingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
711
+ trailingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_END);
712
+ trailingOverlay.setLayoutParams(trailingOverlayParams);
713
+ }
446
714
 
447
- // Swap width and height if the current orientation is landscape
448
- if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
449
- return screenHeight;
715
+ private void playHapticFeedback(boolean isLight) {
716
+ if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && enableHapticFeedback) {
717
+ vibrator.vibrate(VibrationEffect.createOneShot(isLight ? 10 : 25, VibrationEffect.DEFAULT_AMPLITUDE)); // Light vibration
450
718
  }
451
-
452
- return screenWidth;
453
719
  }
454
720
 
455
- private void configure(ReadableMap config) {
456
- if (config.hasKey("maxDuration")) {
457
- this.mMaxDuration = Math.max(0, config.getInt("maxDuration") * 1000L);
721
+ private void startZoomWaitTimer() {
722
+ stopZoomWaitTimer();
723
+ if (isZoomedIn) {
724
+ return;
458
725
  }
459
726
 
460
- if (config.hasKey("minDuration")) {
461
- this.mMinDuration = Math.max(1000L, config.getInt("minDuration") * 1000L);
462
- }
727
+ zoomRunnable = () -> {
728
+ Log.i("tag", "A Kiss after 500ms");
729
+ stopZoomWaitTimer();
730
+ zoomIfNeeded();
731
+ };
463
732
 
464
- if (config.hasKey("cancelButtonText")) {
465
- TextView tv = findViewById(R.id.cancelBtn);
466
- tv.setText(config.getString("cancelButtonText"));
467
- }
733
+ zoomWaitTimer.postDelayed(zoomRunnable, 500);
734
+ }
468
735
 
469
- if (config.hasKey("saveButtonText")) {
470
- TextView tv = findViewById(R.id.saveBtn);
471
- tv.setText(config.getString("saveButtonText"));
736
+ private void stopZoomWaitTimer() {
737
+ zoomWaitTimer.removeCallbacks(zoomRunnable);
738
+ }
739
+
740
+ private void stopZoomIfNeeded() {
741
+ stopZoomWaitTimer();
742
+ isZoomedIn = false;
743
+ }
744
+
745
+ private void zoomIfNeeded() {
746
+ if (isZoomedIn) {
747
+ return;
472
748
  }
749
+
750
+ startShootVideoThumbs(mContext, 10, 5000, 10000);
751
+
752
+ isZoomedIn = true;
473
753
  }
474
754
  }