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