react-native-video-trim 1.0.23 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/README.md +22 -0
  2. package/android/src/main/AndroidManifest.xml +2 -0
  3. package/android/src/main/java/com/videotrim/VideoTrimModule.java +17 -2
  4. package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +5 -1
  5. package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +38 -3
  6. package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +406 -295
  7. package/android/src/main/res/drawable/chevron_compact_left.xml +15 -0
  8. package/android/src/main/res/drawable/chevron_compact_right.xml +15 -0
  9. package/android/src/main/res/drawable/chevron_right_with_bg.xml +13 -0
  10. package/android/src/main/res/drawable/pause_fill.xml +15 -0
  11. package/android/src/main/res/drawable/play_fill.xml +15 -0
  12. package/android/src/main/res/drawable/rounded_progress_indicator.xml +16 -0
  13. package/android/src/main/res/drawable/rounded_yellow_left_background.xml +8 -0
  14. package/android/src/main/res/drawable/rounded_yellow_right_background.xml +8 -0
  15. package/android/src/main/res/drawable/yellow_border.xml +9 -0
  16. package/android/src/main/res/layout/video_trimmer_view.xml +148 -76
  17. package/android/src/main/res/values/colors.xml +15 -13
  18. package/ios/VideoTrim.swift +41 -5
  19. package/ios/VideoTrimmerViewController.swift +1 -1
  20. package/package.json +1 -1
  21. package/android/src/main/java/com/videotrim/adapters/VideoTrimmerAdapter.java +0 -54
  22. package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +0 -534
  23. package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +0 -33
  24. package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +0 -48
  25. package/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
  26. package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
  27. package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
  28. package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
  29. package/android/src/main/res/layout/video_thumb_item_layout.xml +0 -16
@@ -1,9 +1,9 @@
1
1
  package com.videotrim.widgets;
2
2
 
3
+ import static com.facebook.react.bridge.UiThreadUtil.runOnUiThread;
3
4
  import static com.videotrim.utils.VideoTrimmerUtil.RECYCLER_VIEW_PADDING;
4
5
  import static com.videotrim.utils.VideoTrimmerUtil.VIDEO_FRAMES_WIDTH;
5
6
 
6
- import android.animation.ValueAnimator;
7
7
  import android.content.Context;
8
8
  import android.content.pm.ActivityInfo;
9
9
  import android.content.res.Configuration;
@@ -11,80 +11,77 @@ import android.graphics.Bitmap;
11
11
  import android.media.MediaMetadataRetriever;
12
12
  import android.media.MediaPlayer;
13
13
  import android.net.Uri;
14
+ import android.os.Build;
14
15
  import android.os.Handler;
16
+ import android.os.VibrationEffect;
17
+ import android.os.Vibrator;
15
18
  import android.util.AttributeSet;
19
+ import android.util.Log;
16
20
  import android.view.LayoutInflater;
17
21
  import android.view.MotionEvent;
18
22
  import android.view.View;
19
- import android.view.ViewGroup;
20
- import android.view.animation.LinearInterpolator;
21
23
  import android.widget.FrameLayout;
22
24
  import android.widget.ImageView;
23
25
  import android.widget.LinearLayout;
24
26
  import android.widget.RelativeLayout;
25
27
  import android.widget.TextView;
26
- import android.widget.Toast;
27
-
28
- import androidx.annotation.NonNull;
29
- import androidx.recyclerview.widget.LinearLayoutManager;
30
- import androidx.recyclerview.widget.RecyclerView;
28
+ import android.widget.VideoView;
31
29
 
32
30
  import com.facebook.react.bridge.ReactApplicationContext;
33
31
  import com.facebook.react.bridge.ReadableMap;
34
32
  import com.videotrim.R;
35
- import com.videotrim.adapters.VideoTrimmerAdapter;
36
33
  import com.videotrim.interfaces.IVideoTrimmerView;
37
34
  import com.videotrim.interfaces.VideoTrimListener;
38
35
  import com.videotrim.utils.StorageUtil;
39
36
  import com.videotrim.utils.VideoTrimmerUtil;
40
37
 
38
+ import java.util.Locale;
39
+
41
40
  import iknow.android.utils.DeviceUtil;
42
41
  import iknow.android.utils.thread.BackgroundExecutor;
43
42
  import iknow.android.utils.thread.UiThreadExecutor;
44
43
 
45
- /**
46
- * Author:J.Chou
47
- * Date: 2016.08.01 2:23 PM
48
- * Email: who_know_me@163.com
49
- * Describe:
50
- */
51
44
  public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
