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.
- package/README.md +55 -9
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/com/videotrim/VideoTrimModule.java +191 -63
- package/android/src/main/java/com/videotrim/enums/ErrorCode.java +11 -0
- package/android/src/main/java/com/videotrim/interfaces/VideoTrimListener.java +6 -1
- package/android/src/main/java/com/videotrim/utils/MediaMetadataUtil.java +75 -0
- package/android/src/main/java/com/videotrim/utils/StorageUtil.java +2 -2
- package/android/src/main/java/com/videotrim/utils/VideoTrimmerUtil.java +36 -8
- package/android/src/main/java/com/videotrim/widgets/VideoTrimmerView.java +588 -308
- package/android/src/main/res/drawable/airpodsmax.xml +19 -0
- package/android/src/main/res/drawable/chevron_compact_left.xml +15 -0
- package/android/src/main/res/drawable/chevron_compact_right.xml +15 -0
- package/android/src/main/res/drawable/chevron_right_with_bg.xml +13 -0
- package/android/src/main/res/drawable/exclamationmark_triangle_fill.xml +15 -0
- package/android/src/main/res/drawable/pause_fill.xml +15 -0
- package/android/src/main/res/drawable/play_fill.xml +15 -0
- package/android/src/main/res/drawable/rounded_progress_indicator.xml +16 -0
- package/android/src/main/res/drawable/rounded_yellow_left_background.xml +8 -0
- package/android/src/main/res/drawable/rounded_yellow_right_background.xml +8 -0
- package/android/src/main/res/drawable/thumb_container_bg.xml +8 -0
- package/android/src/main/res/drawable/yellow_border.xml +9 -0
- package/android/src/main/res/layout/video_trimmer_view.xml +194 -75
- package/android/src/main/res/values/colors.xml +15 -13
- package/android/src/main/res/xml/file_paths.xml +5 -0
- package/ios/AssetLoader.swift +99 -0
- package/ios/ErrorCode.swift +16 -0
- package/ios/VideoTrim.mm +4 -2
- package/ios/VideoTrim.swift +405 -168
- package/ios/VideoTrimmer.swift +16 -10
- package/ios/VideoTrimmerViewController.swift +79 -13
- package/lib/commonjs/index.js +20 -57
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/index.js +19 -57
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/index.d.ts +47 -9
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.tsx +56 -66
- package/android/src/main/java/com/videotrim/adapters/VideoTrimmerAdapter.java +0 -54
- package/android/src/main/java/com/videotrim/widgets/RangeSeekBarView.java +0 -534
- package/android/src/main/java/com/videotrim/widgets/SpacesItemDecoration2.java +0 -33
- package/android/src/main/java/com/videotrim/widgets/ZVideoView.java +0 -48
- package/android/src/main/java/iknow/android/utils/BuildConfig.java +0 -18
- package/android/src/main/java/iknow/android/utils/DateUtil.java +0 -64
- package/android/src/main/res/drawable/ic_video_pause_black.png +0 -0
- package/android/src/main/res/drawable/ic_video_play_black.png +0 -0
- package/android/src/main/res/drawable/ic_video_thumb_handle.png +0 -0
- package/android/src/main/res/drawable/icon_seek_bar.png +0 -0
- 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.
|
|
34
|
+
import android.widget.VideoView;
|
|
27
35
|
|
|
28
|
-
import androidx.
|
|
29
|
-
import androidx.
|
|
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.
|
|
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
|
|
58
|
-
private ZVideoView mVideoView;
|
|
61
|
+
private VideoView mVideoView;
|
|
59
62
|
private ImageView mPlayView;
|
|
60
|
-
private
|
|
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 =
|
|
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
|
-
|
|
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
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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 (
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
159
|
-
|
|
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
|
-
|
|
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(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
228
|
+
private void videoPrepared() {
|
|
229
|
+
mDuration = mVideoView.getDuration();
|
|
230
|
+
mMaxDuration = Math.min(mMaxDuration, mDuration);
|
|
231
|
+
mediaMetadataRetriever = MediaMetadataUtil.getMediaMetadataRetriever(mSourceUri.toString());
|
|
171
232
|
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
|
-
|
|
181
|
-
float r = videoHeight / (float) videoWidth;
|
|
182
|
-
lp.height = (int) (lp.width * r);
|
|
255
|
+
endTime = mDuration;
|
|
183
256
|
}
|
|
184
|
-
|
|
185
|
-
mDuration = mVideoView.getDuration();
|
|
257
|
+
updateHandlePositions();
|
|
186
258
|
|
|
187
|
-
|
|
259
|
+
loadingIndicator.setVisibility(View.GONE);
|
|
260
|
+
mPlayView.setVisibility(View.VISIBLE);
|
|
261
|
+
saveBtn.setVisibility(View.VISIBLE);
|
|
262
|
+
}
|
|
188
263
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
196
|
-
|
|
268
|
+
// Set initial handle positions if mMaxDuration < video duration
|
|
269
|
+
if (mMaxDuration < mDuration) {
|
|
270
|
+
endTime = mMaxDuration;
|
|
197
271
|
} else {
|
|
198
|
-
|
|
199
|
-
seekTo((int) mRedProgressBarPos);
|
|
272
|
+
endTime = mDuration;
|
|
200
273
|
}
|
|
201
|
-
|
|
202
|
-
|
|
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
|
|
206
|
-
|
|
207
|
-
|
|
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
|
|
211
|
-
mVideoView.pause();
|
|
315
|
+
private void mediaCompleted() {
|
|
212
316
|
setPlayPauseViewIcon(false);
|
|
317
|
+
mTimingHandler.removeCallbacks(mTimingRunnable);
|
|
213
318
|
}
|
|
214
319
|
|
|
215
|
-
private void
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
-
|
|
283
|
-
mVideoView.seekTo((int) msec);
|
|
394
|
+
updateCurrentTime(needUpdateProgress);
|
|
284
395
|
}
|
|
285
396
|
|
|
286
|
-
private boolean
|
|
287
|
-
|
|
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
|
-
|
|
291
|
-
|
|
402
|
+
@Override
|
|
403
|
+
protected void onDetachedFromWindow() {
|
|
404
|
+
super.onDetachedFromWindow();
|
|
405
|
+
mContext.getCurrentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
|
292
406
|
}
|
|
293
407
|
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
467
|
+
if (config.hasKey("enableHapticFeedback")) {
|
|
468
|
+
enableHapticFeedback = config.getBoolean("enableHapticFeedback");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
335
471
|
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
364
|
-
|
|
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
|
-
|
|
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
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
-
|
|
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
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
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
|
|
441
|
-
|
|
442
|
-
|
|
693
|
+
private void fadeInProgressIndicator() {
|
|
694
|
+
progressIndicator.setVisibility(View.VISIBLE);
|
|
695
|
+
progressIndicator.animate().alpha(1f).setDuration(250).start();
|
|
696
|
+
}
|
|
443
697
|
|
|
444
|
-
|
|
445
|
-
int
|
|
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
|
-
|
|
448
|
-
if (
|
|
449
|
-
|
|
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
|
|
456
|
-
|
|
457
|
-
|
|
721
|
+
private void startZoomWaitTimer() {
|
|
722
|
+
stopZoomWaitTimer();
|
|
723
|
+
if (isZoomedIn) {
|
|
724
|
+
return;
|
|
458
725
|
}
|
|
459
726
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
727
|
+
zoomRunnable = () -> {
|
|
728
|
+
Log.i("tag", "A Kiss after 500ms");
|
|
729
|
+
stopZoomWaitTimer();
|
|
730
|
+
zoomIfNeeded();
|
|
731
|
+
};
|
|
463
732
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
tv.setText(config.getString("cancelButtonText"));
|
|
467
|
-
}
|
|
733
|
+
zoomWaitTimer.postDelayed(zoomRunnable, 500);
|
|
734
|
+
}
|
|
468
735
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
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
|
}
|