react-native-video-trim 1.0.21 → 1.0.23

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.
@@ -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 didBeginTrimming = UIControl.Event(rawValue: 0b00000001 << 24)
17
- static let selectedRangeChanged = UIControl.Event(rawValue: 0b00000010 << 24)
18
- static let didEndTrimming = UIControl.Event(rawValue: 0b00000100 << 24)
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
- let thumbView = VideoTrimmerThumb()
32
- private let wrapperView = UIView()
33
- private let shadowView = UIView()
34
- private let thumbnailClipView = UIView()
35
- private let thumbnailWrapperView = UIView()
36
- private let thumbnailTrackView = UIView()
37
- private let thumbnailLeadingCoverView = UIView()
38
- private let thumbnailTrailingCoverView = UIView()
39
- private let leadingThumbRest = UIView()
40
- private let trailingThumbRest = UIView()
41
- private let progressIndicator = UIView()
42
- private let progressIndicatorControl = UIControl()
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
- case .none: return .zero
187
- case .leading: return selectedRange.start
188
- case .trailing: return selectedRange.end
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
- sendActions(for: Self.didEndTrimming)
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
- case .alwaysHidden:
471
- progressIndicator.alpha = 0
472
- progressIndicatorControl.isUserInteractionEnabled = false
473
-
474
- case .alwaysShown:
475
- progressIndicator.alpha = 1
476
- progressIndicatorControl.isUserInteractionEnabled = true
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
- case .hiddenOnlyWhenTrimming:
480
- progressIndicator.alpha = (trimmingState == .none ? 1 : 0)
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
- case .began:
528
-
529
- UISelectionFeedbackGenerator().selectionChanged()
530
- impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy)
531
- impactFeedbackGenerator?.prepare()
532
- didClampWhilePanning = false
533
-
534
- isScrubbing = true
535
- sendActions(for: Self.didBeginScrubbing)
536
- handleChanged()
537
-
538
- case .changed:
539
- handleChanged()
540
-
541
- case .ended, .cancelled:
542
- impactFeedbackGenerator = nil
543
-
544
- isScrubbing = false
545
- sendActions(for: Self.didEndScrubbing)
546
-
547
- case .possible, .failed:
548
- break
549
-
550
- @unknown default:
551
- break
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
- case .began:
559
- trimmingState = .leading
560
- grabberOffset = thumbView.chevronWidth - sender.location(in: thumbView.leadingGrabber).x
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
- if CMTimeCompare(newRange.start, .zero) == -1 { // prevent start time to be less than 0
575
- return
576
- }
577
-
578
- var didClamp = false
579
- if CMTimeCompare(newDuration, minimumDuration) == -1 {
580
- // time = CMTimeSubtract(selectedRange.end, minimumDuration)
581
- // didClamp = true
582
- return
583
- }
584
- if CMTimeCompare(time, range.start) == -1 {
585
- time = range.start
586
- didClamp = true
587
- }
588
- if CMTimeCompare(time, range.end) == 1 {
589
- time = range.end
590
- didClamp = true
591
- }
592
-
593
-
594
- if didClamp == true && didClamp != didClampWhilePanning {
595
- impactFeedbackGenerator?.impactOccurred()
596
- } else {
597
- // if didClamp == false && CMTimeCompare(progress, time) == 0 && CMTimeCompare(progress, range.start) != 0 && CMTimeCompare(progress, range.end) != 0 {
598
- // impactFeedbackGenerator?.impactOccurred(intensity: 0.5)
599
- // didClamp = true
600
- // }
601
- }
602
- didClampWhilePanning = didClamp
603
-
604
- selectedRange = newRange
605
- sendActions(for: Self.selectedRangeChanged)
606
- setNeedsLayout()
607
-
608
- startZoomWaitTimer()
609
-
610
- case .ended:
611
- stopPanning()
612
-
613
- case .cancelled:
614
- stopPanning()
615
-
616
- case .possible, .failed:
617
- break
618
-
619
- @unknown default:
620
- break
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
- case .began:
627
- trimmingState = .trailing
628
- grabberOffset = sender.location(in: thumbView.trailingGrabber).x
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
- if CMTimeCompare(newRange.end, range.duration) == 1 { // prevent endTime to be greater than video endTime
643
- return
644
- }
645
-
646
- var didClamp = false
647
- if CMTimeCompare(newDuration, minimumDuration) == -1 {
648
- // time = CMTimeAdd(selectedRange.start, minimumDuration)
649
- // didClamp = true
650
- return
651
- }
652
- if CMTimeCompare(time, range.start) == -1 {
653
- time = range.start
654
- didClamp = true
655
- }
656
- if CMTimeCompare(time, range.end) == 1 {
657
- time = range.end
658
- didClamp = true
659
- }
660
-
661
- if didClamp == true && didClamp != didClampWhilePanning {
662
- impactFeedbackGenerator?.impactOccurred()
663
- } else {
664
- // if didClamp == false && CMTimeCompare(progress, time) == 0 && CMTimeCompare(progress, range.start) != 0 && CMTimeCompare(progress, range.end) != 0 {
665
- // impactFeedbackGenerator?.impactOccurred(intensity: 0.5)
666
- // didClamp = true
667
- // }
668
- }
669
- didClampWhilePanning = didClamp
670
-
671
- selectedRange = newRange
672
- sendActions(for: Self.selectedRangeChanged)
673
- setNeedsLayout()
674
-
675
- startZoomWaitTimer()
676
-
677
- case .ended:
678
- stopPanning()
679
-
680
- case .cancelled:
681
- stopPanning()
682
-
683
- case .possible, .failed:
684
- break
685
-
686
- @unknown default:
687
- break
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
  }