52
45
 
53
46
  private static final String TAG = VideoTrimmerView.class.getSimpleName();
54
47
 
55
- private final int mMaxWidth = VIDEO_FRAMES_WIDTH;
56
48
  private ReactApplicationContext mContext;
57
- private RelativeLayout mLinearVideo;
58
- private ZVideoView mVideoView;
49
+ private VideoView mVideoView;
59
50
  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毫秒
51
+ private LinearLayout mThumbnailContainer;
66
52
  private Uri mSourceUri;
67
53
  private VideoTrimListener mOnTrimVideoListener;
68
54
  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
55
  private Boolean mIsPrepared = false;
81
- private long mMaxDuration = VideoTrimmerUtil.MAX_SHOOT_DURATION;
56
+ private long mMaxDuration = (long) Double.POSITIVE_INFINITY;
82
57
  private long mMinDuration = VideoTrimmerUtil.MIN_SHOOT_DURATION;
83
58
 
59
+ private final Handler mTimingHandler = new Handler();
60
+ private Runnable mTimingRunnable;
61
+ private static final long TIMING_UPDATE_INTERVAL = 30; // Update every 30 milliseconds
62
+ private TextView currentTimeText;
63
+ private TextView startTimeText;
64
+ private TextView endTimeText;
65
+ private View progressIndicator;
66
+ private View trimmerContainer;
67
+ // background of the trimmer container, its width never changes
68
+ // 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
69
+ private View trimmerContainerBg;
70
+ private FrameLayout leadingHandle;
71
+ private View trailingHandle;
72
+ private View leadingOverlay;
73
+ private View trailingOverlay;
74
+ private RelativeLayout trimmerContainerWrapper;
75
+
76
+ private long startTime = 0, endTime = 0;
77
+ private Vibrator vibrator;
78
+ private boolean didClampWhilePanning = false;
79
+
80
+ // zoom
81
+ private boolean isZoomedIn = false;
82
+ private final Handler zoomWaitTimer = new Handler();
83
+ private Runnable zoomRunnable;
84
84
 
85
- public VideoTrimmerView(ReactApplicationContext context, AttributeSet attrs) {
86
- this(context, attrs, 0, null);
87
- }
88
85
  public VideoTrimmerView(ReactApplicationContext context, ReadableMap config, AttributeSet attrs) {
89
86
  this(context, attrs, 0, config);
90
87
  }
@@ -97,56 +94,33 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
97
94
  private void init(ReactApplicationContext context, ReadableMap config) {
98
95
  this.mContext = context;
99
96
 
100
- // listen to onConfigurationChanged doesn't work for this, it runs too soon
101
97
  context.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
102
98
  LayoutInflater.from(context).inflate(R.layout.video_trimmer_view, this, true);
99
+ vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE);
103
100
 
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
-
101
+ initializeViews();
115
102
  configure(config);
116
103
  setUpListeners();
104
+ setProgressIndicatorTouchListener();
117
105
  }
118
106
 
119
- private void initRangeSeekBarView() {
120
- if(mRangeSeekBarView != null) return;
121
- mLeftProgressPos = 0;
122
-
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);
126
-
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
- }
134
-
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);
146
- } else {
147
- mAverageMsPx = 0f;
148
- }
149
- averagePxMs = (mMaxWidth * 1.0f / (mRightProgressPos - mLeftProgressPos));
107
+ private void initializeViews() {
108
+ mThumbnailContainer = findViewById(R.id.thumbnailContainer);
109
+ mVideoView = findViewById(R.id.video_loader);
110
+ mPlayView = findViewById(R.id.icon_video_play);
111
+ startTimeText = findViewById(R.id.startTime);
112
+ currentTimeText = findViewById(R.id.currentTime);
113
+ endTimeText = findViewById(R.id.endTime);
114
+ progressIndicator = findViewById(R.id.progressIndicator);
115
+ trimmerContainer = findViewById(R.id.trimmerContainer);
116
+ trimmerContainerBg = findViewById(R.id.trimmerContainerBg);
117
+ leadingHandle = findViewById(R.id.leadingHandle);
118
+ trailingHandle = findViewById(R.id.trailingHandle);
119
+ leadingOverlay = findViewById(R.id.leadingOverlay);
120
+ trailingOverlay = findViewById(R.id.trailingOverlay);
121
+ trimmerContainerWrapper = findViewById(R.id.trimmerContainerWrapper);
122
+ trimmerContainerWrapper.setVisibility(View.INVISIBLE);
123
+ trimmerContainerWrapper.setAlpha(0f);
150
124
  }
