react-native-video-trim 4.1.0 → 5.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +89 -76
- package/VideoTrim.podspec +3 -3
- package/android/build.gradle +6 -53
- package/android/gradle.properties +1 -1
- package/android/src/main/AndroidManifest.xml +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim/VideoTrim.kt → videotrim/VideoTrimModule.kt} +246 -232
- package/android/src/main/java/com/videotrim/VideoTrimPackage.kt +33 -0
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/enums/ErrorCode.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/IVideoTrimmerView.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/interfaces/VideoTrimListener.java +5 -4
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/MediaMetadataUtil.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/StorageUtil.java +1 -1
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/utils/VideoTrimmerUtil.java +20 -18
- package/android/src/main/java/com/{margelo/nitro/videotrim → videotrim}/widgets/VideoTrimmerView.java +44 -45
- package/ios/AssetLoader.h +19 -0
- package/ios/AssetLoader.mm +87 -0
- package/ios/ErrorCode.h +9 -0
- package/ios/ProgressAlertController.h +15 -0
- package/ios/ProgressAlertController.mm +78 -0
- package/ios/VideoTrim.h +31 -0
- package/ios/VideoTrim.mm +663 -0
- package/ios/VideoTrimmer.h +67 -0
- package/ios/VideoTrimmer.mm +863 -0
- package/ios/VideoTrimmerThumb.h +23 -0
- package/ios/VideoTrimmerThumb.mm +175 -0
- package/ios/VideoTrimmerViewController.h +52 -0
- package/ios/VideoTrimmerViewController.mm +533 -0
- package/lib/module/NativeVideoTrim.js +5 -0
- package/lib/module/NativeVideoTrim.js.map +1 -0
- package/lib/module/index.js +24 -24
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/src/NativeVideoTrim.d.ts +107 -0
- package/lib/typescript/src/NativeVideoTrim.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +16 -10
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/package.json +15 -18
- package/src/NativeVideoTrim.ts +113 -0
- package/src/index.tsx +29 -31
- package/android/CMakeLists.txt +0 -24
- package/android/src/main/cpp/cpp-adapter.cpp +0 -6
- package/android/src/main/java/com/margelo/nitro/videotrim/VideoTrimPackage.kt +0 -22
- package/ios/AssetLoader.swift +0 -99
- package/ios/ErrorCode.swift +0 -17
- package/ios/ProgressAlertController.swift +0 -100
- package/ios/VideoTrim.swift +0 -67
- package/ios/VideoTrimImpl.swift +0 -957
- package/ios/VideoTrimmer.swift +0 -872
- package/ios/VideoTrimmerThumb.swift +0 -175
- package/ios/VideoTrimmerViewController.swift +0 -557
- package/lib/module/VideoTrim.nitro.js +0 -4
- package/lib/module/VideoTrim.nitro.js.map +0 -1
- package/lib/typescript/src/VideoTrim.nitro.d.ts +0 -257
- package/lib/typescript/src/VideoTrim.nitro.d.ts.map +0 -1
- package/nitrogen/generated/android/c++/JEditorConfig.hpp +0 -237
- package/nitrogen/generated/android/c++/JFileValidationResult.hpp +0 -61
- package/nitrogen/generated/android/c++/JFunc_void.hpp +0 -74
- package/nitrogen/generated/android/c++/JFunc_void_std__string_std__unordered_map_std__string__std__string_.hpp +0 -89
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.cpp +0 -151
- package/nitrogen/generated/android/c++/JHybridVideoTrimSpec.hpp +0 -68
- package/nitrogen/generated/android/c++/JTrimOptions.hpp +0 -109
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/EditorConfig.kt +0 -72
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/FileValidationResult.kt +0 -28
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/Func_void_std__string_std__unordered_map_std__string__std__string_.kt +0 -80
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/HybridVideoTrimSpec.kt +0 -86
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/TrimOptions.kt +0 -40
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/videotrim/videotrimOnLoad.kt +0 -35
- package/nitrogen/generated/android/videotrim+autolinking.cmake +0 -78
- package/nitrogen/generated/android/videotrim+autolinking.gradle +0 -27
- package/nitrogen/generated/android/videotrimOnLoad.cpp +0 -50
- package/nitrogen/generated/android/videotrimOnLoad.hpp +0 -25
- package/nitrogen/generated/ios/VideoTrim+autolinking.rb +0 -60
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.cpp +0 -96
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Bridge.hpp +0 -374
- package/nitrogen/generated/ios/VideoTrim-Swift-Cxx-Umbrella.hpp +0 -56
- package/nitrogen/generated/ios/VideoTrimAutolinking.mm +0 -33
- package/nitrogen/generated/ios/VideoTrimAutolinking.swift +0 -25
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.cpp +0 -11
- package/nitrogen/generated/ios/c++/HybridVideoTrimSpecSwift.hpp +0 -127
- package/nitrogen/generated/ios/swift/EditorConfig.swift +0 -541
- package/nitrogen/generated/ios/swift/FileValidationResult.swift +0 -57
- package/nitrogen/generated/ios/swift/Func_void.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_FileValidationResult.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_bool.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_double.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__string.swift +0 -46
- package/nitrogen/generated/ios/swift/Func_void_std__string_std__unordered_map_std__string__std__string_.swift +0 -54
- package/nitrogen/generated/ios/swift/Func_void_std__vector_std__string_.swift +0 -46
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec.swift +0 -54
- package/nitrogen/generated/ios/swift/HybridVideoTrimSpec_cxx.swift +0 -241
- package/nitrogen/generated/ios/swift/TrimOptions.swift +0 -189
- package/nitrogen/generated/shared/c++/EditorConfig.hpp +0 -253
- package/nitrogen/generated/shared/c++/FileValidationResult.hpp +0 -77
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.cpp +0 -27
- package/nitrogen/generated/shared/c++/HybridVideoTrimSpec.hpp +0 -80
- package/nitrogen/generated/shared/c++/TrimOptions.hpp +0 -125
- package/src/VideoTrim.nitro.ts +0 -263
|
@@ -0,0 +1,863 @@
|
|
|
1
|
+
#import "VideoTrimmer.h"
|
|
2
|
+
#import "VideoTrimmerThumb.h"
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
#import <AVFoundation/AVFoundation.h>
|
|
5
|
+
|
|
6
|
+
@interface VideoTrimmer ()
|
|
7
|
+
|
|
8
|
+
// UI Components
|
|
9
|
+
@property (nonatomic, strong) VideoTrimmerThumb *thumbView;
|
|
10
|
+
@property (nonatomic, strong) UIView *wrapperView;
|
|
11
|
+
@property (nonatomic, strong) UIView *shadowView;
|
|
12
|
+
@property (nonatomic, strong) UIView *thumbnailClipView;
|
|
13
|
+
@property (nonatomic, strong) UIView *thumbnailWrapperView;
|
|
14
|
+
@property (nonatomic, strong) UIView *thumbnailTrackView;
|
|
15
|
+
@property (nonatomic, strong) UIView *thumbnailLeadingCoverView;
|
|
16
|
+
@property (nonatomic, strong) UIView *thumbnailTrailingCoverView;
|
|
17
|
+
@property (nonatomic, strong) UIView *leadingThumbRest;
|
|
18
|
+
@property (nonatomic, strong) UIView *trailingThumbRest;
|
|
19
|
+
@property (nonatomic, strong) UIView *progressIndicator;
|
|
20
|
+
@property (nonatomic, strong) UIControl *progressIndicatorControl;
|
|
21
|
+
|
|
22
|
+
// Timing and state properties
|
|
23
|
+
@property (nonatomic, assign, readwrite) CMTimeRange range;
|
|
24
|
+
@property (nonatomic, assign, readwrite) BOOL isZoomedIn;
|
|
25
|
+
@property (nonatomic, assign, readwrite) CMTimeRange zoomedInRange;
|
|
26
|
+
@property (nonatomic, assign, readwrite) BOOL isScrubbing;
|
|
27
|
+
@property (nonatomic, assign) CGFloat grabberOffset;
|
|
28
|
+
|
|
29
|
+
// Thumbnail management
|
|
30
|
+
@property (nonatomic, assign) CGSize lastKnownViewSizeForThumbnailGeneration;
|
|
31
|
+
@property (nonatomic, assign) CGSize thumbnailSize;
|
|
32
|
+
@property (nonatomic, assign) CMTimeRange lastKnownThumbnailRange;
|
|
33
|
+
@property (nonatomic, strong) NSMutableArray *thumbnails;
|
|
34
|
+
@property (nonatomic, strong) AVAssetImageGenerator *generator;
|
|
35
|
+
|
|
36
|
+
// Haptic feedback
|
|
37
|
+
@property (nonatomic, strong) UIImpactFeedbackGenerator *impactFeedbackGenerator;
|
|
38
|
+
@property (nonatomic, assign) BOOL didClampWhilePanning;
|
|
39
|
+
|
|
40
|
+
// Timers
|
|
41
|
+
@property (nonatomic, strong) NSTimer *zoomWaitTimer;
|
|
42
|
+
|
|
43
|
+
// Gesture recognizers
|
|
44
|
+
@property (nonatomic, strong, readwrite) UILongPressGestureRecognizer *leadingGestureRecognizer;
|
|
45
|
+
@property (nonatomic, strong, readwrite) UILongPressGestureRecognizer *trailingGestureRecognizer;
|
|
46
|
+
@property (nonatomic, strong, readwrite) UILongPressGestureRecognizer *progressGestureRecognizer;
|
|
47
|
+
@property (nonatomic, strong, readwrite) UILongPressGestureRecognizer *thumbnailInteractionGestureRecognizer;
|
|
48
|
+
|
|
49
|
+
@end
|
|
50
|
+
|
|
51
|
+
@interface VideoTrimmerThumbnail : NSObject
|
|
52
|
+
@property (nonatomic, strong) NSString *uuid;
|
|
53
|
+
@property (nonatomic, strong) UIImageView *imageView;
|
|
54
|
+
@property (nonatomic, assign) CMTime time;
|
|
55
|
+
- (instancetype)initWithImageView:(UIImageView *)imageView time:(CMTime)time;
|
|
56
|
+
@end
|
|
57
|
+
|
|
58
|
+
@implementation VideoTrimmerThumbnail
|
|
59
|
+
- (instancetype)initWithImageView:(UIImageView *)imageView time:(CMTime)time {
|
|
60
|
+
if (self = [super init]) {
|
|
61
|
+
self.uuid = [[NSUUID UUID] UUIDString];
|
|
62
|
+
self.imageView = imageView;
|
|
63
|
+
self.time = time;
|
|
64
|
+
}
|
|
65
|
+
return self;
|
|
66
|
+
}
|
|
67
|
+
@end
|
|
68
|
+
|
|
69
|
+
// Helper functions
|
|
70
|
+
CGFloat SnapToDevicePixels(CGFloat value) {
|
|
71
|
+
CGFloat scale = [UIScreen mainScreen].scale;
|
|
72
|
+
return round(value * scale) / scale;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
CGRect SnapToDevicePixelsRect(CGRect rect) {
|
|
76
|
+
return CGRectMake(
|
|
77
|
+
SnapToDevicePixels(rect.origin.x),
|
|
78
|
+
SnapToDevicePixels(rect.origin.y),
|
|
79
|
+
SnapToDevicePixels(CGRectGetMaxX(rect) - rect.origin.x),
|
|
80
|
+
SnapToDevicePixels(CGRectGetMaxY(rect) - rect.origin.y)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
CGSize ApplyVideoTransform(CGSize size, CGAffineTransform transform) {
|
|
85
|
+
return CGRectApplyAffineTransform(CGRectMake(0, 0, size.width, size.height), transform).size;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@implementation VideoTrimmer
|
|
89
|
+
|
|
90
|
+
+ (UIControlEvents)didBeginTrimmingFromStart { return (UIControlEvents)(1 << 19); }
|
|
91
|
+
+ (UIControlEvents)leadingGrabberChanged { return (UIControlEvents)(1 << 20); }
|
|
92
|
+
+ (UIControlEvents)didEndTrimmingFromStart { return (UIControlEvents)(1 << 21); }
|
|
93
|
+
+ (UIControlEvents)didBeginTrimmingFromEnd { return (UIControlEvents)(1 << 22); }
|
|
94
|
+
+ (UIControlEvents)trailingGrabberChanged { return (UIControlEvents)(1 << 23); }
|
|
95
|
+
+ (UIControlEvents)didEndTrimmingFromEnd { return (UIControlEvents)(1 << 24); }
|
|
96
|
+
+ (UIControlEvents)didBeginScrubbing { return (UIControlEvents)(0b00001000 << 24); }
|
|
97
|
+
+ (UIControlEvents)progressChanged { return (UIControlEvents)(0b00010000 << 24); }
|
|
98
|
+
+ (UIControlEvents)didEndScrubbing { return (UIControlEvents)(0b00100000 << 24); }
|
|
99
|
+
|
|
100
|
+
- (instancetype)initWithFrame:(CGRect)frame {
|
|
101
|
+
if (self = [super initWithFrame:frame]) {
|
|
102
|
+
[self setup];
|
|
103
|
+
}
|
|
104
|
+
return self;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
- (instancetype)initWithCoder:(NSCoder *)coder {
|
|
108
|
+
if (self = [super initWithCoder:coder]) {
|
|
109
|
+
[self setup];
|
|
110
|
+
}
|
|
111
|
+
return self;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
- (void)setAsset:(AVAsset *)asset {
|
|
115
|
+
_asset = asset;
|
|
116
|
+
if (asset) {
|
|
117
|
+
CMTime duration = asset.duration;
|
|
118
|
+
self.range = CMTimeRangeMake(kCMTimeZero, duration);
|
|
119
|
+
self.selectedRange = self.range;
|
|
120
|
+
self.lastKnownViewSizeForThumbnailGeneration = CGSizeZero;
|
|
121
|
+
[self setNeedsLayout];
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
- (void)setVideoComposition:(AVVideoComposition *)videoComposition {
|
|
126
|
+
_videoComposition = videoComposition;
|
|
127
|
+
self.lastKnownViewSizeForThumbnailGeneration = CGSizeZero;
|
|
128
|
+
[self setNeedsLayout];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
- (void)setRange:(CMTimeRange)range {
|
|
132
|
+
_range = range;
|
|
133
|
+
[self setNeedsLayout];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
- (void)setSelectedRange:(CMTimeRange)selectedRange {
|
|
137
|
+
_selectedRange = selectedRange;
|
|
138
|
+
[self setNeedsLayout];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
- (void)setProgress:(CMTime)progress {
|
|
142
|
+
_progress = progress;
|
|
143
|
+
[self setNeedsLayout];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
- (void)setProgressIndicatorMode:(VideoTrimmerProgressIndicatorMode)progressIndicatorMode {
|
|
147
|
+
_progressIndicatorMode = progressIndicatorMode;
|
|
148
|
+
[self updateProgressIndicator];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
- (void)setTrimmingState:(VideoTrimmerTrimmingState)trimmingState {
|
|
152
|
+
_trimmingState = trimmingState;
|
|
153
|
+
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
|
|
154
|
+
self.shadowView.layer.shadowOpacity = (trimmingState != VideoTrimmerTrimmingStateNone) ? 0.5 : 0.25;
|
|
155
|
+
self.shadowView.layer.shadowRadius = (trimmingState != VideoTrimmerTrimmingStateNone) ? 4 : 2;
|
|
156
|
+
} completion:nil];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
- (void)setTrackBackgroundColor:(UIColor *)trackBackgroundColor {
|
|
160
|
+
_trackBackgroundColor = trackBackgroundColor;
|
|
161
|
+
self.thumbnailWrapperView.backgroundColor = trackBackgroundColor;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
- (void)setThumbRestColor:(UIColor *)thumbRestColor {
|
|
165
|
+
_thumbRestColor = thumbRestColor;
|
|
166
|
+
self.leadingThumbRest.backgroundColor = thumbRestColor;
|
|
167
|
+
self.trailingThumbRest.backgroundColor = thumbRestColor;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
- (CMTimeRange)visibleRange {
|
|
171
|
+
return self.isZoomedIn ? self.zoomedInRange : self.range;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
- (CMTime)selectedTime {
|
|
175
|
+
switch (self.trimmingState) {
|
|
176
|
+
case VideoTrimmerTrimmingStateNone:
|
|
177
|
+
return kCMTimeZero;
|
|
178
|
+
case VideoTrimmerTrimmingStateLeading:
|
|
179
|
+
return self.selectedRange.start;
|
|
180
|
+
case VideoTrimmerTrimmingStateTrailing:
|
|
181
|
+
return CMTimeRangeGetEnd(self.selectedRange);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
- (void)setup {
|
|
186
|
+
// Initialize properties
|
|
187
|
+
self.horizontalInset = 16;
|
|
188
|
+
self.minimumDuration = CMTimeMake(600, 600); // 1 second
|
|
189
|
+
self.maximumDuration = kCMTimePositiveInfinity;
|
|
190
|
+
self.enableHapticFeedback = YES;
|
|
191
|
+
self.range = kCMTimeRangeInvalid;
|
|
192
|
+
self.selectedRange = kCMTimeRangeInvalid;
|
|
193
|
+
self.progressIndicatorMode = VideoTrimmerProgressIndicatorModeHiddenOnlyWhenTrimming;
|
|
194
|
+
self.progress = kCMTimeZero;
|
|
195
|
+
self.trimmingState = VideoTrimmerTrimmingStateNone;
|
|
196
|
+
self.isZoomedIn = NO;
|
|
197
|
+
self.zoomedInRange = kCMTimeRangeZero;
|
|
198
|
+
self.isScrubbing = NO;
|
|
199
|
+
self.trackBackgroundColor = [UIColor blackColor];
|
|
200
|
+
self.thumbRestColor = [UIColor blackColor];
|
|
201
|
+
self.lastKnownViewSizeForThumbnailGeneration = CGSizeZero;
|
|
202
|
+
self.thumbnailSize = CGSizeZero;
|
|
203
|
+
self.lastKnownThumbnailRange = kCMTimeRangeZero;
|
|
204
|
+
self.thumbnails = [[NSMutableArray alloc] init];
|
|
205
|
+
|
|
206
|
+
// Initialize UI components
|
|
207
|
+
self.thumbView = [[VideoTrimmerThumb alloc] init];
|
|
208
|
+
self.thumbView.accessibilityIdentifier = @"thumbView";
|
|
209
|
+
|
|
210
|
+
self.wrapperView = [[UIView alloc] init];
|
|
211
|
+
self.wrapperView.accessibilityIdentifier = @"wrapperView";
|
|
212
|
+
self.wrapperView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
213
|
+
|
|
214
|
+
self.shadowView = [[UIView alloc] init];
|
|
215
|
+
self.shadowView.accessibilityIdentifier = @"shadowView";
|
|
216
|
+
self.shadowView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
217
|
+
|
|
218
|
+
self.thumbnailClipView = [[UIView alloc] init];
|
|
219
|
+
self.thumbnailClipView.accessibilityIdentifier = @"thumbnailClipView";
|
|
220
|
+
self.thumbnailClipView.translatesAutoresizingMaskIntoConstraints = NO;
|
|
221
|
+
|
|
222
|
+
self.thumbnailWrapperView = [[UIView alloc] init];
|
|
223
|
+
self.thumbnailWrapperView.accessibilityIdentifier = @"thumbnailWrapperView";
|
|
224
|
+
|
|
225
|
+
self.thumbnailTrackView = [[UIView alloc] init];
|
|
226
|
+
self.thumbnailTrackView.accessibilityIdentifier = @"thumbnailTrackView";
|
|
227
|
+
|
|
228
|
+
self.thumbnailLeadingCoverView = [[UIView alloc] init];
|
|
229
|
+
self.thumbnailLeadingCoverView.accessibilityIdentifier = @"thumbnailLeadingCoverView";
|
|
230
|
+
|
|
231
|
+
self.thumbnailTrailingCoverView = [[UIView alloc] init];
|
|
232
|
+
self.thumbnailTrailingCoverView.accessibilityIdentifier = @"thumbnailTrailingCoverView";
|
|
233
|
+
|
|
234
|
+
self.leadingThumbRest = [[UIView alloc] init];
|
|
235
|
+
self.leadingThumbRest.accessibilityIdentifier = @"leadingThumbRest";
|
|
236
|
+
self.leadingThumbRest.translatesAutoresizingMaskIntoConstraints = NO;
|
|
237
|
+
|
|
238
|
+
self.trailingThumbRest = [[UIView alloc] init];
|
|
239
|
+
self.trailingThumbRest.accessibilityIdentifier = @"trailingThumbRest";
|
|
240
|
+
self.trailingThumbRest.translatesAutoresizingMaskIntoConstraints = NO;
|
|
241
|
+
|
|
242
|
+
self.progressIndicator = [[UIView alloc] init];
|
|
243
|
+
self.progressIndicator.accessibilityIdentifier = @"progressIndicator";
|
|
244
|
+
|
|
245
|
+
self.progressIndicatorControl = [[UIControl alloc] init];
|
|
246
|
+
self.progressIndicatorControl.accessibilityIdentifier = @"progressIndicatorControl";
|
|
247
|
+
|
|
248
|
+
// Set up view hierarchy
|
|
249
|
+
[self addSubview:self.thumbnailClipView];
|
|
250
|
+
[self.thumbnailClipView addSubview:self.thumbnailWrapperView];
|
|
251
|
+
[self.thumbnailWrapperView addSubview:self.leadingThumbRest];
|
|
252
|
+
[self.thumbnailWrapperView addSubview:self.trailingThumbRest];
|
|
253
|
+
[self.thumbnailWrapperView addSubview:self.thumbnailTrackView];
|
|
254
|
+
[self.thumbnailWrapperView addSubview:self.thumbnailLeadingCoverView];
|
|
255
|
+
[self.thumbnailWrapperView addSubview:self.thumbnailTrailingCoverView];
|
|
256
|
+
|
|
257
|
+
[self addSubview:self.shadowView];
|
|
258
|
+
self.wrapperView.clipsToBounds = YES;
|
|
259
|
+
[self.shadowView addSubview:self.wrapperView];
|
|
260
|
+
[self.wrapperView addSubview:self.thumbView];
|
|
261
|
+
[self.wrapperView addSubview:self.progressIndicator];
|
|
262
|
+
[self.wrapperView addSubview:self.progressIndicatorControl];
|
|
263
|
+
|
|
264
|
+
// Configure styles
|
|
265
|
+
self.progressIndicator.backgroundColor = [UIColor whiteColor];
|
|
266
|
+
self.progressIndicator.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
267
|
+
self.progressIndicator.layer.shadowOffset = CGSizeZero;
|
|
268
|
+
self.progressIndicator.layer.shadowRadius = 2;
|
|
269
|
+
self.progressIndicator.layer.shadowOpacity = 0.25;
|
|
270
|
+
self.progressIndicator.layer.cornerRadius = 2;
|
|
271
|
+
if (@available(iOS 13.0, *)) {
|
|
272
|
+
self.progressIndicator.layer.cornerCurve = kCACornerCurveContinuous;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
self.thumbnailClipView.clipsToBounds = YES;
|
|
276
|
+
self.thumbnailTrackView.clipsToBounds = YES;
|
|
277
|
+
self.thumbnailLeadingCoverView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.75];
|
|
278
|
+
self.thumbnailTrailingCoverView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.75];
|
|
279
|
+
|
|
280
|
+
self.leadingThumbRest.backgroundColor = self.thumbRestColor;
|
|
281
|
+
self.trailingThumbRest.backgroundColor = self.thumbRestColor;
|
|
282
|
+
|
|
283
|
+
self.thumbnailWrapperView.backgroundColor = self.trackBackgroundColor;
|
|
284
|
+
self.thumbnailWrapperView.layer.cornerRadius = 6;
|
|
285
|
+
if (@available(iOS 13.0, *)) {
|
|
286
|
+
self.thumbnailWrapperView.layer.cornerCurve = kCACornerCurveContinuous;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
self.leadingThumbRest.layer.cornerRadius = 6;
|
|
290
|
+
self.leadingThumbRest.layer.maskedCorners = kCALayerMinXMinYCorner | kCALayerMinXMaxYCorner;
|
|
291
|
+
if (@available(iOS 13.0, *)) {
|
|
292
|
+
self.leadingThumbRest.layer.cornerCurve = kCACornerCurveContinuous;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
self.trailingThumbRest.layer.cornerRadius = 6;
|
|
296
|
+
self.trailingThumbRest.layer.maskedCorners = kCALayerMaxXMinYCorner | kCALayerMaxXMaxYCorner;
|
|
297
|
+
if (@available(iOS 13.0, *)) {
|
|
298
|
+
self.trailingThumbRest.layer.cornerCurve = kCACornerCurveContinuous;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
self.shadowView.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
302
|
+
self.shadowView.layer.shadowOffset = CGSizeZero;
|
|
303
|
+
self.shadowView.layer.shadowRadius = 2;
|
|
304
|
+
self.shadowView.layer.shadowOpacity = 0.25;
|
|
305
|
+
|
|
306
|
+
[self setupConstraints];
|
|
307
|
+
[self setupGestures];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
- (void)setupConstraints {
|
|
311
|
+
[NSLayoutConstraint activateConstraints:@[
|
|
312
|
+
[self.thumbnailClipView.topAnchor constraintEqualToAnchor:self.topAnchor],
|
|
313
|
+
[self.thumbnailClipView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
|
314
|
+
[self.thumbnailClipView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
|
315
|
+
[self.thumbnailClipView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
|
316
|
+
|
|
317
|
+
[self.shadowView.topAnchor constraintEqualToAnchor:self.topAnchor],
|
|
318
|
+
[self.shadowView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor],
|
|
319
|
+
[self.shadowView.leadingAnchor constraintEqualToAnchor:self.leadingAnchor],
|
|
320
|
+
[self.shadowView.trailingAnchor constraintEqualToAnchor:self.trailingAnchor],
|
|
321
|
+
|
|
322
|
+
[self.wrapperView.topAnchor constraintEqualToAnchor:self.shadowView.topAnchor],
|
|
323
|
+
[self.wrapperView.bottomAnchor constraintEqualToAnchor:self.shadowView.bottomAnchor],
|
|
324
|
+
[self.wrapperView.leadingAnchor constraintEqualToAnchor:self.shadowView.leadingAnchor],
|
|
325
|
+
[self.wrapperView.trailingAnchor constraintEqualToAnchor:self.shadowView.trailingAnchor],
|
|
326
|
+
|
|
327
|
+
[self.leadingThumbRest.topAnchor constraintEqualToAnchor:self.thumbnailWrapperView.topAnchor],
|
|
328
|
+
[self.leadingThumbRest.bottomAnchor constraintEqualToAnchor:self.thumbnailWrapperView.bottomAnchor],
|
|
329
|
+
[self.leadingThumbRest.leadingAnchor constraintEqualToAnchor:self.thumbnailWrapperView.leadingAnchor],
|
|
330
|
+
[self.leadingThumbRest.widthAnchor constraintEqualToConstant:self.thumbView.chevronWidth],
|
|
331
|
+
|
|
332
|
+
[self.trailingThumbRest.topAnchor constraintEqualToAnchor:self.thumbnailWrapperView.topAnchor],
|
|
333
|
+
[self.trailingThumbRest.bottomAnchor constraintEqualToAnchor:self.thumbnailWrapperView.bottomAnchor],
|
|
334
|
+
[self.trailingThumbRest.trailingAnchor constraintEqualToAnchor:self.thumbnailWrapperView.trailingAnchor],
|
|
335
|
+
[self.trailingThumbRest.widthAnchor constraintEqualToConstant:self.thumbView.chevronWidth]
|
|
336
|
+
]];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
- (void)setupGestures {
|
|
340
|
+
self.leadingGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(leadingGrabberPanned:)];
|
|
341
|
+
self.leadingGestureRecognizer.allowableMovement = CGFLOAT_MAX;
|
|
342
|
+
self.leadingGestureRecognizer.minimumPressDuration = 0;
|
|
343
|
+
[self.thumbView.leadingGrabber addGestureRecognizer:self.leadingGestureRecognizer];
|
|
344
|
+
|
|
345
|
+
self.trailingGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(trailingGrabberPanned:)];
|
|
346
|
+
self.trailingGestureRecognizer.allowableMovement = CGFLOAT_MAX;
|
|
347
|
+
self.trailingGestureRecognizer.minimumPressDuration = 0;
|
|
348
|
+
[self.thumbView.trailingGrabber addGestureRecognizer:self.trailingGestureRecognizer];
|
|
349
|
+
|
|
350
|
+
self.progressGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(progressGrabberPanned:)];
|
|
351
|
+
self.progressGestureRecognizer.allowableMovement = CGFLOAT_MAX;
|
|
352
|
+
self.progressGestureRecognizer.minimumPressDuration = 0;
|
|
353
|
+
[self.progressGestureRecognizer requireGestureRecognizerToFail:self.leadingGestureRecognizer];
|
|
354
|
+
[self.progressGestureRecognizer requireGestureRecognizerToFail:self.trailingGestureRecognizer];
|
|
355
|
+
[self.progressIndicatorControl addGestureRecognizer:self.progressGestureRecognizer];
|
|
356
|
+
|
|
357
|
+
self.thumbnailInteractionGestureRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(thumbnailPanned:)];
|
|
358
|
+
self.thumbnailInteractionGestureRecognizer.allowableMovement = CGFLOAT_MAX;
|
|
359
|
+
self.thumbnailInteractionGestureRecognizer.minimumPressDuration = 0;
|
|
360
|
+
[self.thumbnailInteractionGestureRecognizer requireGestureRecognizerToFail:self.leadingGestureRecognizer];
|
|
361
|
+
[self.thumbnailInteractionGestureRecognizer requireGestureRecognizerToFail:self.trailingGestureRecognizer];
|
|
362
|
+
[self.thumbView addGestureRecognizer:self.thumbnailInteractionGestureRecognizer];
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
- (void)regenerateThumbnailsIfNeeded {
|
|
366
|
+
CGSize size = self.bounds.size;
|
|
367
|
+
if (size.width <= 0 || size.height <= 0) return;
|
|
368
|
+
if (CGSizeEqualToSize(self.lastKnownViewSizeForThumbnailGeneration, size) && CMTimeRangeEqual(self.lastKnownThumbnailRange, [self visibleRange])) return;
|
|
369
|
+
if (!self.asset) return;
|
|
370
|
+
|
|
371
|
+
NSArray<AVAssetTrack *> *videoTracks = [self.asset tracksWithMediaType:AVMediaTypeVideo];
|
|
372
|
+
if (videoTracks.count == 0) return;
|
|
373
|
+
AVAssetTrack *track = videoTracks.firstObject;
|
|
374
|
+
|
|
375
|
+
self.lastKnownViewSizeForThumbnailGeneration = size;
|
|
376
|
+
self.lastKnownThumbnailRange = [self visibleRange];
|
|
377
|
+
|
|
378
|
+
CGSize naturalSize = track.naturalSize;
|
|
379
|
+
CGAffineTransform transform = track.preferredTransform;
|
|
380
|
+
CGSize fixedSize = ApplyVideoTransform(naturalSize, transform);
|
|
381
|
+
|
|
382
|
+
AVAssetImageGenerator *generator = [[AVAssetImageGenerator alloc] initWithAsset:self.asset];
|
|
383
|
+
generator.apertureMode = AVAssetImageGeneratorApertureModeCleanAperture;
|
|
384
|
+
generator.videoComposition = self.videoComposition;
|
|
385
|
+
self.generator = generator;
|
|
386
|
+
|
|
387
|
+
CGFloat height = size.height - self.thumbView.edgeHeight * 2;
|
|
388
|
+
self.thumbnailSize = CGSizeMake(height / fixedSize.height * fixedSize.width, height);
|
|
389
|
+
NSInteger numberOfThumbnails = (NSInteger)ceil(size.width / self.thumbnailSize.width);
|
|
390
|
+
|
|
391
|
+
NSMutableArray *newThumbnails = [[NSMutableArray alloc] init];
|
|
392
|
+
double thumbnailDuration = CMTimeGetSeconds([self visibleRange].duration) / (double)numberOfThumbnails;
|
|
393
|
+
NSMutableArray *times = [[NSMutableArray alloc] init];
|
|
394
|
+
|
|
395
|
+
for (NSInteger index = -3; index < numberOfThumbnails + 6; index++) {
|
|
396
|
+
CMTime time = CMTimeAdd([self visibleRange].start, CMTimeMakeWithSeconds(thumbnailDuration * (double)index, self.asset.duration.timescale * 2));
|
|
397
|
+
if (CMTimeCompare(time, kCMTimeZero) < 0) continue;
|
|
398
|
+
[times addObject:[NSValue valueWithCMTime:time]];
|
|
399
|
+
|
|
400
|
+
UIImageView *imageView = [[UIImageView alloc] init];
|
|
401
|
+
VideoTrimmerThumbnail *newThumbnail = [[VideoTrimmerThumbnail alloc] initWithImageView:imageView time:time];
|
|
402
|
+
[self.thumbnailTrackView addSubview:imageView];
|
|
403
|
+
[newThumbnails addObject:newThumbnail];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
generator.appliesPreferredTrackTransform = YES;
|
|
407
|
+
CGFloat scale = [UIScreen mainScreen].scale;
|
|
408
|
+
generator.maximumSize = CGSizeMake(self.thumbnailSize.width * scale, self.thumbnailSize.height * scale);
|
|
409
|
+
|
|
410
|
+
NSArray *oldThumbnails = [self.thumbnails copy];
|
|
411
|
+
[self.thumbnails addObjectsFromArray:newThumbnails];
|
|
412
|
+
|
|
413
|
+
[UIView animateWithDuration:0.25 delay:0.25 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
|
|
414
|
+
for (VideoTrimmerThumbnail *thumbnail in oldThumbnails) {
|
|
415
|
+
thumbnail.imageView.alpha = 0;
|
|
416
|
+
}
|
|
417
|
+
} completion:^(BOOL finished) {
|
|
418
|
+
for (VideoTrimmerThumbnail *thumbnail in oldThumbnails) {
|
|
419
|
+
[thumbnail.imageView removeFromSuperview];
|
|
420
|
+
}
|
|
421
|
+
NSSet *uuidsToRemove = [NSSet setWithArray:[oldThumbnails valueForKey:@"uuid"]];
|
|
422
|
+
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"NOT (uuid IN %@)", uuidsToRemove];
|
|
423
|
+
self.thumbnails = [[self.thumbnails filteredArrayUsingPredicate:predicate] mutableCopy];
|
|
424
|
+
}];
|
|
425
|
+
|
|
426
|
+
__block NSInteger seenIndex = 0;
|
|
427
|
+
generator.requestedTimeToleranceBefore = kCMTimeZero;
|
|
428
|
+
generator.requestedTimeToleranceAfter = kCMTimeZero;
|
|
429
|
+
|
|
430
|
+
[generator generateCGImagesAsynchronouslyForTimes:times completionHandler:^(CMTime requestedTime, CGImageRef cgImage, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * error) {
|
|
431
|
+
seenIndex++;
|
|
432
|
+
|
|
433
|
+
if (!cgImage) return;
|
|
434
|
+
UIImage *image = [UIImage imageWithCGImage:cgImage];
|
|
435
|
+
|
|
436
|
+
if (seenIndex <= newThumbnails.count) {
|
|
437
|
+
UIImageView *imageView = ((VideoTrimmerThumbnail *)newThumbnails[seenIndex - 1]).imageView;
|
|
438
|
+
|
|
439
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
440
|
+
[UIView transitionWithView:imageView duration:0.25 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
|
|
441
|
+
imageView.image = image;
|
|
442
|
+
} completion:nil];
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
}];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
- (CMTime)timeForLocation:(CGFloat)x {
|
|
449
|
+
CGSize size = self.bounds.size;
|
|
450
|
+
CGFloat inset = self.thumbView.chevronWidth + self.horizontalInset;
|
|
451
|
+
CGFloat offset = x - inset;
|
|
452
|
+
|
|
453
|
+
CGFloat availableWidth = size.width - inset * 2;
|
|
454
|
+
CGFloat visibleDurationInSeconds = (CGFloat)CMTimeGetSeconds([self visibleRange].duration);
|
|
455
|
+
CGFloat ratio = visibleDurationInSeconds != 0 ? availableWidth / visibleDurationInSeconds : 0;
|
|
456
|
+
|
|
457
|
+
CMTime timeDifference = CMTimeMakeWithSeconds((double)(offset / ratio), 600);
|
|
458
|
+
return CMTimeAdd([self visibleRange].start, timeDifference);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
- (CGFloat)locationForTime:(CMTime)time {
|
|
462
|
+
CGSize size = self.bounds.size;
|
|
463
|
+
CGFloat inset = self.thumbView.chevronWidth + self.horizontalInset;
|
|
464
|
+
CGFloat availableWidth = size.width - inset * 2;
|
|
465
|
+
|
|
466
|
+
CMTime offset = CMTimeSubtract(time, [self visibleRange].start);
|
|
467
|
+
|
|
468
|
+
CGFloat visibleDurationInSeconds = (CGFloat)CMTimeGetSeconds([self visibleRange].duration);
|
|
469
|
+
CGFloat ratio = visibleDurationInSeconds != 0 ? availableWidth / visibleDurationInSeconds : 0;
|
|
470
|
+
|
|
471
|
+
CGFloat location = (CGFloat)CMTimeGetSeconds(offset) * ratio;
|
|
472
|
+
return SnapToDevicePixels(location) + inset;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
- (void)startZoomWaitTimer {
|
|
476
|
+
[self stopZoomWaitTimer];
|
|
477
|
+
if (self.isZoomedIn) return;
|
|
478
|
+
|
|
479
|
+
self.zoomWaitTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 repeats:NO block:^(NSTimer * _Nonnull timer) {
|
|
480
|
+
[self stopZoomWaitTimer];
|
|
481
|
+
[self zoomIfNeeded];
|
|
482
|
+
}];
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
- (void)stopZoomWaitTimer {
|
|
486
|
+
[self.zoomWaitTimer invalidate];
|
|
487
|
+
self.zoomWaitTimer = nil;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
- (void)stopZoomIfNeeded {
|
|
491
|
+
[self stopZoomWaitTimer];
|
|
492
|
+
self.isZoomedIn = NO;
|
|
493
|
+
[self animateChanges];
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
- (void)zoomIfNeeded {
|
|
497
|
+
if (self.isZoomedIn) return;
|
|
498
|
+
|
|
499
|
+
CGSize size = self.bounds.size;
|
|
500
|
+
CGFloat inset = self.thumbView.chevronWidth + self.horizontalInset;
|
|
501
|
+
CGFloat availableWidth = size.width - inset * 2;
|
|
502
|
+
CGFloat newDuration = (CGFloat)(CMTimeGetSeconds(self.range.duration) > 4 ? 2.0 : CMTimeGetSeconds(self.range.duration) * 0.5);
|
|
503
|
+
|
|
504
|
+
CMTime durationTime = CMTimeMakeWithSeconds((double)newDuration, 600);
|
|
505
|
+
|
|
506
|
+
if (self.trimmingState == VideoTrimmerTrimmingStateLeading) {
|
|
507
|
+
CGFloat position = [self locationForTime:self.selectedRange.start] - inset;
|
|
508
|
+
CGFloat start = position / availableWidth * newDuration;
|
|
509
|
+
self.zoomedInRange = CMTimeRangeMake(CMTimeSubtract(self.selectedRange.start, CMTimeMakeWithSeconds((double)start, 600)), durationTime);
|
|
510
|
+
} else {
|
|
511
|
+
CGFloat position = [self locationForTime:CMTimeRangeGetEnd(self.selectedRange)] - inset;
|
|
512
|
+
CGFloat durationToStart = position / availableWidth * newDuration;
|
|
513
|
+
CMTime newStart = CMTimeSubtract(CMTimeRangeGetEnd(self.selectedRange), CMTimeMakeWithSeconds((double)durationToStart, 600));
|
|
514
|
+
self.zoomedInRange = CMTimeRangeMake(newStart, durationTime);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
self.isZoomedIn = YES;
|
|
518
|
+
[self animateChanges];
|
|
519
|
+
|
|
520
|
+
if (self.enableHapticFeedback) {
|
|
521
|
+
UISelectionFeedbackGenerator *generator = [[UISelectionFeedbackGenerator alloc] init];
|
|
522
|
+
[generator selectionChanged];
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
- (void)animateChanges {
|
|
527
|
+
[self setNeedsLayout];
|
|
528
|
+
[self.thumbView setNeedsLayout];
|
|
529
|
+
[UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
|
|
530
|
+
[self layoutIfNeeded];
|
|
531
|
+
[self.thumbView layoutIfNeeded];
|
|
532
|
+
} completion:nil];
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
- (void)startPanning {
|
|
536
|
+
self.didClampWhilePanning = NO;
|
|
537
|
+
|
|
538
|
+
if (self.enableHapticFeedback) {
|
|
539
|
+
UISelectionFeedbackGenerator *generator = [[UISelectionFeedbackGenerator alloc] init];
|
|
540
|
+
[generator selectionChanged];
|
|
541
|
+
self.impactFeedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy];
|
|
542
|
+
[self.impactFeedbackGenerator prepare];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
|
|
546
|
+
[self updateProgressIndicator];
|
|
547
|
+
} completion:nil];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
- (void)stopPanning {
|
|
551
|
+
self.trimmingState = VideoTrimmerTrimmingStateNone;
|
|
552
|
+
[self stopZoomIfNeeded];
|
|
553
|
+
self.impactFeedbackGenerator = nil;
|
|
554
|
+
|
|
555
|
+
[UIView animateWithDuration:0.25 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionAllowUserInteraction animations:^{
|
|
556
|
+
[self updateProgressIndicator];
|
|
557
|
+
} completion:nil];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
- (void)updateProgressIndicator {
|
|
561
|
+
switch (self.progressIndicatorMode) {
|
|
562
|
+
case VideoTrimmerProgressIndicatorModeAlwaysHidden:
|
|
563
|
+
self.progressIndicator.alpha = 0;
|
|
564
|
+
self.progressIndicatorControl.userInteractionEnabled = NO;
|
|
565
|
+
break;
|
|
566
|
+
|
|
567
|
+
case VideoTrimmerProgressIndicatorModeAlwaysShown:
|
|
568
|
+
self.progressIndicator.alpha = 1;
|
|
569
|
+
self.progressIndicatorControl.userInteractionEnabled = YES;
|
|
570
|
+
[self setNeedsLayout];
|
|
571
|
+
break;
|
|
572
|
+
|
|
573
|
+
case VideoTrimmerProgressIndicatorModeHiddenOnlyWhenTrimming:
|
|
574
|
+
self.progressIndicator.alpha = (self.trimmingState == VideoTrimmerTrimmingStateNone) ? 1 : 0;
|
|
575
|
+
self.progressIndicatorControl.userInteractionEnabled = (self.trimmingState == VideoTrimmerTrimmingStateNone);
|
|
576
|
+
if (self.trimmingState == VideoTrimmerTrimmingStateNone) {
|
|
577
|
+
[self setNeedsLayout];
|
|
578
|
+
if ([UIView inheritedAnimationDuration] > 0) {
|
|
579
|
+
[UIView performWithoutAnimation:^{
|
|
580
|
+
[self layoutIfNeeded];
|
|
581
|
+
}];
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
break;
|
|
585
|
+
}
|
|
586
|
+
self.progressIndicatorControl.alpha = self.progressIndicator.alpha;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
#pragma mark - Gesture Handlers
|
|
590
|
+
|
|
591
|
+
- (void)thumbnailPanned:(UILongPressGestureRecognizer *)sender {
|
|
592
|
+
[self progressGrabberPanned:sender];
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
- (void)progressGrabberPanned:(UILongPressGestureRecognizer *)sender {
|
|
596
|
+
void (^handleChanged)(void) = ^{
|
|
597
|
+
CGPoint location = [sender locationInView:self];
|
|
598
|
+
CMTime time = [self timeForLocation:location.x + self.grabberOffset];
|
|
599
|
+
|
|
600
|
+
BOOL didClamp = NO;
|
|
601
|
+
if (CMTimeCompare(time, self.selectedRange.start) < 0) {
|
|
602
|
+
time = self.selectedRange.start;
|
|
603
|
+
didClamp = YES;
|
|
604
|
+
}
|
|
605
|
+
if (CMTimeCompare(time, CMTimeRangeGetEnd(self.selectedRange)) > 0) {
|
|
606
|
+
time = CMTimeRangeGetEnd(self.selectedRange);
|
|
607
|
+
didClamp = YES;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (didClamp && didClamp != self.didClampWhilePanning) {
|
|
611
|
+
[self.impactFeedbackGenerator impactOccurred];
|
|
612
|
+
}
|
|
613
|
+
self.didClampWhilePanning = didClamp;
|
|
614
|
+
|
|
615
|
+
self.progress = time;
|
|
616
|
+
[self setNeedsLayout];
|
|
617
|
+
[self sendActionsForControlEvents:[VideoTrimmer progressChanged]];
|
|
618
|
+
};
|
|
619
|
+
|
|
620
|
+
switch (sender.state) {
|
|
621
|
+
case UIGestureRecognizerStateBegan:
|
|
622
|
+
if (self.enableHapticFeedback) {
|
|
623
|
+
UISelectionFeedbackGenerator *generator = [[UISelectionFeedbackGenerator alloc] init];
|
|
624
|
+
[generator selectionChanged];
|
|
625
|
+
self.impactFeedbackGenerator = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleHeavy];
|
|
626
|
+
[self.impactFeedbackGenerator prepare];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
self.didClampWhilePanning = NO;
|
|
630
|
+
self.isScrubbing = YES;
|
|
631
|
+
[self sendActionsForControlEvents:[VideoTrimmer didBeginScrubbing]];
|
|
632
|
+
handleChanged();
|
|
633
|
+
break;
|
|
634
|
+
|
|
635
|
+
case UIGestureRecognizerStateChanged:
|
|
636
|
+
handleChanged();
|
|
637
|
+
break;
|
|
638
|
+
|
|
639
|
+
case UIGestureRecognizerStateEnded:
|
|
640
|
+
case UIGestureRecognizerStateCancelled:
|
|
641
|
+
self.impactFeedbackGenerator = nil;
|
|
642
|
+
self.isScrubbing = NO;
|
|
643
|
+
[self sendActionsForControlEvents:[VideoTrimmer didEndScrubbing]];
|
|
644
|
+
break;
|
|
645
|
+
|
|
646
|
+
default:
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
- (void)leadingGrabberPanned:(UILongPressGestureRecognizer *)sender {
|
|
652
|
+
switch (sender.state) {
|
|
653
|
+
case UIGestureRecognizerStateBegan:
|
|
654
|
+
self.trimmingState = VideoTrimmerTrimmingStateLeading;
|
|
655
|
+
self.grabberOffset = self.thumbView.chevronWidth - [sender locationInView:self.thumbView.leadingGrabber].x;
|
|
656
|
+
|
|
657
|
+
[self startPanning];
|
|
658
|
+
[self sendActionsForControlEvents:[VideoTrimmer didBeginTrimmingFromStart]];
|
|
659
|
+
break;
|
|
660
|
+
|
|
661
|
+
case UIGestureRecognizerStateChanged: {
|
|
662
|
+
CGPoint location = [sender locationInView:self];
|
|
663
|
+
CMTime current = [self timeForLocation:location.x + self.grabberOffset];
|
|
664
|
+
CMTime min = CMTimeSubtract(CMTimeRangeGetEnd(self.selectedRange), self.minimumDuration);
|
|
665
|
+
|
|
666
|
+
BOOL didClamp = NO;
|
|
667
|
+
CMTime endTime = CMTimeRangeGetEnd(self.selectedRange);
|
|
668
|
+
|
|
669
|
+
// Create range with explicit duration calculation (like Swift CMTimeRange(start:, end:))
|
|
670
|
+
CMTime duration = CMTimeSubtract(endTime, current);
|
|
671
|
+
CMTimeRange newRange = CMTimeRangeMake(current, duration);
|
|
672
|
+
|
|
673
|
+
if (CMTimeCompare(current, min) != -1) {
|
|
674
|
+
CMTime minDuration = CMTimeSubtract(endTime, min);
|
|
675
|
+
newRange = CMTimeRangeMake(min, minDuration);
|
|
676
|
+
didClamp = YES;
|
|
677
|
+
} else if (CMTimeCompare(newRange.duration, self.maximumDuration) != -1) {
|
|
678
|
+
CMTime time = CMTimeSubtract(endTime, self.maximumDuration);
|
|
679
|
+
newRange = CMTimeRangeMake(time, self.maximumDuration);
|
|
680
|
+
didClamp = YES;
|
|
681
|
+
} else if (CMTimeCompare(newRange.start, self.range.start) != 1) {
|
|
682
|
+
CMTime rangeDuration = CMTimeSubtract(endTime, self.range.start);
|
|
683
|
+
newRange = CMTimeRangeMake(self.range.start, rangeDuration);
|
|
684
|
+
didClamp = YES;
|
|
685
|
+
} else if (CMTimeCompare(newRange.duration, self.minimumDuration) != 1) {
|
|
686
|
+
CMTime minDuration = CMTimeSubtract(endTime, min);
|
|
687
|
+
newRange = CMTimeRangeMake(min, minDuration);
|
|
688
|
+
didClamp = YES;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (didClamp && didClamp != self.didClampWhilePanning) {
|
|
692
|
+
[self.impactFeedbackGenerator impactOccurred];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
self.didClampWhilePanning = didClamp;
|
|
696
|
+
self.selectedRange = newRange;
|
|
697
|
+
[self sendActionsForControlEvents:[VideoTrimmer leadingGrabberChanged]];
|
|
698
|
+
[self setNeedsLayout];
|
|
699
|
+
|
|
700
|
+
[self startZoomWaitTimer];
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
case UIGestureRecognizerStateEnded:
|
|
705
|
+
[self stopPanning];
|
|
706
|
+
[self sendActionsForControlEvents:[VideoTrimmer didEndTrimmingFromStart]];
|
|
707
|
+
break;
|
|
708
|
+
|
|
709
|
+
case UIGestureRecognizerStateCancelled:
|
|
710
|
+
[self stopPanning];
|
|
711
|
+
break;
|
|
712
|
+
|
|
713
|
+
case UIGestureRecognizerStatePossible:
|
|
714
|
+
case UIGestureRecognizerStateFailed:
|
|
715
|
+
break;
|
|
716
|
+
|
|
717
|
+
default:
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
- (void)trailingGrabberPanned:(UILongPressGestureRecognizer *)sender {
|
|
723
|
+
switch (sender.state) {
|
|
724
|
+
case UIGestureRecognizerStateBegan:
|
|
725
|
+
self.trimmingState = VideoTrimmerTrimmingStateTrailing;
|
|
726
|
+
self.grabberOffset = [sender locationInView:self.thumbView.trailingGrabber].x;
|
|
727
|
+
|
|
728
|
+
[self startPanning];
|
|
729
|
+
[self sendActionsForControlEvents:[VideoTrimmer didBeginTrimmingFromEnd]];
|
|
730
|
+
break;
|
|
731
|
+
|
|
732
|
+
case UIGestureRecognizerStateChanged: {
|
|
733
|
+
CGPoint location = [sender locationInView:self];
|
|
734
|
+
CMTime current = [self timeForLocation:location.x - self.grabberOffset];
|
|
735
|
+
CMTime min = CMTimeAdd(self.selectedRange.start, self.minimumDuration);
|
|
736
|
+
|
|
737
|
+
BOOL didClamp = NO;
|
|
738
|
+
CMTime endTime = [self timeForLocation:location.x - self.grabberOffset];
|
|
739
|
+
|
|
740
|
+
// Create range with explicit duration calculation (like Swift CMTimeRange(start:, end:))
|
|
741
|
+
CMTime duration = CMTimeSubtract(endTime, self.selectedRange.start);
|
|
742
|
+
CMTimeRange newRange = CMTimeRangeMake(self.selectedRange.start, duration);
|
|
743
|
+
|
|
744
|
+
|
|
745
|
+
if (CMTimeCompare(current, min) == -1) {
|
|
746
|
+
CMTime minDuration = CMTimeSubtract(min, self.selectedRange.start);
|
|
747
|
+
newRange = CMTimeRangeMake(self.selectedRange.start, minDuration);
|
|
748
|
+
didClamp = YES;
|
|
749
|
+
} else if (CMTimeCompare(newRange.duration, self.maximumDuration) != -1) {
|
|
750
|
+
newRange = CMTimeRangeMake(self.selectedRange.start, self.maximumDuration);
|
|
751
|
+
didClamp = YES;
|
|
752
|
+
} else if (CMTimeCompare(endTime, CMTimeRangeGetEnd(self.range)) != -1) {
|
|
753
|
+
// prevent endTime to be greater than video endTime
|
|
754
|
+
CMTime maxDuration = CMTimeSubtract(CMTimeRangeGetEnd(self.range), self.selectedRange.start);
|
|
755
|
+
newRange = CMTimeRangeMake(self.selectedRange.start, maxDuration);
|
|
756
|
+
didClamp = YES;
|
|
757
|
+
} else if (CMTimeCompare(newRange.duration, self.minimumDuration) != 1) {
|
|
758
|
+
CMTime minDuration = CMTimeSubtract(min, self.selectedRange.start);
|
|
759
|
+
newRange = CMTimeRangeMake(self.selectedRange.start, minDuration);
|
|
760
|
+
didClamp = YES;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
if (didClamp && didClamp != self.didClampWhilePanning) {
|
|
764
|
+
[self.impactFeedbackGenerator impactOccurred];
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
self.didClampWhilePanning = didClamp;
|
|
768
|
+
self.selectedRange = newRange;
|
|
769
|
+
[self sendActionsForControlEvents:[VideoTrimmer trailingGrabberChanged]];
|
|
770
|
+
[self setNeedsLayout];
|
|
771
|
+
|
|
772
|
+
[self startZoomWaitTimer];
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
case UIGestureRecognizerStateEnded:
|
|
777
|
+
[self stopPanning];
|
|
778
|
+
[self sendActionsForControlEvents:[VideoTrimmer didEndTrimmingFromEnd]];
|
|
779
|
+
break;
|
|
780
|
+
|
|
781
|
+
case UIGestureRecognizerStateCancelled:
|
|
782
|
+
[self stopPanning];
|
|
783
|
+
break;
|
|
784
|
+
|
|
785
|
+
case UIGestureRecognizerStatePossible:
|
|
786
|
+
case UIGestureRecognizerStateFailed:
|
|
787
|
+
break;
|
|
788
|
+
|
|
789
|
+
default:
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
#pragma mark - UIView
|
|
795
|
+
|
|
796
|
+
- (CGSize)intrinsicContentSize {
|
|
797
|
+
return CGSizeMake(UIViewNoIntrinsicMetric, 50);
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
- (void)layoutSubviews {
|
|
801
|
+
[super layoutSubviews];
|
|
802
|
+
|
|
803
|
+
CGSize size = self.bounds.size;
|
|
804
|
+
CGFloat inset = self.thumbView.chevronWidth;
|
|
805
|
+
CGFloat left = [self locationForTime:self.selectedRange.start] - inset;
|
|
806
|
+
CGFloat right = [self locationForTime:CMTimeRangeGetEnd(self.selectedRange)] + inset;
|
|
807
|
+
|
|
808
|
+
if (right > self.bounds.size.width) {
|
|
809
|
+
right = self.bounds.size.width + inset * 2;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
if (left < 0) {
|
|
813
|
+
left = -inset;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
CGRect rect = CGRectMake(0, 0, size.width, size.height);
|
|
817
|
+
self.thumbView.frame = CGRectMake(left, 0, MAX(right - left, inset * 2), size.height);
|
|
818
|
+
|
|
819
|
+
BOOL isZoomedToEnd = (self.trimmingState == VideoTrimmerTrimmingStateLeading && self.isZoomedIn);
|
|
820
|
+
|
|
821
|
+
CGFloat thumbnailOffset = (self.isZoomedIn ? self.horizontalInset + inset + 6 : 0);
|
|
822
|
+
CGFloat coverOffset = thumbnailOffset - self.horizontalInset;
|
|
823
|
+
CGFloat coverStartOffset = (self.isZoomedIn ? 0 : inset);
|
|
824
|
+
|
|
825
|
+
CGRect thumbnailRect = CGRectInset(rect, self.horizontalInset - thumbnailOffset, self.thumbView.edgeHeight);
|
|
826
|
+
self.thumbnailWrapperView.frame = thumbnailRect;
|
|
827
|
+
self.thumbnailTrackView.frame = CGRectMake(0, 0, thumbnailRect.size.width - (isZoomedToEnd ? 0 : inset), thumbnailRect.size.height);
|
|
828
|
+
self.thumbnailLeadingCoverView.frame = CGRectMake(coverStartOffset, 0, left + inset * 0.5 + coverOffset - coverStartOffset, thumbnailRect.size.height);
|
|
829
|
+
self.thumbnailTrailingCoverView.frame = CGRectMake(right - inset * 0.5 + coverOffset, 0, thumbnailRect.size.width - coverStartOffset - (right - inset * 0.5 + coverOffset), thumbnailRect.size.height);
|
|
830
|
+
|
|
831
|
+
if (self.progressIndicator.alpha > 0) {
|
|
832
|
+
CGFloat progressWidth = 4;
|
|
833
|
+
CGFloat progressIndicatorOffset = [self locationForTime:self.progress];
|
|
834
|
+
CGFloat progressLeft = MIN(MAX(CGRectGetMinX(self.thumbView.frame) + inset, progressIndicatorOffset - progressWidth * 0.5), CGRectGetMaxX(self.thumbView.frame) - inset - progressWidth);
|
|
835
|
+
self.progressIndicator.frame = CGRectMake(progressLeft, CGRectGetMinY(thumbnailRect), progressWidth, thumbnailRect.size.height);
|
|
836
|
+
|
|
837
|
+
CGFloat progressControlWidth = 24;
|
|
838
|
+
|
|
839
|
+
CGFloat progressControlLeft = MAX(CGRectGetMinX(self.thumbView.frame) + inset, progressLeft);
|
|
840
|
+
CGFloat progressControlRight = progressLeft + progressControlWidth;
|
|
841
|
+
if (progressControlRight > CGRectGetMaxX(self.thumbView.frame) - inset) {
|
|
842
|
+
progressControlRight = CGRectGetMaxX(self.thumbView.frame) - inset;
|
|
843
|
+
progressControlLeft = MAX(CGRectGetMinX(self.thumbView.frame) + inset, progressControlRight - progressControlWidth);
|
|
844
|
+
}
|
|
845
|
+
self.progressIndicatorControl.frame = CGRectMake(progressControlLeft, CGRectGetMinY(thumbnailRect), progressControlRight - progressControlLeft, thumbnailRect.size.height);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
[self regenerateThumbnailsIfNeeded];
|
|
849
|
+
|
|
850
|
+
for (VideoTrimmerThumbnail *thumbnail in self.thumbnails) {
|
|
851
|
+
CGFloat position = [self locationForTime:thumbnail.time] - self.horizontalInset + thumbnailOffset;
|
|
852
|
+
CGRect frame = CGRectMake(position, 0, self.thumbnailSize.width, self.thumbnailSize.height);
|
|
853
|
+
if (thumbnail.imageView.bounds.size.width == 0) {
|
|
854
|
+
[UIView performWithoutAnimation:^{
|
|
855
|
+
thumbnail.imageView.frame = frame;
|
|
856
|
+
}];
|
|
857
|
+
} else {
|
|
858
|
+
thumbnail.imageView.frame = frame;
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
@end
|