react-native-video-trim 1.0.21 → 1.0.22
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/ios/VideoTrimmer.swift +387 -330
- package/ios/VideoTrimmerThumb.swift +139 -90
- package/ios/VideoTrimmerViewController.swift +224 -166
- package/package.json +1 -1
package/ios/VideoTrimmer.swift
CHANGED
|
@@ -11,36 +11,113 @@ import AVFoundation
|
|
|
11
11
|
// Controls that allows trimming a range and scrubbing a progress indicator
|
|
12
12
|
@available(iOS 13.0, *)
|
|
13
13
|
@IBDesignable class VideoTrimmer: UIControl {
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
// events for changing selectedRange ("trimming")
|
|
16
|
-
static let
|
|
17
|
-
static let
|
|
18
|
-
static let
|
|
19
|
-
|
|
16
|
+
static let didBeginTrimmingFromStart = UIControl.Event(rawValue: 1 << 19)
|
|
17
|
+
static let leadingGrabberChanged = UIControl.Event(rawValue: 1 << 20)
|
|
18
|
+
static let didEndTrimmingFromStart = UIControl.Event(rawValue: 1 << 21)
|
|
19
|
+
|
|
20
|
+
static let didBeginTrimmingFromEnd = UIControl.Event(rawValue: 1 << 22)
|
|
21
|
+
static let trailingGrabberChanged = UIControl.Event(rawValue: 1 << 23)
|
|
22
|
+
static let didEndTrimmingFromEnd = UIControl.Event(rawValue: 1 << 24)
|
|
23
|
+
|
|
20
24
|
// events for scrubbing the progress indicator ("scrubbing")
|
|
21
25
|
static let didBeginScrubbing = UIControl.Event(rawValue: 0b00001000 << 24)
|
|
22
26
|
static let progressChanged = UIControl.Event(rawValue: 0b00010000 << 24)
|
|
23
27
|
static let didEndScrubbing = UIControl.Event(rawValue: 0b00100000 << 24)
|
|
24
|
-
|
|
28
|
+
|
|
25
29
|
private struct Thumbnail {
|
|
26
30
|
let uuid = UUID()
|
|
27
31
|
let imageView: UIImageView
|
|
28
32
|
let time: CMTime
|
|
29
33
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
private let
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
private let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
|
|
35
|
+
// currently there're warnings in the console saying that initial width of thumbView is 0
|
|
36
|
+
// TODO: migrate all to AutoLayout
|
|
37
|
+
private let thumbView: VideoTrimmerThumb = {
|
|
38
|
+
let view = VideoTrimmerThumb()
|
|
39
|
+
view.accessibilityIdentifier = "wrapperView"
|
|
40
|
+
return view
|
|
41
|
+
}()
|
|
42
|
+
|
|
43
|
+
private let wrapperView: UIView = {
|
|
44
|
+
let view = UIView()
|
|
45
|
+
view.accessibilityIdentifier = "wrapperView"
|
|
46
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
47
|
+
return view
|
|
48
|
+
}()
|
|
49
|
+
|
|
50
|
+
private let shadowView: UIView = {
|
|
51
|
+
let view = UIView()
|
|
52
|
+
view.accessibilityIdentifier = "shadowView"
|
|
53
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
54
|
+
|
|
55
|
+
return view
|
|
56
|
+
}()
|
|
57
|
+
|
|
58
|
+
private let thumbnailClipView: UIView = {
|
|
59
|
+
let view = UIView()
|
|
60
|
+
view.accessibilityIdentifier = "thumbnailClipView"
|
|
61
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
62
|
+
|
|
63
|
+
return view
|
|
64
|
+
}()
|
|
65
|
+
|
|
66
|
+
private let thumbnailWrapperView: UIView = {
|
|
67
|
+
let view = UIView()
|
|
68
|
+
view.accessibilityIdentifier = "thumbnailWrapperView"
|
|
69
|
+
|
|
70
|
+
return view
|
|
71
|
+
}()
|
|
72
|
+
|
|
73
|
+
private let thumbnailTrackView: UIView = {
|
|
74
|
+
let view = UIView()
|
|
75
|
+
view.accessibilityIdentifier = "thumbnailTrackView"
|
|
76
|
+
|
|
77
|
+
return view
|
|
78
|
+
}()
|
|
79
|
+
|
|
80
|
+
private let thumbnailLeadingCoverView: UIView = {
|
|
81
|
+
let view = UIView()
|
|
82
|
+
view.accessibilityIdentifier = "thumbnailLeadingCoverView"
|
|
83
|
+
|
|
84
|
+
return view
|
|
85
|
+
}()
|
|
86
|
+
|
|
87
|
+
private let thumbnailTrailingCoverView: UIView = {
|
|
88
|
+
let view = UIView()
|
|
89
|
+
view.accessibilityIdentifier = "thumbnailTrailingCoverView"
|
|
90
|
+
|
|
91
|
+
return view
|
|
92
|
+
}()
|
|
93
|
+
|
|
94
|
+
private let leadingThumbRest: UIView = {
|
|
95
|
+
let view = UIView()
|
|
96
|
+
view.accessibilityIdentifier = "leadingThumbRest"
|
|
97
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
98
|
+
return view
|
|
99
|
+
}()
|
|
100
|
+
|
|
101
|
+
private let trailingThumbRest: UIView = {
|
|
102
|
+
let view = UIView()
|
|
103
|
+
view.accessibilityIdentifier = "trailingThumbRest"
|
|
104
|
+
view.translatesAutoresizingMaskIntoConstraints = false
|
|
105
|
+
return view
|
|
106
|
+
}()
|
|
107
|
+
|
|
108
|
+
private let progressIndicator: UIView = {
|
|
109
|
+
let view = UIView()
|
|
110
|
+
view.accessibilityIdentifier = "progressIndicator"
|
|
111
|
+
return view
|
|
112
|
+
}()
|
|
113
|
+
|
|
114
|
+
private let progressIndicatorControl: UIControl = {
|
|
115
|
+
let view = UIControl()
|
|
116
|
+
view.accessibilityIdentifier = "progressIndicatorControl"
|
|
117
|
+
|
|
118
|
+
return view
|
|
119
|
+
}()
|
|
120
|
+
|
|
44
121
|
// defines how much the control is insetted from its sides:
|
|
45
122
|
// this is set to 16, so that you can have the control fullscreen (and have it
|
|
46
123
|
// edge-to-edge when zooming in)
|
|
@@ -50,7 +127,7 @@ import AVFoundation
|
|
|
50
127
|
setNeedsLayout()
|
|
51
128
|
}
|
|
52
129
|
}
|
|
53
|
-
|
|
130
|
+
|
|
54
131
|
// the asset to use
|
|
55
132
|
var asset: AVAsset? {
|
|
56
133
|
didSet {
|
|
@@ -63,7 +140,7 @@ import AVFoundation
|
|
|
63
140
|
}
|
|
64
141
|
}
|
|
65
142
|
}
|
|
66
|
-
|
|
143
|
+
|
|
67
144
|
// the video composition to use
|
|
68
145
|
var videoComposition: AVVideoComposition? {
|
|
69
146
|
didSet {
|
|
@@ -71,11 +148,11 @@ import AVFoundation
|
|
|
71
148
|
setNeedsLayout()
|
|
72
149
|
}
|
|
73
150
|
}
|
|
74
|
-
|
|
151
|
+
|
|
75
152
|
// a clip cannot be trimmed shorter than this duration
|
|
76
153
|
var minimumDuration: CMTime = CMTime(seconds: 1, preferredTimescale: 600)
|
|
77
154
|
var maximumDuration: CMTime = .positiveInfinity
|
|
78
|
-
|
|
155
|
+
|
|
79
156
|
// the available range of the asset.
|
|
80
157
|
// Will be set to the full duration of the asset when assigning a new asset
|
|
81
158
|
var range: CMTimeRange = .invalid {
|
|
@@ -83,7 +160,7 @@ import AVFoundation
|
|
|
83
160
|
setNeedsLayout()
|
|
84
161
|
}
|
|
85
162
|
}
|
|
86
|
-
|
|
163
|
+
|
|
87
164
|
// the range that is selected, will be set to the full duration
|
|
88
165
|
// when changing asset.
|
|
89
166
|
var selectedRange: CMTimeRange = .invalid {
|
|
@@ -91,7 +168,7 @@ import AVFoundation
|
|
|
91
168
|
setNeedsLayout()
|
|
92
169
|
}
|
|
93
170
|
}
|
|
94
|
-
|
|
171
|
+
|
|
95
172
|
// defines what to do with the progress indicator
|
|
96
173
|
enum ProgressIndicatorMode {
|
|
97
174
|
case hiddenOnlyWhenTrimming // the progress indicator gets hidden when the user starts trimming
|
|
@@ -103,46 +180,21 @@ import AVFoundation
|
|
|
103
180
|
updateProgressIndicator()
|
|
104
181
|
}
|
|
105
182
|
}
|
|
106
|
-
|
|
107
|
-
func setProgressIndicatorMode(_ mode: ProgressIndicatorMode, animated: Bool) {
|
|
108
|
-
guard progressIndicatorMode != mode else {return}
|
|
109
|
-
|
|
110
|
-
if animated == true {
|
|
111
|
-
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
112
|
-
self.progressIndicatorMode = mode
|
|
113
|
-
self.layoutIfNeeded()
|
|
114
|
-
})
|
|
115
|
-
} else {
|
|
116
|
-
progressIndicatorMode = mode
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
|
|
183
|
+
|
|
120
184
|
// defines where the progress indicator is shown.
|
|
121
185
|
var progress: CMTime = .zero {
|
|
122
186
|
didSet {
|
|
123
187
|
setNeedsLayout()
|
|
124
188
|
}
|
|
125
189
|
}
|
|
126
|
-
|
|
127
|
-
func setProgress(_ progress: CMTime, animated: Bool) {
|
|
128
|
-
guard CMTimeCompare(self.progress, progress) != 0 else {return}
|
|
129
|
-
|
|
130
|
-
self.progress = progress
|
|
131
|
-
if animated == true {
|
|
132
|
-
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
133
|
-
self.layoutIfNeeded()
|
|
134
|
-
})
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
190
|
+
|
|
139
191
|
// defines if the user is trimming or not, and if so, which edge
|
|
140
192
|
enum TrimmingState {
|
|
141
193
|
case none // user isn't trimming
|
|
142
194
|
case leading // user is trimming the leading part of the asset
|
|
143
195
|
case trailing // user is trimming the trailing part of the asset
|
|
144
196
|
}
|
|
145
|
-
|
|
197
|
+
|
|
146
198
|
private(set) var trimmingState = TrimmingState.none {
|
|
147
199
|
didSet {
|
|
148
200
|
UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
@@ -151,22 +203,22 @@ import AVFoundation
|
|
|
151
203
|
})
|
|
152
204
|
}
|
|
153
205
|
}
|
|
154
|
-
|
|
206
|
+
|
|
155
207
|
// yes if the user is zoomed in
|
|
156
208
|
private(set) var isZoomedIn = false
|
|
157
209
|
private(set) var zoomedInRange: CMTimeRange = .zero
|
|
158
|
-
|
|
159
|
-
|
|
210
|
+
|
|
211
|
+
|
|
160
212
|
// yes if the user is scrubbing the progress indicator
|
|
161
213
|
private(set) var isScrubbing = false
|
|
162
|
-
|
|
214
|
+
|
|
163
215
|
// background color for the track
|
|
164
216
|
var trackBackgroundColor = UIColor.black {
|
|
165
217
|
didSet {
|
|
166
218
|
thumbnailWrapperView.backgroundColor = trackBackgroundColor
|
|
167
219
|
}
|
|
168
220
|
}
|
|
169
|
-
|
|
221
|
+
|
|
170
222
|
// background color for the place where the thumbs rest on when the selectedRange == range
|
|
171
223
|
var thumbRestColor = UIColor.black {
|
|
172
224
|
didSet {
|
|
@@ -174,42 +226,42 @@ import AVFoundation
|
|
|
174
226
|
trailingThumbRest.backgroundColor = thumbRestColor
|
|
175
227
|
}
|
|
176
228
|
}
|
|
177
|
-
|
|
229
|
+
|
|
178
230
|
// the range that's currently visible: could be less than "range" when zoomed in
|
|
179
231
|
var visibleRange: CMTimeRange {
|
|
180
232
|
return isZoomedIn == true ? zoomedInRange : range
|
|
181
233
|
}
|
|
182
|
-
|
|
234
|
+
|
|
183
235
|
// the time that's currently selected by the user when trimming
|
|
184
236
|
var selectedTime: CMTime {
|
|
185
237
|
switch trimmingState {
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
238
|
+
case .none: return .zero
|
|
239
|
+
case .leading: return selectedRange.start
|
|
240
|
+
case .trailing: return selectedRange.end
|
|
189
241
|
}
|
|
190
242
|
}
|
|
191
|
-
|
|
243
|
+
|
|
192
244
|
// gesture recognizers used. Can be used, for instance, to
|
|
193
245
|
// require a tableview panGestureRecognizer to fail
|
|
194
246
|
private (set) var leadingGestureRecognizer: UILongPressGestureRecognizer!
|
|
195
247
|
private (set) var trailingGestureRecognizer: UILongPressGestureRecognizer!
|
|
196
248
|
private (set) var progressGestureRecognizer: UILongPressGestureRecognizer!
|
|
197
249
|
private (set) var thumbnailInteractionGestureRecognizer: UILongPressGestureRecognizer!
|
|
198
|
-
|
|
250
|
+
|
|
199
251
|
// private stuff
|
|
200
252
|
private var grabberOffset = CGFloat(0)
|
|
201
253
|
private var zoomWaitTimer: Timer?
|
|
202
|
-
|
|
254
|
+
|
|
203
255
|
private var lastKnownViewSizeForThumbnailGeneration: CGSize = .zero
|
|
204
256
|
private var thumbnailSize: CGSize = .zero
|
|
205
257
|
private var lastKnownThumbnailRange: CMTimeRange = .zero
|
|
206
258
|
private var thumbnails = Array<Thumbnail>()
|
|
207
259
|
private var generator: AVAssetImageGenerator?
|
|
208
|
-
|
|
260
|
+
|
|
209
261
|
private var impactFeedbackGenerator: UIImpactFeedbackGenerator?
|
|
210
262
|
private var didClampWhilePanning = false
|
|
211
|
-
|
|
212
|
-
|
|
263
|
+
|
|
264
|
+
|
|
213
265
|
// MARK: - Private
|
|
214
266
|
private func setup() {
|
|
215
267
|
addSubview(thumbnailClipView)
|
|
@@ -219,7 +271,7 @@ import AVFoundation
|
|
|
219
271
|
thumbnailWrapperView.addSubview(thumbnailTrackView)
|
|
220
272
|
thumbnailWrapperView.addSubview(thumbnailLeadingCoverView)
|
|
221
273
|
thumbnailWrapperView.addSubview(thumbnailTrailingCoverView)
|
|
222
|
-
|
|
274
|
+
|
|
223
275
|
progressIndicator.backgroundColor = .white
|
|
224
276
|
progressIndicator.layer.shadowColor = UIColor.black.cgColor
|
|
225
277
|
progressIndicator.layer.shadowOffset = .zero
|
|
@@ -227,57 +279,94 @@ import AVFoundation
|
|
|
227
279
|
progressIndicator.layer.shadowOpacity = 0.25
|
|
228
280
|
progressIndicator.layer.cornerRadius = 2
|
|
229
281
|
progressIndicator.layer.cornerCurve = .continuous
|
|
230
|
-
|
|
282
|
+
|
|
231
283
|
addSubview(shadowView)
|
|
232
284
|
wrapperView.clipsToBounds = true
|
|
233
285
|
shadowView.addSubview(wrapperView)
|
|
234
286
|
wrapperView.addSubview(thumbView)
|
|
235
|
-
|
|
236
287
|
wrapperView.addSubview(progressIndicator)
|
|
237
288
|
wrapperView.addSubview(progressIndicatorControl)
|
|
238
|
-
|
|
289
|
+
|
|
239
290
|
thumbnailClipView.clipsToBounds = true
|
|
240
291
|
thumbnailTrackView.clipsToBounds = true
|
|
241
292
|
thumbnailLeadingCoverView.backgroundColor = UIColor(white: 0, alpha: 0.75)
|
|
242
293
|
thumbnailTrailingCoverView.backgroundColor = UIColor(white: 0, alpha: 0.75)
|
|
243
|
-
|
|
294
|
+
|
|
244
295
|
leadingThumbRest.backgroundColor = thumbRestColor
|
|
245
296
|
trailingThumbRest.backgroundColor = thumbRestColor
|
|
246
|
-
|
|
297
|
+
|
|
247
298
|
thumbnailWrapperView.backgroundColor = trackBackgroundColor
|
|
248
299
|
thumbnailWrapperView.layer.cornerRadius = 6
|
|
249
300
|
thumbnailWrapperView.layer.cornerCurve = .continuous
|
|
250
|
-
|
|
301
|
+
|
|
251
302
|
leadingThumbRest.layer.cornerRadius = 6
|
|
252
303
|
leadingThumbRest.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMinXMinYCorner]
|
|
253
304
|
leadingThumbRest.layer.cornerCurve = .continuous
|
|
254
|
-
|
|
305
|
+
|
|
255
306
|
trailingThumbRest.layer.cornerRadius = 6
|
|
256
307
|
trailingThumbRest.layer.maskedCorners = [.layerMaxXMaxYCorner, .layerMaxXMinYCorner]
|
|
257
308
|
trailingThumbRest.layer.cornerCurve = .continuous
|
|
258
|
-
|
|
309
|
+
|
|
259
310
|
shadowView.layer.shadowColor = UIColor.black.cgColor
|
|
260
311
|
shadowView.layer.shadowOffset = .zero
|
|
261
312
|
shadowView.layer.shadowRadius = 2
|
|
262
313
|
shadowView.layer.shadowOpacity = 0.25
|
|
263
|
-
|
|
314
|
+
|
|
315
|
+
setupConstraints()
|
|
316
|
+
setupGestures()
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// thumbView.topAnchor.constraint(equalTo: thumbnailWrapperView.topAnchor),
|
|
320
|
+
// thumbView.bottomAnchor.constraint(equalTo: thumbnailWrapperView.bottomAnchor),
|
|
321
|
+
// thumbView.leadingAnchor.constraint(equalTo: thumbnailWrapperView.leadingAnchor),
|
|
322
|
+
// thumbView.trailingAnchor.constraint(equalTo: thumbnailWrapperView.trailingAnchor),
|
|
323
|
+
private func setupConstraints() {
|
|
324
|
+
NSLayoutConstraint.activate([
|
|
325
|
+
thumbnailClipView.topAnchor.constraint(equalTo: self.topAnchor),
|
|
326
|
+
thumbnailClipView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
327
|
+
thumbnailClipView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
|
328
|
+
thumbnailClipView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
|
329
|
+
|
|
330
|
+
shadowView.topAnchor.constraint(equalTo: self.topAnchor),
|
|
331
|
+
shadowView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
|
|
332
|
+
shadowView.leadingAnchor.constraint(equalTo: self.leadingAnchor),
|
|
333
|
+
shadowView.trailingAnchor.constraint(equalTo: self.trailingAnchor),
|
|
334
|
+
|
|
335
|
+
wrapperView.topAnchor.constraint(equalTo: shadowView.topAnchor),
|
|
336
|
+
wrapperView.bottomAnchor.constraint(equalTo: shadowView.bottomAnchor),
|
|
337
|
+
wrapperView.leadingAnchor.constraint(equalTo: shadowView.leadingAnchor),
|
|
338
|
+
wrapperView.trailingAnchor.constraint(equalTo: shadowView.trailingAnchor),
|
|
339
|
+
|
|
340
|
+
leadingThumbRest.topAnchor.constraint(equalTo: thumbnailWrapperView.topAnchor),
|
|
341
|
+
leadingThumbRest.bottomAnchor.constraint(equalTo: thumbnailWrapperView.bottomAnchor),
|
|
342
|
+
leadingThumbRest.leadingAnchor.constraint(equalTo: thumbnailWrapperView.leadingAnchor),
|
|
343
|
+
leadingThumbRest.widthAnchor.constraint(equalToConstant: thumbView.chevronWidth),
|
|
344
|
+
|
|
345
|
+
trailingThumbRest.topAnchor.constraint(equalTo: thumbnailWrapperView.topAnchor),
|
|
346
|
+
trailingThumbRest.bottomAnchor.constraint(equalTo: thumbnailWrapperView.bottomAnchor),
|
|
347
|
+
trailingThumbRest.trailingAnchor.constraint(equalTo: thumbnailWrapperView.trailingAnchor),
|
|
348
|
+
trailingThumbRest.widthAnchor.constraint(equalToConstant: thumbView.chevronWidth),
|
|
349
|
+
])
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private func setupGestures() {
|
|
264
353
|
leadingGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(leadingGrabberPanned(_:)))
|
|
265
354
|
leadingGestureRecognizer.allowableMovement = CGFloat.greatestFiniteMagnitude
|
|
266
355
|
leadingGestureRecognizer.minimumPressDuration = 0
|
|
267
356
|
thumbView.leadingGrabber.addGestureRecognizer(leadingGestureRecognizer)
|
|
268
|
-
|
|
357
|
+
|
|
269
358
|
trailingGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(trailingGrabberPanned(_:)))
|
|
270
359
|
trailingGestureRecognizer.allowableMovement = CGFloat.greatestFiniteMagnitude
|
|
271
360
|
trailingGestureRecognizer.minimumPressDuration = 0
|
|
272
361
|
thumbView.trailingGrabber.addGestureRecognizer(trailingGestureRecognizer)
|
|
273
|
-
|
|
362
|
+
|
|
274
363
|
progressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(progressGrabberPanned(_:)))
|
|
275
364
|
progressGestureRecognizer.allowableMovement = CGFloat.greatestFiniteMagnitude
|
|
276
365
|
progressGestureRecognizer.minimumPressDuration = 0
|
|
277
366
|
progressGestureRecognizer.require(toFail: leadingGestureRecognizer)
|
|
278
367
|
progressGestureRecognizer.require(toFail: trailingGestureRecognizer)
|
|
279
368
|
progressIndicatorControl.addGestureRecognizer(progressGestureRecognizer)
|
|
280
|
-
|
|
369
|
+
|
|
281
370
|
thumbnailInteractionGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(thumbnailPanned(_:)))
|
|
282
371
|
thumbnailInteractionGestureRecognizer.allowableMovement = CGFloat.greatestFiniteMagnitude
|
|
283
372
|
thumbnailInteractionGestureRecognizer.minimumPressDuration = 0
|
|
@@ -285,30 +374,30 @@ import AVFoundation
|
|
|
285
374
|
thumbnailInteractionGestureRecognizer.require(toFail: trailingGestureRecognizer)
|
|
286
375
|
thumbView.addGestureRecognizer(thumbnailInteractionGestureRecognizer)
|
|
287
376
|
}
|
|
288
|
-
|
|
377
|
+
|
|
289
378
|
private func regenerateThumbnailsIfNeeded() {
|
|
290
379
|
let size = bounds.size
|
|
291
380
|
guard size.width > 0 && size.height > 0 else {return}
|
|
292
381
|
guard lastKnownViewSizeForThumbnailGeneration != size || CMTimeRangeEqual(lastKnownThumbnailRange, visibleRange) == false else {return}
|
|
293
382
|
guard let asset = asset else {return}
|
|
294
383
|
guard let track = asset.tracks(withMediaType: .video).first else {return}
|
|
295
|
-
|
|
384
|
+
|
|
296
385
|
lastKnownViewSizeForThumbnailGeneration = size
|
|
297
386
|
lastKnownThumbnailRange = visibleRange
|
|
298
|
-
|
|
387
|
+
|
|
299
388
|
let naturalSize = track.naturalSize
|
|
300
389
|
let transform = track.preferredTransform
|
|
301
390
|
let fixedSize = naturalSize.applyingVideoTransform(transform)
|
|
302
|
-
|
|
391
|
+
|
|
303
392
|
let generator = AVAssetImageGenerator(asset: asset)
|
|
304
393
|
generator.apertureMode = .cleanAperture
|
|
305
394
|
generator.videoComposition = videoComposition
|
|
306
395
|
self.generator = generator
|
|
307
|
-
|
|
396
|
+
|
|
308
397
|
let height = size.height - thumbView.edgeHeight * 2
|
|
309
398
|
thumbnailSize = CGSize(width: height / fixedSize.height * fixedSize.width, height: height)
|
|
310
399
|
let numberOfThumbnails = Int(ceil(size.width / thumbnailSize.width))
|
|
311
|
-
|
|
400
|
+
|
|
312
401
|
var newThumbnails = Array<Thumbnail>()
|
|
313
402
|
let thumbnailDuration = visibleRange.duration.seconds / Double(numberOfThumbnails)
|
|
314
403
|
var times = Array<NSValue>()
|
|
@@ -317,18 +406,18 @@ import AVFoundation
|
|
|
317
406
|
let time = CMTimeAdd(visibleRange.start, CMTime(seconds: thumbnailDuration * Double(index), preferredTimescale: asset.duration.timescale * 2))
|
|
318
407
|
guard CMTimeCompare(time, .zero) != -1 else {continue}
|
|
319
408
|
times.append(NSValue(time: time))
|
|
320
|
-
|
|
409
|
+
|
|
321
410
|
let newThumbnail = Thumbnail(imageView: UIImageView(), time: time)
|
|
322
411
|
self.thumbnailTrackView.addSubview(newThumbnail.imageView)
|
|
323
412
|
newThumbnails.append(newThumbnail)
|
|
324
413
|
}
|
|
325
|
-
|
|
414
|
+
|
|
326
415
|
generator.appliesPreferredTrackTransform = true
|
|
327
416
|
generator.maximumSize = CGSize(width: thumbnailSize.width * UIScreen.main.scale, height: thumbnailSize.height * UIScreen.main.scale)
|
|
328
|
-
|
|
417
|
+
|
|
329
418
|
let oldThumbnails = thumbnails
|
|
330
419
|
thumbnails.append(contentsOf: newThumbnails)
|
|
331
|
-
|
|
420
|
+
|
|
332
421
|
UIView.animate(withDuration: 0.25, delay: 0.25, options: [.beginFromCurrentState], animations: {
|
|
333
422
|
oldThumbnails.forEach {$0.imageView.alpha = 0}
|
|
334
423
|
}, completion: { _ in
|
|
@@ -336,17 +425,17 @@ import AVFoundation
|
|
|
336
425
|
let uuidsToRemove = Set(oldThumbnails.map({$0.uuid}))
|
|
337
426
|
self.thumbnails.removeAll(where: {uuidsToRemove.contains($0.uuid)})
|
|
338
427
|
})
|
|
339
|
-
|
|
428
|
+
|
|
340
429
|
var seenIndex = 0
|
|
341
430
|
generator.requestedTimeToleranceBefore = .zero
|
|
342
431
|
generator.requestedTimeToleranceAfter = .zero
|
|
343
432
|
generator.generateCGImagesAsynchronously(forTimes: times) { requestedTime, cgImage, actualTime, result, error in
|
|
344
433
|
DispatchQueue.main.async {
|
|
345
434
|
seenIndex += 1
|
|
346
|
-
|
|
435
|
+
|
|
347
436
|
guard let cgImage = cgImage else {return}
|
|
348
437
|
let image = UIImage(cgImage: cgImage)
|
|
349
|
-
|
|
438
|
+
|
|
350
439
|
let imageView = newThumbnails[seenIndex - 1].imageView
|
|
351
440
|
UIView.transition(with: imageView, duration: 0.25, options: [.transitionCrossDissolve], animations: {
|
|
352
441
|
imageView.image = image
|
|
@@ -354,34 +443,34 @@ import AVFoundation
|
|
|
354
443
|
}
|
|
355
444
|
}
|
|
356
445
|
}
|
|
357
|
-
|
|
446
|
+
|
|
358
447
|
private func timeForLocation(_ x: CGFloat) -> CMTime {
|
|
359
448
|
let size = bounds.size
|
|
360
449
|
let inset = thumbView.chevronWidth + horizontalInset
|
|
361
450
|
let offset = x - inset
|
|
362
|
-
|
|
451
|
+
|
|
363
452
|
let availableWidth = size.width - inset * 2
|
|
364
453
|
let visibleDurationInSeconds = CGFloat(visibleRange.duration.seconds)
|
|
365
454
|
let ratio = visibleDurationInSeconds != 0 ? availableWidth / visibleDurationInSeconds : 0
|
|
366
|
-
|
|
455
|
+
|
|
367
456
|
let timeDifference = CMTime(seconds: Double(offset / ratio), preferredTimescale: 600)
|
|
368
457
|
return CMTimeAdd(visibleRange.start, timeDifference)
|
|
369
458
|
}
|
|
370
|
-
|
|
459
|
+
|
|
371
460
|
private func locationForTime(_ time: CMTime) -> CGFloat {
|
|
372
461
|
let size = bounds.size
|
|
373
462
|
let inset = thumbView.chevronWidth + horizontalInset
|
|
374
463
|
let availableWidth = size.width - inset * 2
|
|
375
|
-
|
|
464
|
+
|
|
376
465
|
let offset = CMTimeSubtract(time, visibleRange.start)
|
|
377
|
-
|
|
466
|
+
|
|
378
467
|
let visibleDurationInSeconds = CGFloat(visibleRange.duration.seconds)
|
|
379
468
|
let ratio = visibleDurationInSeconds != 0 ? availableWidth / visibleDurationInSeconds : 0
|
|
380
|
-
|
|
469
|
+
|
|
381
470
|
let location = CGFloat(offset.seconds) * ratio
|
|
382
471
|
return SnapToDevicePixels(location) + inset
|
|
383
472
|
}
|
|
384
|
-
|
|
473
|
+
|
|
385
474
|
private func startZoomWaitTimer() {
|
|
386
475
|
stopZoomWaitTimer()
|
|
387
476
|
guard isZoomedIn == false else {return}
|
|
@@ -391,46 +480,46 @@ import AVFoundation
|
|
|
391
480
|
self.zoomIfNeeded()
|
|
392
481
|
})
|
|
393
482
|
}
|
|
394
|
-
|
|
483
|
+
|
|
395
484
|
private func stopZoomWaitTimer() {
|
|
396
485
|
zoomWaitTimer?.invalidate()
|
|
397
486
|
zoomWaitTimer = nil
|
|
398
487
|
}
|
|
399
|
-
|
|
488
|
+
|
|
400
489
|
private func stopZoomIfNeeded() {
|
|
401
490
|
stopZoomWaitTimer()
|
|
402
491
|
isZoomedIn = false
|
|
403
492
|
animateChanges()
|
|
404
493
|
}
|
|
405
|
-
|
|
494
|
+
|
|
406
495
|
private func zoomIfNeeded() {
|
|
407
496
|
guard isZoomedIn == false else {return}
|
|
408
|
-
|
|
497
|
+
|
|
409
498
|
let size = bounds.size
|
|
410
499
|
let inset = thumbView.chevronWidth + horizontalInset
|
|
411
500
|
let availableWidth = size.width - inset * 2
|
|
412
501
|
let newDuration = CGFloat(range.duration.seconds > 4 ? 2.0 : range.duration.seconds * 0.5)
|
|
413
|
-
|
|
502
|
+
|
|
414
503
|
let durationTime = CMTime(seconds: Double(newDuration), preferredTimescale: 600)
|
|
415
|
-
|
|
504
|
+
|
|
416
505
|
if trimmingState == .leading {
|
|
417
506
|
let position = locationForTime(selectedRange.start) - inset
|
|
418
507
|
let start = position / availableWidth * newDuration
|
|
419
508
|
zoomedInRange = CMTimeRange(start: CMTimeSubtract(selectedRange.start, CMTime(seconds: Double(start), preferredTimescale: 600)), duration: durationTime)
|
|
420
509
|
} else {
|
|
421
510
|
let position = locationForTime(selectedRange.end) - inset
|
|
422
|
-
|
|
511
|
+
|
|
423
512
|
let durationToStart = position / availableWidth * newDuration
|
|
424
513
|
let newStart = CMTimeSubtract(selectedRange.end, CMTime(seconds: Double(durationToStart), preferredTimescale: 600))
|
|
425
514
|
zoomedInRange = CMTimeRange(start: newStart, duration: durationTime)
|
|
426
515
|
}
|
|
427
|
-
|
|
516
|
+
|
|
428
517
|
isZoomedIn = true
|
|
429
518
|
animateChanges()
|
|
430
|
-
|
|
519
|
+
|
|
431
520
|
UISelectionFeedbackGenerator().selectionChanged()
|
|
432
521
|
}
|
|
433
|
-
|
|
522
|
+
|
|
434
523
|
private func animateChanges() {
|
|
435
524
|
setNeedsLayout()
|
|
436
525
|
thumbView.setNeedsLayout()
|
|
@@ -439,71 +528,69 @@ import AVFoundation
|
|
|
439
528
|
self.thumbView.layoutIfNeeded()
|
|
440
529
|
})
|
|
441
530
|
}
|
|
442
|
-
|
|
531
|
+
|
|
443
532
|
private func startPanning() {
|
|
444
|
-
sendActions(for: Self.didBeginTrimming)
|
|
445
533
|
UISelectionFeedbackGenerator().selectionChanged()
|
|
446
|
-
|
|
534
|
+
|
|
447
535
|
didClampWhilePanning = false
|
|
448
|
-
|
|
536
|
+
|
|
449
537
|
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
450
538
|
impactFeedbackGenerator?.prepare()
|
|
451
|
-
|
|
539
|
+
|
|
452
540
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
453
541
|
self.updateProgressIndicator()
|
|
454
542
|
})
|
|
455
543
|
}
|
|
456
|
-
|
|
544
|
+
|
|
457
545
|
private func stopPanning() {
|
|
458
546
|
trimmingState = .none
|
|
459
547
|
stopZoomIfNeeded()
|
|
460
548
|
impactFeedbackGenerator = nil
|
|
461
|
-
|
|
462
|
-
|
|
549
|
+
|
|
463
550
|
UIView.animate(withDuration: 0.25, delay: 0, options: [.beginFromCurrentState, .allowUserInteraction], animations: {
|
|
464
551
|
self.updateProgressIndicator()
|
|
465
552
|
})
|
|
466
553
|
}
|
|
467
|
-
|
|
554
|
+
|
|
468
555
|
private func updateProgressIndicator() {
|
|
469
556
|
switch progressIndicatorMode {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
557
|
+
case .alwaysHidden:
|
|
558
|
+
progressIndicator.alpha = 0
|
|
559
|
+
progressIndicatorControl.isUserInteractionEnabled = false
|
|
560
|
+
|
|
561
|
+
case .alwaysShown:
|
|
562
|
+
progressIndicator.alpha = 1
|
|
563
|
+
progressIndicatorControl.isUserInteractionEnabled = true
|
|
564
|
+
setNeedsLayout()
|
|
565
|
+
|
|
566
|
+
case .hiddenOnlyWhenTrimming:
|
|
567
|
+
progressIndicator.alpha = (trimmingState == .none ? 1 : 0)
|
|
568
|
+
progressIndicatorControl.isUserInteractionEnabled = (trimmingState == .none)
|
|
569
|
+
if trimmingState == .none {
|
|
477
570
|
setNeedsLayout()
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
progressIndicatorControl.isUserInteractionEnabled = (trimmingState == .none)
|
|
482
|
-
if trimmingState == .none {
|
|
483
|
-
setNeedsLayout()
|
|
484
|
-
if UIView.inheritedAnimationDuration > 0 {
|
|
485
|
-
UIView.performWithoutAnimation {
|
|
486
|
-
layoutIfNeeded()
|
|
487
|
-
}
|
|
571
|
+
if UIView.inheritedAnimationDuration > 0 {
|
|
572
|
+
UIView.performWithoutAnimation {
|
|
573
|
+
layoutIfNeeded()
|
|
488
574
|
}
|
|
489
575
|
}
|
|
576
|
+
}
|
|
490
577
|
}
|
|
491
578
|
progressIndicatorControl.alpha = progressIndicator.alpha
|
|
492
579
|
}
|
|
493
|
-
|
|
494
|
-
|
|
580
|
+
|
|
581
|
+
|
|
495
582
|
// MARK: - Input
|
|
496
583
|
@objc private func thumbnailPanned(_ sender: UILongPressGestureRecognizer) {
|
|
497
584
|
progressGrabberPanned(sender)
|
|
498
585
|
}
|
|
499
|
-
|
|
500
|
-
|
|
586
|
+
|
|
587
|
+
|
|
501
588
|
@objc private func progressGrabberPanned(_ sender: UILongPressGestureRecognizer) {
|
|
502
|
-
|
|
589
|
+
|
|
503
590
|
func handleChanged() {
|
|
504
591
|
let location = sender.location(in: self)
|
|
505
592
|
var time = timeForLocation(location.x + grabberOffset)
|
|
506
|
-
|
|
593
|
+
|
|
507
594
|
var didClamp = false
|
|
508
595
|
if CMTimeCompare(time, selectedRange.start) == -1 {
|
|
509
596
|
time = selectedRange.start
|
|
@@ -513,232 +600,210 @@ import AVFoundation
|
|
|
513
600
|
time = selectedRange.end
|
|
514
601
|
didClamp = true
|
|
515
602
|
}
|
|
516
|
-
|
|
603
|
+
|
|
517
604
|
if didClamp == true && didClamp != didClampWhilePanning {
|
|
518
605
|
impactFeedbackGenerator?.impactOccurred()
|
|
519
606
|
}
|
|
520
607
|
didClampWhilePanning = didClamp
|
|
521
|
-
|
|
608
|
+
|
|
522
609
|
progress = time
|
|
523
610
|
setNeedsLayout()
|
|
524
611
|
sendActions(for: Self.progressChanged)
|
|
525
612
|
}
|
|
526
613
|
switch sender.state {
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
614
|
+
case .began:
|
|
615
|
+
|
|
616
|
+
UISelectionFeedbackGenerator().selectionChanged()
|
|
617
|
+
impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
|
|
618
|
+
impactFeedbackGenerator?.prepare()
|
|
619
|
+
didClampWhilePanning = false
|
|
620
|
+
|
|
621
|
+
isScrubbing = true
|
|
622
|
+
sendActions(for: Self.didBeginScrubbing)
|
|
623
|
+
handleChanged()
|
|
624
|
+
|
|
625
|
+
case .changed:
|
|
626
|
+
handleChanged()
|
|
627
|
+
|
|
628
|
+
case .ended, .cancelled:
|
|
629
|
+
impactFeedbackGenerator = nil
|
|
630
|
+
|
|
631
|
+
isScrubbing = false
|
|
632
|
+
sendActions(for: Self.didEndScrubbing)
|
|
633
|
+
|
|
634
|
+
case .possible, .failed:
|
|
635
|
+
break
|
|
636
|
+
|
|
637
|
+
@unknown default:
|
|
638
|
+
break
|
|
552
639
|
}
|
|
553
640
|
}
|
|
554
|
-
|
|
555
|
-
|
|
641
|
+
|
|
642
|
+
|
|
556
643
|
@objc private func leadingGrabberPanned(_ sender: UILongPressGestureRecognizer) {
|
|
557
644
|
switch sender.state {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
startPanning()
|
|
563
|
-
|
|
564
|
-
case .changed:
|
|
565
|
-
let location = sender.location(in: self)
|
|
566
|
-
var time = timeForLocation(location.x + grabberOffset)
|
|
567
|
-
let newDuration = CMTimeSubtract(selectedRange.end, time)
|
|
568
|
-
let newRange = CMTimeRange(start: time, end: selectedRange.end)
|
|
569
|
-
|
|
570
|
-
if CMTimeCompare(newRange.duration, maximumDuration) == 1 {
|
|
571
|
-
return
|
|
572
|
-
}
|
|
645
|
+
case .began:
|
|
646
|
+
trimmingState = .leading
|
|
647
|
+
grabberOffset = thumbView.chevronWidth - sender.location(in: thumbView.leadingGrabber).x
|
|
573
648
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
649
|
+
startPanning()
|
|
650
|
+
sendActions(for: Self.didBeginTrimmingFromStart)
|
|
651
|
+
|
|
652
|
+
case .changed:
|
|
653
|
+
let location = sender.location(in: self)
|
|
654
|
+
let current = timeForLocation(location.x + grabberOffset)
|
|
655
|
+
let min = CMTimeSubtract(selectedRange.end, minimumDuration)
|
|
656
|
+
|
|
657
|
+
var didClamp = false
|
|
658
|
+
var newRange = CMTimeRange(start: current, end: selectedRange.end)
|
|
659
|
+
|
|
660
|
+
if CMTimeCompare(current, min) != -1 {
|
|
661
|
+
newRange = CMTimeRange(start: min, end: selectedRange.end)
|
|
662
|
+
didClamp = true
|
|
663
|
+
} else if CMTimeCompare(newRange.duration, maximumDuration) != -1 {
|
|
664
|
+
let time = CMTimeSubtract(selectedRange.end, maximumDuration)
|
|
665
|
+
newRange = CMTimeRange(start: time, end: selectedRange.end)
|
|
666
|
+
didClamp = true
|
|
667
|
+
} else if CMTimeCompare(newRange.start, range.start) != 1 {
|
|
668
|
+
// prevent startTime to be smaller than video startTime
|
|
669
|
+
newRange = CMTimeRange(start: range.start, end: selectedRange.end)
|
|
670
|
+
didClamp = true
|
|
671
|
+
} else if CMTimeCompare(newRange.duration, minimumDuration) != 1 {
|
|
672
|
+
newRange = CMTimeRange(start: min, end: selectedRange.end)
|
|
673
|
+
didClamp = true
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if didClamp == true && didClamp != didClampWhilePanning {
|
|
677
|
+
impactFeedbackGenerator?.impactOccurred()
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
didClampWhilePanning = didClamp
|
|
681
|
+
selectedRange = newRange
|
|
682
|
+
sendActions(for: Self.leadingGrabberChanged)
|
|
683
|
+
setNeedsLayout()
|
|
684
|
+
|
|
685
|
+
startZoomWaitTimer()
|
|
686
|
+
|
|
687
|
+
case .ended:
|
|
688
|
+
stopPanning()
|
|
689
|
+
sendActions(for: Self.didEndTrimmingFromStart)
|
|
690
|
+
|
|
691
|
+
case .cancelled:
|
|
692
|
+
stopPanning()
|
|
693
|
+
|
|
694
|
+
case .possible, .failed:
|
|
695
|
+
break
|
|
696
|
+
|
|
697
|
+
@unknown default:
|
|
698
|
+
break
|
|
621
699
|
}
|
|
622
700
|
}
|
|
623
|
-
|
|
701
|
+
|
|
624
702
|
@objc private func trailingGrabberPanned(_ sender: UILongPressGestureRecognizer) {
|
|
625
703
|
switch sender.state {
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
startPanning()
|
|
631
|
-
|
|
632
|
-
case .changed:
|
|
633
|
-
let location = sender.location(in: self)
|
|
634
|
-
var time = timeForLocation(location.x - grabberOffset)
|
|
635
|
-
let newDuration = CMTimeSubtract(time, selectedRange.start)
|
|
636
|
-
let newRange = CMTimeRange(start: selectedRange.start, end: time)
|
|
637
|
-
|
|
638
|
-
if CMTimeCompare(newRange.duration, maximumDuration) == 1 {
|
|
639
|
-
return
|
|
640
|
-
}
|
|
704
|
+
case .began:
|
|
705
|
+
trimmingState = .trailing
|
|
706
|
+
grabberOffset = sender.location(in: thumbView.trailingGrabber).x
|
|
641
707
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
708
|
+
startPanning()
|
|
709
|
+
sendActions(for: Self.didBeginTrimmingFromEnd)
|
|
710
|
+
|
|
711
|
+
case .changed:
|
|
712
|
+
let location = sender.location(in: self)
|
|
713
|
+
|
|
714
|
+
let current = timeForLocation(location.x - grabberOffset)
|
|
715
|
+
let min = CMTimeAdd(selectedRange.start, minimumDuration)
|
|
716
|
+
|
|
717
|
+
var didClamp = false
|
|
718
|
+
var newRange = CMTimeRange(start: selectedRange.start, end: timeForLocation(location.x - grabberOffset))
|
|
719
|
+
|
|
720
|
+
if CMTimeCompare(current, min) == -1 {
|
|
721
|
+
newRange = CMTimeRange(start: selectedRange.start, end: min)
|
|
722
|
+
didClamp = true
|
|
723
|
+
} else if CMTimeCompare(newRange.duration, maximumDuration) != -1 {
|
|
724
|
+
let time = CMTimeAdd(selectedRange.start, maximumDuration)
|
|
725
|
+
newRange = CMTimeRange(start: selectedRange.start, end: time)
|
|
726
|
+
didClamp = true
|
|
727
|
+
} else if CMTimeCompare(newRange.end, range.end) != -1 {
|
|
728
|
+
// prevent endTime to be greater than video endTime
|
|
729
|
+
newRange = CMTimeRange(start: selectedRange.start, end: range.end)
|
|
730
|
+
didClamp = true
|
|
731
|
+
} else if CMTimeCompare(newRange.duration, minimumDuration) != 1 {
|
|
732
|
+
newRange = CMTimeRange(start: selectedRange.start, end: min)
|
|
733
|
+
didClamp = true
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if didClamp == true && didClamp != didClampWhilePanning {
|
|
737
|
+
impactFeedbackGenerator?.impactOccurred()
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
didClampWhilePanning = didClamp
|
|
741
|
+
selectedRange = newRange
|
|
742
|
+
sendActions(for: Self.trailingGrabberChanged)
|
|
743
|
+
setNeedsLayout()
|
|
744
|
+
|
|
745
|
+
startZoomWaitTimer()
|
|
746
|
+
|
|
747
|
+
case .ended:
|
|
748
|
+
stopPanning()
|
|
749
|
+
sendActions(for: Self.didEndTrimmingFromEnd)
|
|
750
|
+
|
|
751
|
+
case .cancelled:
|
|
752
|
+
stopPanning()
|
|
753
|
+
|
|
754
|
+
case .possible, .failed:
|
|
755
|
+
break
|
|
756
|
+
|
|
757
|
+
@unknown default:
|
|
758
|
+
break
|
|
688
759
|
}
|
|
689
760
|
}
|
|
690
|
-
|
|
761
|
+
|
|
691
762
|
// MARK: - UIView
|
|
692
|
-
|
|
763
|
+
|
|
693
764
|
override var intrinsicContentSize: CGSize {
|
|
694
765
|
return CGSize(width: UIView.noIntrinsicMetric, height: 50)
|
|
695
766
|
}
|
|
696
|
-
|
|
767
|
+
|
|
697
768
|
override func layoutSubviews() {
|
|
698
769
|
super.layoutSubviews()
|
|
699
|
-
|
|
770
|
+
|
|
700
771
|
let size = bounds.size
|
|
701
772
|
let inset = thumbView.chevronWidth
|
|
702
773
|
var left = locationForTime(selectedRange.start) - inset
|
|
703
774
|
var right = locationForTime(selectedRange.end) + inset
|
|
704
|
-
|
|
775
|
+
|
|
705
776
|
if right > bounds.width {
|
|
706
777
|
right = bounds.width + inset * 2
|
|
707
778
|
}
|
|
708
|
-
|
|
779
|
+
|
|
709
780
|
if left < 0 {
|
|
710
781
|
left = -inset
|
|
711
782
|
}
|
|
712
|
-
|
|
783
|
+
|
|
713
784
|
let rect = CGRect(origin: .zero, size: size)
|
|
714
|
-
shadowView.frame = rect
|
|
715
|
-
wrapperView.frame = rect
|
|
716
785
|
thumbView.frame = CGRect(x: left, y: 0, width: max(right - left, inset * 2), height: size.height)
|
|
717
|
-
|
|
786
|
+
|
|
718
787
|
let isZoomedToEnd = (trimmingState == .leading && isZoomedIn == true)
|
|
719
|
-
|
|
788
|
+
|
|
720
789
|
let thumbnailOffset = (isZoomedIn == true ? horizontalInset + inset + 6 : 0)
|
|
721
790
|
let coverOffset = thumbnailOffset - horizontalInset
|
|
722
791
|
let coverStartOffset = (isZoomedIn == false ? inset : 0)
|
|
723
|
-
|
|
792
|
+
|
|
724
793
|
let thumbnailRect = rect.insetBy(dx: horizontalInset - thumbnailOffset, dy: thumbView.edgeHeight)
|
|
725
|
-
thumbnailClipView.frame = rect
|
|
726
794
|
thumbnailWrapperView.frame = thumbnailRect
|
|
727
795
|
thumbnailTrackView.frame = CGRect(origin: .zero, size: CGSize(width: thumbnailRect.width - (isZoomedToEnd == false ? inset : 0), height: thumbnailRect.height))
|
|
728
796
|
thumbnailLeadingCoverView.frame = CGRect(x: coverStartOffset, y: 0, width: left + inset * 0.5 + coverOffset - coverStartOffset, height: thumbnailRect.height)
|
|
729
797
|
thumbnailTrailingCoverView.frame = CGRect(x: right - inset * 0.5 + coverOffset, y: 0, width: thumbnailRect.width - coverStartOffset - (right - inset * 0.5 + coverOffset), height: thumbnailRect.height)
|
|
730
|
-
|
|
731
|
-
leadingThumbRest.frame = CGRect(x: 0, y: 0, width: inset, height: thumbnailRect.height)
|
|
732
|
-
trailingThumbRest.frame = CGRect(x: thumbnailRect.width - inset, y: 0, width: inset, height: thumbnailRect.height)
|
|
733
|
-
|
|
798
|
+
|
|
734
799
|
if progressIndicator.alpha > 0 {
|
|
735
800
|
let progressWidth = CGFloat(4)
|
|
736
801
|
let progressIndicatorOffset = locationForTime(progress)
|
|
737
802
|
let progressLeft = min(max(thumbView.frame.minX + inset, progressIndicatorOffset - progressWidth * 0.5), thumbView.frame.maxX - inset - progressWidth)
|
|
738
803
|
progressIndicator.frame = CGRect(x: progressLeft, y: thumbnailRect.minY, width: progressWidth, height: thumbnailRect.height)
|
|
739
|
-
|
|
804
|
+
|
|
740
805
|
let progressControlWidth = CGFloat(24)
|
|
741
|
-
|
|
806
|
+
|
|
742
807
|
var progressControlLeft = max(thumbView.frame.minX + inset, progressLeft)
|
|
743
808
|
var progressControlRight = progressLeft + progressControlWidth
|
|
744
809
|
if progressControlRight > thumbView.frame.maxX - inset {
|
|
@@ -747,9 +812,9 @@ import AVFoundation
|
|
|
747
812
|
}
|
|
748
813
|
progressIndicatorControl.frame = CGRect(x: progressControlLeft, y: thumbnailRect.minY, width: progressControlRight - progressControlLeft, height: thumbnailRect.height)
|
|
749
814
|
}
|
|
750
|
-
|
|
815
|
+
|
|
751
816
|
regenerateThumbnailsIfNeeded()
|
|
752
|
-
|
|
817
|
+
|
|
753
818
|
for thumbnail in thumbnails {
|
|
754
819
|
let position = locationForTime(thumbnail.time) - horizontalInset + thumbnailOffset
|
|
755
820
|
let frame = CGRect(x: position, y: 0, width: thumbnailSize.width, height: thumbnailSize.height)
|
|
@@ -762,12 +827,12 @@ import AVFoundation
|
|
|
762
827
|
}
|
|
763
828
|
}
|
|
764
829
|
}
|
|
765
|
-
|
|
830
|
+
|
|
766
831
|
override init(frame: CGRect) {
|
|
767
832
|
super.init(frame: frame)
|
|
768
833
|
setup()
|
|
769
834
|
}
|
|
770
|
-
|
|
835
|
+
|
|
771
836
|
required init?(coder: NSCoder) {
|
|
772
837
|
super.init(coder: coder)
|
|
773
838
|
setup()
|
|
@@ -795,14 +860,6 @@ fileprivate extension CGRect {
|
|
|
795
860
|
}
|
|
796
861
|
|
|
797
862
|
fileprivate extension CGSize {
|
|
798
|
-
func ceiled() -> CGSize {
|
|
799
|
-
return CGSize(width: ceil(width), height: ceil((height)))
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
func snappedToDevicePixels(scale: CGFloat? = nil) -> CGSize {
|
|
803
|
-
return CGSize(width: SnapToDevicePixels(width, scale: scale), height: SnapToDevicePixels(height, scale: scale))
|
|
804
|
-
}
|
|
805
|
-
|
|
806
863
|
func applyingVideoTransform(_ transform: CGAffineTransform) -> CGSize {
|
|
807
864
|
return CGRect(origin: .zero, size: self).applying(transform).size
|
|
808
865
|
}
|