151
125
 
152
126
  public void initVideoByURI(final Uri videoURI) {
@@ -156,32 +130,24 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
156
130
  }
157
131
 
158
132
  private void startShootVideoThumbs(final Context context, final Uri videoUri, int totalThumbsCount, long startPosition, long endPosition) {
133
+ mThumbnailContainer.removeAllViews();
159
134
  VideoTrimmerUtil.shootVideoThumbInBackground(context, videoUri, totalThumbsCount, startPosition, endPosition,
160
135
  (bitmap, interval) -> {
161
136
  if (bitmap != null) {
162
- UiThreadExecutor.runTask("", () -> mVideoThumbAdapter.addBitmaps(bitmap), 0L);
137
+ runOnUiThread(() -> {
138
+ ImageView thumbImageView = new ImageView(context);
139
+ thumbImageView.setImageBitmap(bitmap);
140
+ thumbImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
141
+ LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(100, LayoutParams.MATCH_PARENT);
142
+ layoutParams.width = VideoTrimmerUtil.VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.MAX_COUNT_RANGE;
143
+ thumbImageView.setLayoutParams(layoutParams);
144
+ mThumbnailContainer.addView(thumbImageView);
145
+ });
163
146
  }
164
147
  });
165
148
  }
166
149
 
167
150
  private void videoPrepared(MediaPlayer mp) {
168
- ViewGroup.LayoutParams lp = mVideoView.getLayoutParams();
169
- int videoWidth = mp.getVideoWidth();
170
- int videoHeight = mp.getVideoHeight();
171
-
172
- int screenWidth = mLinearVideo.getWidth();
173
- int screenHeight = mLinearVideo.getHeight();
174
-
175
- if (videoHeight > videoWidth) {
176
- lp.height = screenHeight;
177
- float r = videoWidth / (float) videoHeight;
178
- lp.width = (int) (lp.height * r);
179
- } else {
180
- lp.width = screenWidth;
181
- float r = videoHeight / (float) videoWidth;
182
- lp.height = (int) (lp.width * r);
183
- }
184
- mVideoView.setLayoutParams(lp);
185
151
  mDuration = mVideoView.getDuration();
186
152
 
187
153
  mMaxDuration = Math.min(mMaxDuration, mDuration);
@@ -190,46 +156,74 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
190
156
  mediaMetadataRetriever.setDataSource(mContext, mSourceUri);
191
157
  // take first frame
192
158
  Bitmap bitmap = mediaMetadataRetriever.getFrameAtTime(0, MediaMetadataRetriever.OPTION_CLOSEST_SYNC);
193
- VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
194
159
 
195
- if (!getRestoreState()) {
196
- seekTo((int) mRedProgressBarPos);
197
- } else {
198
- setRestoreState(false);
199
- seekTo((int) mRedProgressBarPos);
160
+ if (bitmap != null) {
161
+ VideoTrimmerUtil.mThumbWidth = VideoTrimmerUtil.THUMB_HEIGHT * bitmap.getWidth() / bitmap.getHeight();
200
162
  }
201
- initRangeSeekBarView();
163
+
164
+ VideoTrimmerUtil.SCREEN_WIDTH_FULL = this.getScreenWidthInPortraitMode();
165
+ VideoTrimmerUtil.VIDEO_FRAMES_WIDTH = VideoTrimmerUtil.SCREEN_WIDTH_FULL - RECYCLER_VIEW_PADDING * 2;
166
+ VideoTrimmerUtil.MAX_COUNT_RANGE = Math.max((VIDEO_FRAMES_WIDTH / VideoTrimmerUtil.mThumbWidth), VideoTrimmerUtil.MAX_COUNT_RANGE);
167
+
168
+ int mThumbsTotalCount;
169
+ mThumbsTotalCount = (int) (mDuration * 1.0f / (mMaxDuration * 1.0f) * VideoTrimmerUtil.MAX_COUNT_RANGE);
202
170
  startShootVideoThumbs(mContext, mSourceUri, mThumbsTotalCount, 0, mDuration);
171
+
172
+ // Set initial handle positions if mMaxDuration < video duration
173
+ if (mMaxDuration < mDuration) {
174
+ endTime = mMaxDuration;
175
+ } else {
176
+ endTime = mDuration;
177
+ }
178
+ updateHandlePositions();
203
179
  }
204
180
 
205
- private void videoCompleted() {
206
- seekTo(mLeftProgressPos);
207
- setPlayPauseViewIcon(false);
181
+ private void updateHandlePositions() {
182
+ float startPercent = (float) startTime / mVideoView.getDuration();
183
+ float endPercent = (float) endTime / mVideoView.getDuration();
184
+
185
+ float containerWidth = trimmerContainerBg.getWidth();
186
+ float leadingHandleX = startPercent * containerWidth;
187
+ float trailingHandleX = endPercent * containerWidth;
188
+
189
+ leadingHandle.setX(leadingHandleX);
190
+ trailingHandle.setX(trailingHandleX + trailingHandle.getWidth());
191
+
192
+ updateTrimmerContainerWidth();
193
+ updateCurrentTime(false);
194
+
195
+ trimmerContainerWrapper.setVisibility(View.VISIBLE);
196
+ trimmerContainerWrapper.animate().alpha(1f).setDuration(250).start();
197
+
198
+ // because on load video will not start and just display black screen
199
+ // here we'll seek to first frame to make it more friendly
200
+ mVideoView.seekTo(1);
208
201
  }
209
202
 
210
- private void onVideoReset() {
211
- mVideoView.pause();
203
+ private void videoCompleted() {
212
204
  setPlayPauseViewIcon(false);
205
+ mTimingHandler.removeCallbacks(mTimingRunnable);
213
206
  }
214
207
 
215
- private void playVideoOrPause() {
216
- mRedProgressBarPos = mVideoView.getCurrentPosition();
208
+ private void playOrPause() {
217
209
  if (mVideoView.isPlaying()) {
218
- mVideoView.pause();
219
- pauseRedProgressAnimation();
210
+ onVideoPause();
220
211
  } else {
212
+ // if current video time >= end time, seek to start time
213
+ if (mVideoView.getCurrentPosition() >= endTime) {
214
+ seekTo(startTime, true);
215
+ }
221
216
  mVideoView.start();
222
- playingRedProgressAnimation();
217
+ startTimingRunnable();
223
218
  }
224
219
  setPlayPauseViewIcon(mVideoView.isPlaying());
225
220
  }
226
221
 
227
222
  public void onVideoPause() {
228
223
  if (mVideoView.isPlaying()) {
229
- seekTo(mLeftProgressPos);//复位
224
+ mTimingHandler.removeCallbacks(mTimingRunnable);
230
225
  mVideoView.pause();
231
226
  setPlayPauseViewIcon(false);
232
- mRedProgressIcon.setVisibility(GONE);
233
227
  }
234
228
  }
235
229
 
@@ -238,237 +232,354 @@ public class VideoTrimmerView extends FrameLayout implements IVideoTrimmerView {
238
232
  }
239
233
 
240
234
  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
- });
235
+ findViewById(R.id.cancelBtn).setOnClickListener(view -> mOnTrimVideoListener.onCancel());
236
+ findViewById(R.id.saveBtn).setOnClickListener(view -> mOnTrimVideoListener.onSave());
248
237
 
249
238
  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;
239
+ if (!mIsPrepared) {
240
+ mp.setVideoScalingMode(MediaPlayer.VIDEO_SCALING_MODE_SCALE_TO_FIT);
241
+ videoPrepared(mp);
242
+ mIsPrepared = true;
254
243
  }
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
244
  });
245
+
246
+ mVideoView.setOnCompletionListener(mp -> videoCompleted());
247
+ mPlayView.setOnClickListener(view -> playOrPause());
248
+ setHandleTouchListener(leadingHandle, true);
249
+ setHandleTouchListener(trailingHandle, false);
265
250
  }
266
251
 
267
252
  public void onSaveClicked() {
268
- if (mRightProgressPos - mLeftProgressPos < mMinDuration) {
269
- Toast.makeText(mContext, "Video is too short, can't proceed", Toast.LENGTH_SHORT).show();
270
- } else {
271
- mVideoView.pause();
272
- VideoTrimmerUtil.trim(
273
- mSourceUri.getPath(),
274
- StorageUtil.getOutputPath(mContext),
275
- mDuration,
276
- mLeftProgressPos,
277
- mRightProgressPos,
278
- mOnTrimVideoListener);
279
- }
253
+ onVideoPause();
254
+ VideoTrimmerUtil.trim(
255
+ mSourceUri.getPath(),
256
+ StorageUtil.getOutputPath(mContext),
257
+ mDuration,
258
+ startTime,
259
+ endTime,
260
+ mOnTrimVideoListener);
280
261
  }
281
262
 
282
- private void seekTo(long msec) {
263
+ private void seekTo(long msec, boolean needUpdateProgress) {
283
264
  mVideoView.seekTo((int) msec);
265
+ updateCurrentTime(needUpdateProgress);
284
266
  }
285
267
 
286
- private boolean getRestoreState() {
287
- return isFromRestore;
268
+ private void setPlayPauseViewIcon(boolean isPlaying) {
269
+ // note: icons imported from SF symbols have 0.85 opacity we should change to 1 here
270
+ mPlayView.setImageResource(isPlaying ? R.drawable.pause_fill : R.drawable.play_fill);
288
271
  }
289
272
 
290
- public void setRestoreState(boolean fromRestore) {
291
- isFromRestore = fromRestore;
273
+ @Override
274
+ protected void onDetachedFromWindow() {
275
+ super.onDetachedFromWindow();
276
+ mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
292
277
  }
293
278
 
294
- private void setPlayPauseViewIcon(boolean isPlaying) {
295
- mPlayView.setImageResource(isPlaying ? R.drawable.ic_video_pause_black : R.drawable.ic_video_play_black);
279
+ @Override
280
+ public void onDestroy() {
281
+ BackgroundExecutor.cancelAll("", true);
282
+ UiThreadExecutor.cancelAll("");
283
+ mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
284
+ mTimingHandler.removeCallbacks(mTimingRunnable);
285
+ zoomWaitTimer.removeCallbacks(zoomRunnable);
286
+ }
287
+
288
+ private int getScreenWidthInPortraitMode() {
289
+ int screenWidth = DeviceUtil.getDeviceWidth();
290
+ int screenHeight = DeviceUtil.getDeviceHeight();
291
+ int currentOrientation = getResources().getConfiguration().orientation;
292
+ if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
293
+ return screenHeight;
294
+ }
295
+ return screenWidth;
296
+ }
297
+
298
+ private void configure(ReadableMap config) {
299
+ if (config.hasKey("maxDuration")) {
300
+ mMaxDuration = Math.max(0, config.getInt("maxDuration") * 1000L);
301
+ }
302
+ if (config.hasKey("minDuration")) {
303
+ mMinDuration = Math.max(1000L, config.getInt("minDuration") * 1000L);
304
+ }
305
+ if (config.hasKey("cancelButtonText")) {
306
+ TextView tv = findViewById(R.id.cancelBtn);
307
+ tv.setText(config.getString("cancelButtonText"));
308
+ }
309
+ if (config.hasKey("saveButtonText")) {
310
+ TextView tv = findViewById(R.id.saveBtn);
311
+ tv.setText(config.getString("saveButtonText"));
312
+ }
313
+ }
314
+
315
+ private void startTimingRunnable() {
316
+ mTimingRunnable = new Runnable() {
317
+ @Override
318
+ public void run() {
319
+ int currentPosition = mVideoView.getCurrentPosition();
320
+
321
+ if (currentPosition >= endTime) {
322
+ onVideoPause();
323
+ seekTo(endTime, true); // Ensure exact end time display
324
+ } else {
325
+ updateCurrentTime(true);
326
+ mTimingHandler.postDelayed(this, TIMING_UPDATE_INTERVAL);
327
+ }
328
+ }
329
+ };
330
+ mTimingHandler.postDelayed(mTimingRunnable, TIMING_UPDATE_INTERVAL);
331
+ }
332
+
333
+ private void updateCurrentTime(boolean needUpdateProgress) {
334
+ // TODO: check the case after drag the progress indicator and hit play, it'll play a little bit earlier than the progress indicator
335
+ int currentPosition = mVideoView.getCurrentPosition();
336
+ int duration = mVideoView.getDuration();
337
+
338
+ if (currentPosition >= duration - 100) {
339
+ currentPosition = duration;
340
+ } else if (currentPosition >= endTime - 100) {
341
+ currentPosition = (int) endTime;
342
+ }
343
+
344
+ String currentTime = formatTime(currentPosition);
345
+ currentTimeText.setText(currentTime);
346
+
347
+ String startTime = formatTime((int) this.startTime);
348
+ startTimeText.setText(startTime);
349
+
350
+ String endTime = formatTime((int) this.endTime);
351
+ endTimeText.setText(endTime);
352
+
353
+ if (needUpdateProgress) {
354
+ // Update progressIndicator position
355
+ float indicatorPosition = (float) currentPosition / duration * (trimmerContainerBg.getWidth() - progressIndicator.getWidth()) + leadingHandle.getWidth();
356
+
357
+ float rightBoundary = trimmerContainer.getX() + trimmerContainer.getWidth() - progressIndicator.getWidth();
358
+
359
+ progressIndicator.setX(Math.min(rightBoundary, indicatorPosition));
360
+ }
296
361
  }
297
362
 
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;
363
+ private String formatTime(int milliseconds) {
364
+ int totalSeconds = milliseconds / 1000;
365
+ int minutes = totalSeconds / 60;
366
+ int seconds = totalSeconds % 60;
367
+ int millis = milliseconds % 1000;
368
+ return String.format(Locale.getDefault(), "%d:%02d.%03d", minutes, seconds, millis);
369
+ }
303
370
 
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) {
371
+ private void setProgressIndicatorTouchListener() {
372
+ trimmerContainerBg.setOnTouchListener((view, event) -> {
373
+ switch (event.getAction()) {
308
374
  case MotionEvent.ACTION_DOWN:
375
+ didClampWhilePanning = false;
376
+ onVideoPause();
377
+ onTrimmerContainerPanned(event);
378
+ playHapticFeedback(true);
309
379
  break;
310
380
  case MotionEvent.ACTION_MOVE:
311
- seekTo((int) (pressedThumb == RangeSeekBarView.Thumb.MIN ? mLeftProgressPos : mRightProgressPos));
381
+ onTrimmerContainerPanned(event);
312
382
  break;
313
383
  case MotionEvent.ACTION_UP:
314
- seekTo((int) mLeftProgressPos);
384
+ view.performClick();
315
385
  break;
316
386
  default:
317
- break;
387
+ return false;
318
388
  }
389
+ return true;
390
+ });
391
+ }
319
392
 
320
-
321
- mRangeSeekBarView.setStartEndTime(mLeftProgressPos, mRightProgressPos);
393
+ private void onTrimmerContainerPanned(MotionEvent event) {
394
+ float newX = event.getRawX();
395
+ boolean didClamp = false;
396
+ // Ensure newX is within valid range
397
+ float leftBoundary = trimmerContainer.getX();
398
+ float rightBoundary = trimmerContainer.getX() + trimmerContainer.getWidth() - progressIndicator.getWidth();
399
+ newX = Math.max(leftBoundary, newX);
400
+ newX = Math.min(rightBoundary, newX);
401
+
402
+ // check play haptic feedback
403
+ if (newX <= leftBoundary) {
404
+ didClamp = true;
322
405
  }
323
- };
324
-
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);
406
+ else if (newX >= rightBoundary) {
407
+ didClamp = true;
329
408
  }
330
409
 
331
- @Override
332
- public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
333
- super.onScrolled(recyclerView, dx, dy);
334
- int scrollX = calcScrollXDistance();
335
-
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);
359
- }
360
- mRedProgressIcon.setVisibility(GONE);
361
- seekTo(mLeftProgressPos);
410
+ if (didClamp && !didClampWhilePanning) {
411
+ playHapticFeedback(false);
412
+ }
413
+ didClampWhilePanning = didClamp;
362
414
 
363
- mRangeSeekBarView.setStartEndTime(mLeftProgressPos, mRightProgressPos);
364
- mRangeSeekBarView.invalidate();
365
- }
415
+ progressIndicator.setX(newX);
366
416
 
367
- lastScrollX = scrollX;
368
- }
369
- };
417
+ float indicatorPosition = newX - (trimmerContainerBg.getX());
370
418
 
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();
380
- }
419
+ // TODO: check this
420
+ float indicatorPositionPercent = indicatorPosition / (trimmerContainerBg.getWidth() - progressIndicator.getWidth());
421
+ long newVideoPosition = (long) (indicatorPositionPercent * mVideoView.getDuration());
381
422
 
382
- private void playingRedProgressAnimation() {
383
- pauseRedProgressAnimation();
384
- playingAnimation();
385
- mAnimationHandler.post(mAnimationRunnable);
423
+ seekTo(newVideoPosition, false);
386
424
  }
387
-
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);
425
+ private void setHandleTouchListener(View handle, boolean isLeading) {
426
+ handle.setOnTouchListener((view, event) -> {
427
+ switch (event.getAction()) {
428
+ case MotionEvent.ACTION_DOWN:
429
+ didClampWhilePanning = false;
430
+ onVideoPause();
431
+ fadeOutProgressIndicator();
432
+ seekTo(isLeading ? startTime : endTime, true);
433
+ playHapticFeedback(true);
434
+ break;
435
+ case MotionEvent.ACTION_MOVE:
436
+ boolean didClamp = false;
437
+ float newX = event.getRawX() - ((float) view.getWidth() / 2);
438
+ if (isLeading) {
439
+ newX = Math.max(0, Math.min(newX, trailingHandle.getX() - view.getWidth()));
440
+ } else {
441
+ newX = Math.min(trimmerContainerBg.getWidth() + view.getWidth(), Math.max(newX, leadingHandle.getX() + view.getWidth()));
442
+ }
443
+
444
+ view.setX(newX);
445
+
446
+ // Calculate new startTime or endTime
447
+ if (isLeading) {
448
+ // Calculate the new startTime based on the handle's new position
449
+ long newStartTime = (long) ((newX / trimmerContainerBg.getWidth()) * mVideoView.getDuration());
450
+ // Calculate the duration between the new startTime and the current endTime
451
+ long duration = endTime - newStartTime;
452
+ if (duration >= mMinDuration && duration <= mMaxDuration) {
453
+ // If the duration is within the allowed range, update startTime and move the progress indicator
454
+ startTime = newStartTime;
455
+ progressIndicator.setX(newX + view.getWidth());
456
+ } else if (duration < mMinDuration) {
457
+ didClamp = true;
458
+ // If the duration is less than the minimum, set startTime to the maximum possible to maintain the minimum duration
459
+ startTime = endTime - mMinDuration;
460
+ // Adjust the handle position accordingly
461
+ view.setX((float) startTime / mVideoView.getDuration() * trimmerContainerBg.getWidth());
462
+ progressIndicator.setX(view.getX() + view.getWidth());
463
+ } else {
464
+ didClamp = true;
465
+ // If the duration is greater than the maximum, set startTime to the minimum possible to maintain the maximum duration
466
+ startTime = endTime - mMaxDuration;
467
+ // Adjust the handle position accordingly
468
+ view.setX((float) startTime / mVideoView.getDuration() * trimmerContainerBg.getWidth());
469
+ progressIndicator.setX(view.getX() + view.getWidth());
470
+ }
471
+ } else {
472
+ // Calculate the new endTime based on the handle's new position
473
+ long newEndTime = (long) (((newX - view.getWidth()) / trimmerContainerBg.getWidth()) * mVideoView.getDuration());
474
+ // Calculate the duration between the new endTime and the current startTime
475
+ long duration = newEndTime - startTime;
476
+ if (duration >= mMinDuration && duration <= mMaxDuration) {
477
+ // If the duration is within the allowed range, update endTime and move the progress indicator
478
+ endTime = newEndTime;
479
+ progressIndicator.setX(newX - progressIndicator.getWidth());
480
+ } else if (duration < mMinDuration) {
481
+ didClamp = true;
482
+ // If the duration is less than the minimum, set endTime to the minimum possible to maintain the minimum duration
483
+ endTime = startTime + mMinDuration;
484
+ // Adjust the handle position accordingly
485
+ view.setX((float) endTime / mVideoView.getDuration() * trimmerContainerBg.getWidth() + view.getWidth());
486
+ progressIndicator.setX(view.getX() - progressIndicator.getWidth());
487
+ } else {
488
+ didClamp = true;
489
+ // If the duration is greater than the maximum, set endTime to the maximum possible to maintain the maximum duration
490
+ endTime = startTime + mMaxDuration;
491
+ // Adjust the handle position accordingly
492
+ view.setX((float) endTime / mVideoView.getDuration() * trimmerContainerBg.getWidth() + view.getWidth());
493
+ progressIndicator.setX(view.getX() - progressIndicator.getWidth());
494
+ }
495
+ }
496
+
497
+ if (didClamp && !didClampWhilePanning) {
498
+ playHapticFeedback(false);
499
+ }
500
+ didClampWhilePanning = didClamp;
501
+
502
+ updateTrimmerContainerWidth();
503
+ seekTo(isLeading ? startTime : endTime, false);
504
+
505
+ // TODO: create zoom feature like iOS
506
+ // startZoomWaitTimer();
507
+ break;
508
+ case MotionEvent.ACTION_UP:
509
+ // stopZoomIfNeeded();
510
+ fadeInProgressIndicator();
511
+ view.performClick();
512
+ break;
513
+ default:
514
+ return false;
515
+ }
516
+ return true;
400
517
  });
401
- mRedProgressAnimator.start();
402
518
  }
403
519
 
404
- private void pauseRedProgressAnimation() {
405
- mRedProgressIcon.clearAnimation();
406
- if (mRedProgressAnimator != null && mRedProgressAnimator.isRunning()) {
407
- mAnimationHandler.removeCallbacks(mAnimationRunnable);
408
- mRedProgressAnimator.cancel();
409
- }
520
+ private void fadeOutProgressIndicator() {
521
+ progressIndicator.animate().alpha(0f).setDuration(250).withEndAction(() -> progressIndicator.setVisibility(View.INVISIBLE)).start();
410
522
  }
411
523
 
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);
422
- }
524
+ private void fadeInProgressIndicator() {
525
+ progressIndicator.setVisibility(View.VISIBLE);
526
+ progressIndicator.animate().alpha(1f).setDuration(250).start();
423
527
  }
424
528
 
425
- @Override
426
- protected void onDetachedFromWindow() {
427
- super.onDetachedFromWindow();
428
- mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
529
+ private void updateTrimmerContainerWidth() {
530
+ int left = (int) leadingHandle.getX() + leadingHandle.getWidth();
531
+ int right = trimmerContainerBg.getWidth() - (int) trailingHandle.getX() + 2 * trailingHandle.getWidth();
532
+
533
+ RelativeLayout.LayoutParams leadingOverlayParams = (RelativeLayout.LayoutParams) leadingOverlay.getLayoutParams();
534
+ leadingOverlayParams.width = left;
535
+ leadingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
536
+ leadingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_START);
537
+ leadingOverlay.setLayoutParams(leadingOverlayParams);
538
+
539
+ RelativeLayout.LayoutParams trailingOverlayParams = (RelativeLayout.LayoutParams) trailingOverlay.getLayoutParams();
540
+ trailingOverlayParams.width = right;
541
+ trailingOverlayParams.height = RelativeLayout.LayoutParams.MATCH_PARENT;
542
+ trailingOverlayParams.addRule(RelativeLayout.ALIGN_PARENT_END);
543
+ trailingOverlay.setLayoutParams(trailingOverlayParams);
429
544
  }
430
545
 
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);
546
+ private void playHapticFeedback(boolean isLight) {
547
+ if (vibrator != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
548
+ vibrator.vibrate(VibrationEffect.createOneShot(isLight ? 10 : 25, VibrationEffect.DEFAULT_AMPLITUDE)); // Light vibration
549
+ }
438
550
  }
439
551
 
440
- private int getScreenWidthInPortraitMode() {
441
- int screenWidth = DeviceUtil.getDeviceWidth();
442
- int screenHeight = DeviceUtil.getDeviceHeight();
552
+ private void startZoomWaitTimer() {
553
+ stopZoomWaitTimer();
554
+ if (isZoomedIn) {
555
+ return;
556
+ }
443
557
 
444
- // Check the current orientation
445
- int currentOrientation = getResources().getConfiguration().orientation;
558
+ zoomRunnable = () -> {
559
+ Log.i("tag","A Kiss after 500ms");
560
+ stopZoomWaitTimer();
561
+ zoomIfNeeded();
562
+ };
446
563
 
447
- // Swap width and height if the current orientation is landscape
448
- if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
449
- return screenHeight;
450
- }
564
+ zoomWaitTimer.postDelayed(zoomRunnable, 500);
565
+ }
451
566
 
452
- return screenWidth;
567
+ private void stopZoomWaitTimer() {
568
+ zoomWaitTimer.removeCallbacks(zoomRunnable);
453
569
  }
454
570
 
455
- private void configure(ReadableMap config) {
456
- if (config.hasKey("maxDuration")) {
457
- this.mMaxDuration = Math.max(0, config.getInt("maxDuration") * 1000L);
458
- }
571
+ private void stopZoomIfNeeded() {
572
+ stopZoomWaitTimer();
573
+ isZoomedIn = false;
574
+ }
459
575
 
460
- if (config.hasKey("minDuration")) {
461
- this.mMinDuration = Math.max(1000L, config.getInt("minDuration") * 1000L);
576
+ private void zoomIfNeeded() {
577
+ if (isZoomedIn) {
578
+ return;
462
579
  }
463
580
 
464
- if (config.hasKey("cancelButtonText")) {
465
- TextView tv = findViewById(R.id.cancelBtn);
466
- tv.setText(config.getString("cancelButtonText"));
467
- }
581
+ startShootVideoThumbs(mContext, mSourceUri, 10, 5000, 10000);
468
582
 
469
- if (config.hasKey("saveButtonText")) {
470
- TextView tv = findViewById(R.id.saveBtn);
471
- tv.setText(config.getString("saveButtonText"));
472
- }
583
+ isZoomedIn = true;
473
584
  }
474
585
  }