├── VideoTimelineView
├── movie.mov
├── VideoTimelineView
│ ├── Assets.xcassets
│ │ ├── Contents.json
│ │ └── AppIcon.appiconset
│ │ │ └── Contents.json
│ ├── AppDelegate.swift
│ ├── Base.lproj
│ │ ├── Main.storyboard
│ │ └── LaunchScreen.storyboard
│ ├── Info.plist
│ ├── SceneDelegate.swift
│ ├── ViewController.swift
│ └── VideoTimelineView
│ │ ├── CenterLine.swift
│ │ ├── TimelineScroller.swift
│ │ ├── TimelineMeasure.swift
│ │ ├── VideoTimelineView.swift
│ │ ├── FrameImagesView.swift
│ │ ├── TimelineView.swift
│ │ └── TrimView.swift
├── VideoTimelineView.xcodeproj
│ ├── xcuserdata
│ │ └── tom.xcuserdatad
│ │ │ ├── xcdebugger
│ │ │ └── Breakpoints_v2.xcbkptlist
│ │ │ └── xcschemes
│ │ │ └── xcschememanagement.plist
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ ├── xcuserdata
│ │ │ └── tom.xcuserdatad
│ │ │ │ └── UserInterfaceState.xcuserstate
│ │ └── xcshareddata
│ │ │ └── IDEWorkspaceChecks.plist
│ └── project.pbxproj
├── VideoTimelineViewTests
│ ├── Info.plist
│ └── VideoTimelineViewTests.swift
└── VideoTimelineViewUITests
│ ├── Info.plist
│ └── VideoTimelineViewUITests.swift
└── README.md
/VideoTimelineView/movie.mov:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tomohiro-Yamashita/VideoTimelineView/HEAD/VideoTimelineView/movie.mov
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "version" : 1,
4 | "author" : "xcode"
5 | }
6 | }
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView.xcodeproj/xcuserdata/tom.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView.xcodeproj/project.xcworkspace/xcuserdata/tom.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Tomohiro-Yamashita/VideoTimelineView/HEAD/VideoTimelineView/VideoTimelineView.xcodeproj/project.xcworkspace/xcuserdata/tom.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView.xcodeproj/xcuserdata/tom.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | VideoTimelineView.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineViewTests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineViewUITests/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 |
22 |
23 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineViewTests/VideoTimelineViewTests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoTimelineViewTests.swift
3 | // VideoTimelineViewTests
4 | //
5 | // Created by Tom on 2020/03/27.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import XCTest
10 | @testable import VideoTimelineView
11 |
12 | class VideoTimelineViewTests: XCTestCase {
13 |
14 | override func setUp() {
15 | // Put setup code here. This method is called before the invocation of each test method in the class.
16 | }
17 |
18 | override func tearDown() {
19 | // Put teardown code here. This method is called after the invocation of each test method in the class.
20 | }
21 |
22 | func testExample() {
23 | // This is an example of a functional test case.
24 | // Use XCTAssert and related functions to verify your tests produce the correct results.
25 | }
26 |
27 | func testPerformanceExample() {
28 | // This is an example of a performance test case.
29 | self.measure {
30 | // Put the code you want to measure the time of here.
31 | }
32 | }
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppDelegate.swift
3 | // VideoTimelineView
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/27.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | @UIApplicationMain
12 | class AppDelegate: UIResponder, UIApplicationDelegate {
13 |
14 |
15 |
16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
17 | // Override point for customization after application launch.
18 | return true
19 | }
20 |
21 | // MARK: UISceneSession Lifecycle
22 |
23 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
24 | // Called when a new scene session is being created.
25 | // Use this method to select a configuration to create the new scene with.
26 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
27 | }
28 |
29 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) {
30 | // Called when the user discards a scene session.
31 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
32 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return.
33 | }
34 |
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineViewUITests/VideoTimelineViewUITests.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoTimelineViewUITests.swift
3 | // VideoTimelineViewUITests
4 | //
5 | // Created by Tom on 2020/03/27.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import XCTest
10 |
11 | class VideoTimelineViewUITests: XCTestCase {
12 |
13 | override func setUp() {
14 | // Put setup code here. This method is called before the invocation of each test method in the class.
15 |
16 | // In UI tests it is usually best to stop immediately when a failure occurs.
17 | continueAfterFailure = false
18 |
19 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
20 | }
21 |
22 | override func tearDown() {
23 | // Put teardown code here. This method is called after the invocation of each test method in the class.
24 | }
25 |
26 | func testExample() {
27 | // UI tests must launch the application that they test.
28 | let app = XCUIApplication()
29 | app.launch()
30 |
31 | // Use recording to get started writing UI tests.
32 | // Use XCTAssert and related functions to verify your tests produce the correct results.
33 | }
34 |
35 | func testLaunchPerformance() {
36 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) {
37 | // This measures how long it takes to launch your application.
38 | measure(metrics: [XCTOSSignpostMetric.applicationLaunch]) {
39 | XCUIApplication().launch()
40 | }
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "size" : "20x20",
6 | "scale" : "2x"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "size" : "20x20",
11 | "scale" : "3x"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "size" : "29x29",
16 | "scale" : "2x"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "size" : "29x29",
21 | "scale" : "3x"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "size" : "40x40",
26 | "scale" : "2x"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "size" : "40x40",
31 | "scale" : "3x"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "size" : "60x60",
36 | "scale" : "2x"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "size" : "60x60",
41 | "scale" : "3x"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "size" : "20x20",
46 | "scale" : "1x"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "size" : "20x20",
51 | "scale" : "2x"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "size" : "29x29",
56 | "scale" : "1x"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "size" : "29x29",
61 | "scale" : "2x"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "size" : "40x40",
66 | "scale" : "1x"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "size" : "40x40",
71 | "scale" : "2x"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "size" : "76x76",
76 | "scale" : "1x"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "size" : "76x76",
81 | "scale" : "2x"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "size" : "83.5x83.5",
86 | "scale" : "2x"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "size" : "1024x1024",
91 | "scale" : "1x"
92 | }
93 | ],
94 | "info" : {
95 | "version" : 1,
96 | "author" : "xcode"
97 | }
98 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VideoTimelineView
2 | Video timeline UI for iOS Apps
3 | - Zoom in/out with pinch
4 | - Scrub with sound
5 | - Repeat playing in the trimmer
6 |
7 | 
8 |
9 |
10 | ## Usage
11 | Copy the VideoTimelineView folder in this project to yours
12 |
13 |
14 | - To setup
15 | ```
16 | let videoTimelineView = VideoTimelineView()
17 | videoTimelineView.frame = timelineRect
18 | videoTimelineView.new(asset:AVAsset(url:videoURL))
19 | view.addSubview(videoTimelineView)
20 | ```
21 |
22 |
23 | - To get actions from VideoTimelineView
24 | Add TimelinePlayStatusReceiver protocol in your ViewController
25 | ```
26 | class ViewController: UIViewController, TimelinePlayStatusReceiver {
27 | ```
28 | And set viewController as receiver
29 | ```
30 | videoTimelineView.playStatusReceiver = self
31 | ```
32 |
33 | - Get actions
34 | Implement these functions in your viewController
35 | ```
36 | func videoTimelineStopped()
37 | func videoTimelineMoved()
38 | func videoTimelineTrimChanged()
39 | ```
40 |
41 | To get values of the trimmer
42 | ```
43 | let trim = videoTimelineView.currentTrim()
44 | print("start time: \(trim.start)")
45 | print("end time: \(trim.end)")
46 | ```
47 |
48 |
49 | - To control
50 | ```
51 | //Repeat in the trimmer
52 | videoTimelineView.repeatOn = true
53 |
54 | //If set in false, the trimmer will be ignored
55 | videoTimelineView.setTrimIsEnabled(true)
56 |
57 | //Hide trimmer
58 | videoTimelineView.setTrimmerIsHidden(true)
59 |
60 | //Go to 0s with animation
61 | videoTimelineView.moveTo(0, animate:true)
62 |
63 | //Set trimmer from 5 to 10 with animation and move to 3
64 | videoTimelineView.setTrim(start:5, end:10, seek:3, animate:true)
65 | ```
66 | ## Example Product
67 | The app with VideoTimeLlineView [on the AppStore](https://apps.apple.com/us/app/examplay/id1509013277?ls=1)(Free).
68 |
69 | ## License
70 | [MIT](https://choosealicense.com/licenses/mit/)
71 |
72 | ## Contact
73 | [E-mail](tomo_dev@sockettv.org), [twitter](https://twitter.com/DevYamashita), [Facebook](https://www.facebook.com/TomohiroYamashitaApps/)
74 |
75 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | $(EXECUTABLE_NAME)
9 | CFBundleIdentifier
10 | $(PRODUCT_BUNDLE_IDENTIFIER)
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | $(PRODUCT_NAME)
15 | CFBundlePackageType
16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleVersion
20 | 1
21 | LSRequiresIPhoneOS
22 |
23 | UIApplicationSceneManifest
24 |
25 | UIApplicationSupportsMultipleScenes
26 |
27 | UISceneConfigurations
28 |
29 | UIWindowSceneSessionRoleApplication
30 |
31 |
32 | UISceneConfigurationName
33 | Default Configuration
34 | UISceneDelegateClassName
35 | $(PRODUCT_MODULE_NAME).SceneDelegate
36 | UISceneStoryboardFile
37 | Main
38 |
39 |
40 |
41 |
42 | UILaunchStoryboardName
43 | LaunchScreen
44 | UIMainStoryboardFile
45 | Main
46 | UIRequiredDeviceCapabilities
47 |
48 | armv7
49 |
50 | UISupportedInterfaceOrientations
51 |
52 | UIInterfaceOrientationPortrait
53 | UIInterfaceOrientationLandscapeLeft
54 | UIInterfaceOrientationLandscapeRight
55 |
56 | UISupportedInterfaceOrientations~ipad
57 |
58 | UIInterfaceOrientationPortrait
59 | UIInterfaceOrientationPortraitUpsideDown
60 | UIInterfaceOrientationLandscapeLeft
61 | UIInterfaceOrientationLandscapeRight
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/SceneDelegate.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SceneDelegate.swift
3 | // VideoTimelineView
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/27.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate {
12 |
13 | var window: UIWindow?
14 |
15 |
16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
20 | guard let _ = (scene as? UIWindowScene) else { return }
21 | }
22 |
23 | func sceneDidDisconnect(_ scene: UIScene) {
24 | // Called as the scene is being released by the system.
25 | // This occurs shortly after the scene enters the background, or when its session is discarded.
26 | // Release any resources associated with this scene that can be re-created the next time the scene connects.
27 | // The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
28 | }
29 |
30 | func sceneDidBecomeActive(_ scene: UIScene) {
31 | // Called when the scene has moved from an inactive state to an active state.
32 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
33 | }
34 |
35 | func sceneWillResignActive(_ scene: UIScene) {
36 | // Called when the scene will move from an active state to an inactive state.
37 | // This may occur due to temporary interruptions (ex. an incoming phone call).
38 | }
39 |
40 | func sceneWillEnterForeground(_ scene: UIScene) {
41 | // Called as the scene transitions from the background to the foreground.
42 | // Use this method to undo the changes made on entering the background.
43 | }
44 |
45 | func sceneDidEnterBackground(_ scene: UIScene) {
46 | // Called as the scene transitions from the foreground to the background.
47 | // Use this method to save data, release shared resources, and store enough scene-specific state information
48 | // to restore the scene back to its current state.
49 | }
50 |
51 |
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/ViewController.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ViewController.swift
3 | // VideoTimelineView
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/27.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | class ViewController: UIViewController, TimelinePlayStatusReceiver {
13 |
14 | var videoTimelineView:VideoTimelineView!
15 | let playerView = UIView()
16 | var playerLayer:AVPlayerLayer!
17 | let playButton = UIButton()
18 |
19 |
20 | override func viewDidLoad() {
21 |
22 | super.viewDidLoad()
23 |
24 |
25 |
26 | ///Prepare videoTimelineView
27 | let asset = AVAsset(url: URL(fileURLWithPath: Bundle.main.path(forResource: "movie", ofType:"mov")!))
28 |
29 | videoTimelineView = VideoTimelineView()
30 | videoTimelineView.frame = layout().timeline
31 | videoTimelineView.new(asset:asset)
32 | videoTimelineView.playStatusReceiver = self
33 |
34 | videoTimelineView.repeatOn = true
35 | videoTimelineView.setTrimIsEnabled(true)
36 | videoTimelineView.setTrimmerIsHidden(false)
37 | view.addSubview(videoTimelineView)
38 |
39 | videoTimelineView.moveTo(0, animate:false)
40 | videoTimelineView.setTrim(start:5, end:10, seek:nil, animate:false)
41 |
42 |
43 |
44 | ///Prepare playerView
45 | let player = videoTimelineView.player!//You can also set another player like below
46 | //let player = AVPlayer(playerItem: AVPlayerItem(asset: asset))
47 | //videoTimelineView.player = player
48 |
49 | let playerFrame = layout().player
50 | playerLayer = AVPlayerLayer(player: player)
51 | playerLayer.frame.size = playerFrame.size
52 | playerLayer.videoGravity = AVLayerVideoGravity.resizeAspect
53 | player.actionAtItemEnd = AVPlayer.ActionAtItemEnd.none
54 |
55 | playerView.frame = playerFrame
56 | playerView.layer.addSublayer(playerLayer)
57 | view.addSubview(playerView)
58 |
59 |
60 |
61 |
62 | ///Prepare playButton
63 | playButton.frame = layout().button
64 | playButton.addTarget(self,action:#selector(self.playButtonAction), for:.touchUpInside)
65 | setPlayButtonImage()
66 | view.addSubview(playButton)
67 |
68 | }
69 |
70 | override func viewDidLayoutSubviews() {
71 | videoTimelineView.frame = layout().timeline
72 | playerView.frame = layout().player
73 | playerLayer.frame.size = playerView.frame.size
74 | playButton.frame = layout().button
75 | videoTimelineView.viewDidLayoutSubviews()
76 | }
77 |
78 | func layout() -> (timeline:CGRect, player:CGRect, button:CGRect) {
79 | let timeline = CGRect(x: 0,y:view.frame.size.height * 0.6, width:view.frame.size.width, height:view.frame.size.height / 6)
80 | let player = CGRect(x:0, y:40, width:view.frame.size.width, height:view.frame.size.height * 0.4)
81 | let button = CGRect(x:(view.frame.size.width - 60) / 2, y:view.frame.size.height - 60, width:60, height:60)
82 | return (timeline, player, button)
83 | }
84 |
85 | var playButtonStatus:Bool = false
86 | @objc func playButtonAction() {
87 | playButtonStatus = !playButtonStatus
88 | if playButtonStatus {
89 | videoTimelineView.play()
90 | } else {
91 | videoTimelineView.stop()
92 | }
93 | setPlayButtonImage()
94 | }
95 |
96 | func setPlayButtonImage() {
97 | if playButtonStatus {
98 | self.playButton.setImage(UIImage(systemName: "pause.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .light, scale: .medium)), for: .normal)
99 | } else {
100 | self.playButton.setImage(UIImage(systemName: "play.fill", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .light, scale: .medium)), for: .normal)
101 | }
102 | }
103 |
104 | func videoTimelineStopped() {
105 | playButtonStatus = false
106 | setPlayButtonImage()
107 | }
108 |
109 | func videoTimelineMoved() {
110 | let time = videoTimelineView.currentTime
111 | print("time: \(time)")
112 | }
113 |
114 | func videoTimelineTrimChanged() {
115 | let trim = videoTimelineView.currentTrim()
116 | print("start time: \(trim.start)")
117 | print("end time: \(trim.end)")
118 | }
119 |
120 |
121 | }
122 |
123 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/CenterLine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CenterLine.swift
3 | // Examplay
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/09.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | class CenterLine: UIView {
13 |
14 | var mainView:VideoTimelineView!
15 |
16 | let timeLabel = UILabel()
17 | var parentView:TimelineView? = nil
18 | var duration:Float64 = 0
19 | var currentTime:Float64 = 0
20 | let margin:CGFloat = 6
21 |
22 | override init (frame: CGRect) {
23 | super.init(frame: frame)
24 |
25 | self.backgroundColor = .clear
26 | self.isUserInteractionEnabled = false
27 | }
28 |
29 | required init(coder aDecoder: NSCoder) {
30 | fatalError("CenterLine init(coder:) has not been implemented")
31 | }
32 |
33 | func configure(parent:TimelineView) {
34 | parentView = parent
35 | self.setNeedsDisplay()
36 |
37 | self.timeLabel.adjustsFontSizeToFitWidth = true
38 | self.timeLabel.textAlignment = .center
39 |
40 | self.timeLabel.text = String("00:00.00")
41 |
42 |
43 | self.addSubview(timeLabel)
44 | }
45 |
46 | func update() {
47 | self.setNeedsDisplay()
48 | let textMargin = margin + 3
49 | self.timeLabel.frame.size.width = self.bounds.size.width - textMargin * 2
50 | self.timeLabel.frame.origin = CGPoint(x:textMargin,y:0)
51 | self.timeLabel.textColor = UIColor(hue: 0.94, saturation:0.68, brightness:0.95, alpha: 0.94)
52 | setTimeText()
53 | timeLabel.font = UIFont(name:"HelveticaNeue-CondensedBold" ,size:timeLabel.frame.size.height * 0.9)
54 |
55 | }
56 |
57 |
58 | override func draw(_ rect: CGRect) {
59 |
60 |
61 | gradient()
62 |
63 | let context = UIGraphicsGetCurrentContext()!
64 | context.saveGState()
65 | context.setShadow(offset: CGSize(width: 0,height: 0), blur: 6, color: UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.30).cgColor)
66 |
67 | UIColor(hue: 0, saturation:0, brightness:1, alpha: 0.92).setFill()
68 |
69 | let labelRect = CGRect(x: margin,y: 0.3,width: self.frame.size.width - margin * 2,height: timeLabel.frame.size.height + 1)
70 | let rectPath = UIBezierPath(roundedRect:labelRect, cornerRadius:timeLabel.frame.size.height)
71 | rectPath.fill()
72 | context.restoreGState()
73 |
74 |
75 |
76 | let path = UIBezierPath()
77 | path.move(to: CGPoint(x: self.frame.size.width / 2 , y:timeLabel.frame.size.height))
78 | path.addLine(to: CGPoint(x:self.frame.size.width / 2 , y:self.frame.size.height))
79 | path.lineWidth = 1.4
80 | UIColor(hue: 0, saturation:0, brightness:1.0, alpha: 0.7).setStroke()
81 | path.stroke()
82 |
83 | }
84 |
85 |
86 | func gradient() {
87 | let width = self.frame.size.width
88 |
89 |
90 | let context = UIGraphicsGetCurrentContext()!
91 |
92 | let startColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.06).cgColor
93 | let endColor = UIColor.clear.cgColor
94 | let colors = [startColor, endColor] as CFArray
95 |
96 | let locations = [0, 1] as [CGFloat]
97 |
98 | let space = CGColorSpaceCreateDeviceRGB()
99 |
100 | let gradient = CGGradient(colorsSpace: space, colors: colors, locations: locations)!
101 | context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) - 0.7, y:0), end: CGPoint(x: (width / 2) - 4, y: 0), options: [])
102 | context.drawLinearGradient(gradient, start: CGPoint(x:(width / 2) + 0.7, y:0), end: CGPoint(x: (width / 2) + 4, y: 0), options: [])
103 | }
104 |
105 | var ignoreSendScrollToParent = false
106 | func setScrollPoint(_ scrollPoint:CGFloat) {
107 | currentTime = Float64(scrollPoint) * duration
108 | setTimeText()
109 | if !ignoreSendScrollToParent {
110 | if let parent = parentView {
111 | parent.moved(currentTime)
112 | }
113 | }
114 |
115 | mainView!.currentTime = currentTime
116 | ignoreSendScrollToParent = false
117 | }
118 |
119 | func setTimeText() {
120 | let minute = Int(currentTime / 60)
121 | let second = (currentTime - Float64(minute) * 60)
122 | let milliSec = Int((second - Float64(Int(second))) * 100)
123 | let text = String(format: "%02d:%02d.%02d", minute, (Int(second)), milliSec)
124 | timeLabel.text = text
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/TimelineScroller.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimelineScroller.swift
3 | // Examplay
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/01.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TimelineScroller: UIScrollView {
12 |
13 | var parentView:TimelineView? = nil
14 | let frameImagesView = FrameImagesView()
15 | let measure = TimelineMeasure()
16 | let trimView = TrimView()
17 |
18 |
19 | override init (frame: CGRect) {
20 | super.init(frame: frame)
21 |
22 | self.isScrollEnabled = true
23 | self.isDirectionalLockEnabled = true
24 | self.showsHorizontalScrollIndicator = false
25 | self.showsVerticalScrollIndicator = false
26 | self.bounces = false
27 | self.decelerationRate = .fast
28 | self.isMultipleTouchEnabled = true
29 | self.delaysContentTouches = false
30 | self.frameImagesView.parentScroller = self
31 | self.addSubview(frameImagesView)
32 | self.measure.parentScroller = self
33 | self.measure.frameImagesView = self.frameImagesView
34 | self.measure.backgroundColor = .clear
35 | }
36 |
37 | required init(coder aDecoder: NSCoder) {
38 | fatalError("TimelineScroller init(coder:) has not been implemented")
39 | }
40 |
41 | func configure(parent:TimelineView) {
42 | parentView = parent
43 | trimView.configure(parent, scroller:self)
44 | }
45 |
46 | func reset() {
47 | frameImagesView.reset()
48 | }
49 |
50 | var ignoreScrollViewDidScroll:Bool = false
51 | func setContentWidth(_ width:CGFloat) {
52 | setContentWidth(width, setOrigin:true)
53 | }
54 |
55 | func setContentWidth(_ width: CGFloat, setOrigin:Bool) {
56 | ignoreScrollViewDidScroll = true
57 | self.contentSize = CGSize(width:width + self.frame.size.width, height:self.frame.size.height)
58 | frameImagesView.frame.size.width = width
59 | //measure.frame.size.width = width
60 |
61 | if setOrigin {
62 | let halfVisibleWidth = self.frame.size.width / 2
63 | //frameImagesView.frame.size.height = self.frame.size.height
64 | frameImagesView.frame.origin.x = halfVisibleWidth
65 | }
66 | //measure.frame.origin.x = halfVisibleWidth
67 | measure.setNeedsDisplay()
68 | frameImagesView.displayFrames()
69 | }
70 |
71 | var measureHeight:CGFloat = 5
72 | func coordinate() {
73 |
74 | let measureMin:CGFloat = 10
75 |
76 | let wholeHeight = self.frame.size.height
77 | measureHeight = wholeHeight * 0.2
78 | if measureHeight < measureMin {
79 | measureHeight = measureMin
80 | }
81 | frameImagesView.frame.size.height = wholeHeight - measureHeight
82 | if frameImagesView.animating == false {
83 | frameImagesView.frame.origin = CGPoint(x: self.frame.size.width / 2,y: measureHeight)
84 | }
85 | measure.frame.size.height = measureHeight
86 | //frameImagesView.layout()
87 |
88 | trimView.frame = self.frame
89 |
90 | }
91 |
92 | func visibleRect() -> CGRect {
93 | var visibleRect = frame
94 | visibleRect.origin = contentOffset
95 | if contentSize.width < frame.size.width {
96 | visibleRect.size.width = contentSize.width
97 | }
98 | if contentSize.height < frame.size.height {
99 | visibleRect.size.height = contentSize.height
100 | }
101 | if zoomScale != 1 {
102 | let theScale = 1.0 / zoomScale;
103 | visibleRect.origin.x *= theScale;
104 | visibleRect.origin.y *= theScale;
105 | visibleRect.size.width *= theScale;
106 | visibleRect.size.height *= theScale;
107 | }
108 | return visibleRect
109 | }
110 |
111 |
112 | //MARK: - scroll
113 | func setScrollPoint(_ scrollPoint:CGFloat) {
114 | let offset = (scrollPoint * frameImagesView.frame.size.width) + (self.frame.size.width / 2)
115 |
116 | self.contentOffset.x = offset - (self.frame.size.width / 2)
117 | }
118 |
119 |
120 | //MARK: - Touch Events
121 | var allTouches = [UITouch]()
122 |
123 | override open func touchesBegan(_ touches: Set, with event: UIEvent?)
124 | {
125 | for touch in touches {
126 | if !allTouches.contains(touch) {
127 | allTouches += [touch]
128 | }
129 | if !parentView!.allTouches.contains(touch) {
130 | parentView!.allTouches += [touch]
131 | }
132 | }
133 |
134 | }
135 |
136 | override open func touchesMoved(_ touches: Set, with event: UIEvent?)
137 | {
138 | if parentView!.allTouches.count == 2 {
139 | if parentView!.pinching {
140 | parentView!.updatePinch()
141 | } else {
142 | parentView!.startPinch()
143 | }
144 | }
145 | }
146 |
147 | override open func touchesEnded(_ touches: Set, with event: UIEvent?)
148 | {
149 | for touch in touches {
150 | if let index = allTouches.firstIndex(of:touch) {
151 | allTouches.remove(at: index)
152 | }
153 | if let index = parentView!.allTouches.firstIndex(of:touch) {
154 | parentView!.allTouches.remove(at: index)
155 | }
156 | }
157 | if parentView!.pinching && parentView!.allTouches.count < 2 {
158 | parentView!.endPinch()
159 | }
160 | }
161 |
162 | override open func touchesCancelled(_ touches: Set, with event: UIEvent?)
163 | {
164 | for touch in touches {
165 | if let index = allTouches.firstIndex(of:touch) {
166 | allTouches.remove(at: index)
167 | }
168 | if let index = parentView!.allTouches.firstIndex(of:touch) {
169 | parentView!.allTouches.remove(at: index)
170 | }
171 | }
172 |
173 | if parentView!.pinching {
174 | parentView!.endPinch()
175 | }
176 | }
177 | }
178 |
179 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/TimelineMeasure.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimelineMeasure.swift
3 | // Examplay
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/08.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TimelineMeasure: UIView {
12 |
13 | var unitSize:CGFloat = 100
14 | var frameImagesView:FrameImagesView? = nil
15 | var parentScroller:TimelineScroller? = nil
16 | var parentView:TimelineView? = nil
17 | var stringColor:UIColor = UIColor(hue: 0.0, saturation:0.0, brightness:0.35, alpha: 1)
18 | var animating = false
19 |
20 | override init (frame: CGRect) {
21 | super.init(frame: frame)
22 |
23 | self.isMultipleTouchEnabled = true
24 | self.isUserInteractionEnabled = true
25 | }
26 |
27 | required init(coder aDecoder: NSCoder) {
28 | fatalError("TimelineMeasure init(coder:) has not been implemented")
29 | }
30 |
31 |
32 |
33 | override func draw(_ rect: CGRect) {
34 |
35 | if frameImagesView == nil {
36 | return
37 | }
38 |
39 | let max = frameImagesView!.maxWidth
40 | var width = frameImagesView!.frame.size.width
41 | if animating {
42 | if let layer = frameImagesView!.layer.presentation() {
43 | width = layer.frame.size.width
44 | }
45 | }
46 | if width == 0 {
47 | return
48 | }
49 | var unit = (width / max) * unitSize
50 |
51 | let unitWidth = CGFloat(80)
52 | var division = 0
53 | while (unit <= unitWidth) {
54 | unit *= 2
55 | division += 1
56 | if division > 1000 {
57 | break
58 | }
59 | }
60 | let unitLength = pow(2,Double(division + 1)) / 2
61 | func index(_ position:CGFloat) -> Int {
62 | return Int(position / unit)
63 | }
64 |
65 | let paragraphStyle = NSMutableParagraphStyle()
66 | paragraphStyle.alignment = .center
67 |
68 | let attributes = [NSAttributedString.Key.font: UIFont(name: "HelveticaNeue", size: self.frame.size.height * 0.7)!, NSAttributedString.Key.paragraphStyle: paragraphStyle, NSAttributedString.Key.foregroundColor: stringColor]
69 |
70 | var string = ""
71 |
72 | var visibleRect = rect
73 | if let parent = parentScroller {
74 |
75 | if animating {
76 | if let layer = parent.layer.presentation() {
77 | let offset = layer.bounds.origin
78 | visibleRect.origin = offset
79 | visibleRect.size = parent.frame.size
80 | } else {
81 | visibleRect = parent.visibleRect()
82 | }
83 | } else {
84 | visibleRect = parent.visibleRect()
85 | }
86 | }
87 | let startIndex = index(visibleRect.origin.x) - 10 * (division + 1)
88 | let endIndex = index(visibleRect.origin.x + visibleRect.size.width)
89 |
90 | for index in startIndex ... endIndex {
91 | if index < 0 {
92 | continue
93 | }
94 | let position = CGFloat(index) * unit - visibleRect.origin.x + (visibleRect.size.width / 2)
95 | if division == 0 {
96 | let path = UIBezierPath()
97 | path.move(to: CGPoint(x: position + (unit / 2 ) - (unitWidth / 4) , y:(self.frame.size.height / 2)))
98 | path.addLine(to: CGPoint(x: position + (unit / 2) + (unitWidth / 4), y:(self.frame.size.height / 2)))
99 | path.lineWidth = 1
100 | stringColor.setStroke()
101 | path.stroke()
102 | } else {
103 | for i in 1 ... 3 {
104 | let point = position + ((unit / 4 ) * CGFloat(i))
105 | let r:CGFloat = 0.8
106 | let pointRect = CGRect(x: point - r, y: (self.frame.size.height / 2) - r, width: r * 2,height: r * 2)
107 | let path = UIBezierPath(ovalIn:pointRect)
108 | stringColor.setFill()
109 | path.fill()
110 | }
111 | }
112 |
113 | let length = unitLength * Double(index)
114 | let minute = Int(length / 60)
115 | let second = Int(length - Double(minute * 60))
116 |
117 | string = String(format: "%02d:%02d", minute, (Int(second)))
118 |
119 | string.draw(with: CGRect(x: position - (unitWidth / 2), y:0, width: unitWidth, height: self.frame.size.height), options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
120 |
121 |
122 | }
123 | }
124 |
125 | //MARK: - timer for animation
126 | var animationTimer = Timer()
127 | func startAnimation() {
128 | animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true)
129 | RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common)
130 | animating = true
131 | }
132 |
133 | func stopAnimation() {
134 | animating = false
135 | self.setNeedsDisplay()
136 | animationTimer.invalidate()
137 | }
138 |
139 | @objc func animate(_ timer:Timer) {
140 | if animating == false {
141 | return
142 | }
143 | self.setNeedsDisplay()
144 | }
145 |
146 |
147 |
148 | //MARK: - Touch Events
149 | var allTouches = [UITouch]()
150 |
151 | override open func touchesBegan(_ touches: Set, with event: UIEvent?)
152 | {
153 | for touch in touches {
154 | if !allTouches.contains(touch) {
155 | allTouches += [touch]
156 | }
157 | if !parentView!.allTouches.contains(touch) {
158 | parentView!.allTouches += [touch]
159 | }
160 | }
161 | }
162 |
163 |
164 | override open func touchesMoved(_ touches: Set, with event: UIEvent?)
165 | {
166 | if parentView!.allTouches.count == 2 {
167 | if parentView!.pinching {
168 | parentView!.updatePinch()
169 | } else {
170 | parentView!.startPinch()
171 | }
172 | }
173 | }
174 |
175 | override open func touchesEnded(_ touches: Set, with event: UIEvent?)
176 | {
177 | for touch in touches {
178 | if let index = allTouches.firstIndex(of:touch) {
179 | allTouches.remove(at: index)
180 | }
181 | if let index = parentView!.allTouches.firstIndex(of:touch) {
182 | parentView!.allTouches.remove(at: index)
183 | }
184 | }
185 | if parentView!.pinching && parentView!.allTouches.count < 2 {
186 | parentView!.endPinch()
187 | }
188 | }
189 |
190 | override open func touchesCancelled(_ touches: Set, with event: UIEvent?)
191 | {
192 | for touch in touches {
193 | if let index = allTouches.firstIndex(of:touch) {
194 | allTouches.remove(at: index)
195 | }
196 | if let index = parentView!.allTouches.firstIndex(of:touch) {
197 | parentView!.allTouches.remove(at: index)
198 | }
199 | }
200 |
201 | if parentView!.pinching {
202 | parentView!.endPinch()
203 | }
204 | }
205 | }
206 |
207 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/VideoTimelineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // VideoTimelineView.swift
3 | // VideoTimelineView
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/28.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 | import AVFoundation
11 |
12 | protocol TimelinePlayStatusReceiver: class {
13 | func videoTimelineStopped()
14 | func videoTimelineMoved()
15 | func videoTimelineTrimChanged()
16 | }
17 |
18 |
19 | struct VideoTimelineTrim {
20 | var start:Float64
21 | var end:Float64
22 | }
23 |
24 | class VideoTimelineView: UIView {
25 |
26 | public private(set) var asset:AVAsset? = nil
27 | var player:AVPlayer? = nil
28 |
29 | weak var playStatusReceiver:TimelinePlayStatusReceiver? = nil
30 |
31 | var repeatOn:Bool = false
32 |
33 | public private(set) var trimEnabled:Bool = false
34 |
35 |
36 |
37 | var currentTime:Float64 = 0
38 | public private(set) var duration:Float64 = 0
39 |
40 | public private(set) var audioPlayer:AVPlayer!
41 | public private(set) var audioPlayer2:AVPlayer!
42 |
43 | let timelineView = TimelineView()
44 |
45 | override init (frame: CGRect) {
46 | super.init(frame: frame)
47 | timelineView.mainView = self
48 | timelineView.centerLine.mainView = self
49 | timelineView.scroller.frameImagesView.mainView = self
50 | timelineView.scroller.trimView.mainView = self
51 |
52 | self.addSubview(timelineView)
53 | }
54 |
55 | required init(coder aDecoder: NSCoder) {
56 | fatalError("MainView init(coder:) has not been implemented")
57 | }
58 |
59 |
60 | func viewDidLayoutSubviews() {
61 | coordinate()
62 | }
63 |
64 | func coordinate() {
65 | timelineView.coordinate()
66 | }
67 |
68 | func new(asset newAsset:AVAsset?) {
69 | if let new = newAsset {
70 | asset = new
71 | duration = CMTimeGetSeconds(new.duration)
72 | player = AVPlayer(playerItem: AVPlayerItem(asset: asset!))
73 | audioPlayer = AVPlayer(playerItem: AVPlayerItem(asset: asset!))
74 | audioPlayer.volume = 1.0
75 | audioPlayer2 = AVPlayer(playerItem: AVPlayerItem(asset: asset!))
76 | audioPlayer2.volume = 1.0
77 | timelineView.newMovieSet()
78 | }
79 | }
80 |
81 |
82 | func setTrim(start:Float64, end:Float64, seek:Float64?, animate:Bool) {
83 |
84 | var seekTime = currentTime
85 | if let time = seek {
86 | seekTime = time
87 | }
88 | if animate {
89 | timelineView.setTrimWithAnimation(trim:VideoTimelineTrim(start:start, end:end), time:seekTime)
90 | } else {
91 | timelineView.setTrim(start:start, end:end)
92 | if seek != nil {
93 | moveTo(seek!, animate:animate)
94 | }
95 | }
96 | }
97 |
98 | func setTrimIsEnabled(_ enabled:Bool) {
99 | trimEnabled = enabled
100 | timelineView.setTrimmerStatus(enabled:enabled)
101 | }
102 |
103 | func setTrimmerIsHidden(_ hide:Bool) {
104 | timelineView.setTrimmerVisible(!hide)
105 | }
106 |
107 | func currentTrim() -> (start:Float64, end:Float64) {
108 | let trim = timelineView.currentTrim()
109 | return (trim.start,trim.end)
110 | }
111 |
112 | func moveTo(_ time:Float64, animate:Bool) {
113 | if animate {
114 |
115 | } else {
116 | accurateSeek(time, scrub:false)
117 | timelineView.setCurrentTime(time, force:true)
118 | }
119 | }
120 |
121 | //MARK: - seeking
122 | var previousSeektime:Float64 = 0
123 | func timelineIsMoved(_ currentTime:Float64, scrub:Bool) {
124 | let move = abs(currentTime - previousSeektime)
125 | let seekTolerance = CMTimeMakeWithSeconds(move, preferredTimescale:100)
126 |
127 | if player != nil {
128 | player!.seek(to:CMTimeMakeWithSeconds(currentTime , preferredTimescale:100), toleranceBefore:seekTolerance,toleranceAfter:seekTolerance)
129 | }
130 | previousSeektime = currentTime
131 | if scrub {
132 | audioScrub()
133 | }
134 | }
135 |
136 | func accurateSeek(_ currentTime:Float64, scrub:Bool) {
137 | previousSeektime = currentTime
138 | timelineIsMoved(currentTime, scrub:scrub)
139 | }
140 |
141 | var scrubed1 = Date()
142 | var scrubed2 = Date()
143 | var canScrub1 = true
144 | var canScrub2 = true
145 | func audioScrub() {
146 | if player == nil {
147 | return
148 | }
149 | if scrubed2.timeIntervalSinceNow < -0.16 && canScrub1 {
150 | canScrub1 = false
151 | self.scrubed1 = Date()
152 | DispatchQueue.main.async {
153 | if self.audioPlayer.timeControlStatus == .playing {
154 | self.audioPlayer.pause()
155 | self.canScrub1 = true
156 | } else {
157 | self.audioPlayer.seek(to: self.player!.currentTime())
158 | self.audioPlayer.play()
159 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) {
160 | self.audioPlayer.pause()
161 | self.audioPlayer.seek(to: self.player!.currentTime())
162 | self.canScrub1 = true
163 | }
164 | }
165 |
166 | }
167 | }
168 | if scrubed1.timeIntervalSinceNow < -0.16 && canScrub2 {
169 | canScrub2 = false
170 | self.scrubed2 = Date()
171 | DispatchQueue.main.async {
172 | if self.audioPlayer2.timeControlStatus == .playing {
173 | self.audioPlayer2.pause()
174 | self.canScrub2 = true
175 | } else {
176 | self.audioPlayer2.seek(to: self.player!.currentTime())
177 | self.audioPlayer2.play()
178 |
179 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.22) {
180 | self.audioPlayer2.pause()
181 | self.audioPlayer2.seek(to: self.player!.currentTime())
182 | self.canScrub2 = true
183 | }
184 | }
185 | }
186 | }
187 | }
188 |
189 |
190 | //MARK: - play
191 | var playerTimer = Timer()
192 | @objc dynamic var playing = false
193 | func play() {
194 | if asset == nil {
195 | return
196 | }
197 | let currentTime = timelineView.centerLine.currentTime
198 | let reached = timeReachesEnd(currentTime)
199 |
200 | if reached.trimEnd {
201 | accurateSeek(timelineView.currentTrim().start, scrub:false)
202 | timelineView.manualScrolledAfterEnd = false
203 | } else if reached.movieEnd {
204 | accurateSeek(0, scrub:false)
205 | timelineView.manualScrolledAfterEnd = false
206 | }
207 | if player != nil {
208 | player!.play()
209 | }
210 | playerTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.playerTimerAction(_:)), userInfo: nil, repeats: true)
211 | RunLoop.main.add(playerTimer, forMode:RunLoop.Mode.common)
212 | playing = true
213 | }
214 |
215 | func stop() {
216 | playing = false
217 | if asset == nil || player == nil {
218 | return
219 | }
220 | player!.pause()
221 | playerTimer.invalidate()
222 |
223 | if let receiver = playStatusReceiver {
224 | receiver.videoTimelineStopped()
225 | }
226 | }
227 |
228 | var reachFlg = false
229 | @objc func playerTimerAction(_ timer:Timer) {
230 | if player == nil {
231 | return
232 | }
233 | var currentPlayerTime = CMTimeGetSeconds(player!.currentTime())
234 |
235 | let trim = timelineView.currentTrim()
236 | let reached = timeReachesEnd(currentPlayerTime)
237 | if timelineView.inAction() {
238 | if player!.timeControlStatus == .playing {
239 | player!.pause()
240 | }
241 | } else if reached.reached {
242 | if repeatOn && reached.trimEnd {
243 |
244 | if player!.timeControlStatus == .playing {
245 | player!.pause()
246 | }
247 | currentPlayerTime = trim.start
248 | accurateSeek(currentPlayerTime, scrub:false)
249 | reachFlg = true
250 |
251 | } else {
252 | stop()
253 | }
254 | timelineView.setCurrentTime(currentPlayerTime,force:false)
255 | timelineView.manualScrolledAfterEnd = false
256 | } else if timelineView.animating == false {
257 | timelineView.setCurrentTime(currentPlayerTime,force:false)
258 | if player!.timeControlStatus == .paused {
259 | player!.play()
260 | }
261 | if reachFlg {
262 | if let receiver = playStatusReceiver {
263 | receiver.videoTimelineMoved()
264 | }
265 | reachFlg = false
266 | }
267 | }
268 | }
269 |
270 | func timeReachesEnd(_ time:Float64) -> (reached:Bool, trimEnd:Bool, movieEnd:Bool) {
271 | var reached = false
272 | var trimEnd = false
273 | var movieEnd = false
274 | if asset != nil {
275 | let duration = CMTimeGetSeconds(asset!.duration)
276 | let trimTimeEnd = timelineView.currentTrim().end
277 | if (time >= trimTimeEnd && timelineView.manualScrolledAfterEnd == false && trimEnabled) {
278 | trimEnd = true
279 | reached = true
280 | }
281 | if time >= duration {
282 | if trimTimeEnd < duration {
283 | trimEnd = false
284 | }
285 | movieEnd = true
286 | reached = true
287 | }
288 | }
289 | return (reached, trimEnd, movieEnd)
290 | }
291 |
292 |
293 | //MARK: -
294 | func resizeHeightKeepRatio(_ size:CGSize, height:CGFloat) -> CGSize {
295 | var result = size
296 | let ratio = size.width / size.height
297 | result.height = height
298 | result.width = height * ratio
299 | return result
300 | }
301 | }
302 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/FrameImagesView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // FrameImagesView.swift
3 | // Examplay
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/01.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AVFoundation
12 |
13 |
14 |
15 | class FrameImage: UIImageView {
16 | var tolerance:Float64? = nil
17 | }
18 |
19 |
20 | // MARK: - FrameImagesView
21 | class FrameImagesView: UIScrollView {
22 |
23 |
24 | var mainView:VideoTimelineView!
25 |
26 | var frameImagesArray:[FrameImage] = []
27 |
28 |
29 | var thumbnailFrameSize:CGSize = CGSize(width: 640,height: 480)
30 | let preferredTimescale:Int32 = 100
31 | var timeTolerance = CMTimeMakeWithSeconds(10 , preferredTimescale:100)
32 |
33 | var maxWidth:CGFloat = 0
34 | var minWidth:CGFloat = 0
35 |
36 | var parentScroller:TimelineScroller? = nil
37 |
38 | override init (frame: CGRect) {
39 | super.init(frame: frame)
40 | self.isScrollEnabled = false
41 | self.isUserInteractionEnabled = false
42 | self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.02)
43 | }
44 |
45 | required init(coder aDecoder: NSCoder) {
46 | fatalError("FrameImagesView init(coder:) has not been implemented")
47 | }
48 |
49 |
50 |
51 |
52 | func reset() {
53 | discardAllFrameImages()
54 | cancelImageGenerator()
55 |
56 | prepareFrameViews()
57 | layout()
58 | requestVisible(depth:0, wide:2, direction:0)
59 | }
60 |
61 | //MARK: - timer for animation
62 | var animationTimer = Timer()
63 | var animating = false
64 | func startAnimation() {
65 | animating = true
66 | displayFrames()
67 | animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true)
68 | RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common)
69 |
70 | }
71 |
72 | func stopAnimation() {
73 | animationTimer.invalidate()
74 | animating = false
75 |
76 |
77 | if let parent = parentScroller {
78 | frame.origin = CGPoint(x: parent.frame.size.width / 2, y: parent.measureHeight)
79 | parent.addSubview(self)
80 | }
81 | displayFrames()
82 | }
83 |
84 | @objc func animate(_ timer:Timer) {
85 | if animating == false {
86 | return
87 | }
88 | displayFrames()
89 | }
90 |
91 | //MARK: - layout
92 |
93 |
94 | var uponFrames = Set()
95 | var belowFrames = Set()
96 | var deepFrames = Set()
97 | var hiddenFrames = Set()
98 |
99 |
100 | func layout() {
101 | setThumnailFrameSize()
102 | let coordinated = coordinateFrames()
103 | uponFrames = coordinated.upon
104 | belowFrames = coordinated.below
105 | hiddenFrames = coordinated.hidden
106 | deepFrames = coordinated.deep
107 | displayFrames()
108 | }
109 |
110 | func displayFrames() {
111 |
112 | var baseView:UIView = self
113 | var offset:CGPoint = CGPoint.zero
114 |
115 | if let parent = parentScroller {
116 | let visibleHalf = parent.frame.size.width / 2
117 |
118 | if animating {
119 | if let layer = parent.layer.presentation() {
120 | offset.x = visibleHalf - layer.bounds.origin.x
121 | offset.y = parent.frame.origin.y + parent.measureHeight
122 | frame.origin = offset
123 | mainView!.timelineView.viewForAnimate.addSubview(self)
124 | }
125 | }
126 | }
127 |
128 | let visibleSide = indexOfVisibleSide()
129 | func locate(_ index:Int, visible:Bool) {
130 | if frameImagesArray.count > index && index >= 0 {
131 | let frameImg = frameImagesArray[index]
132 | if index >= visibleSide.left && index <= visibleSide.right {
133 | let position = positionWithIndex(index)
134 | frameImg.frame = CGRect(x: position, y:0, width: thumbnailFrameSize.width, height: thumbnailFrameSize.height)
135 | if visible {
136 | self.addSubview(frameImg)
137 | } else {
138 | frameImg.removeFromSuperview()
139 | }
140 | } else {
141 | frameImg.removeFromSuperview()
142 | }
143 |
144 | }
145 | }
146 | for element in belowFrames {//do addSubview under the upon
147 | locate(element, visible:true)
148 | }
149 | for element in uponFrames {
150 | locate(element, visible:true)
151 | }
152 | for element in hiddenFrames {// includes deep
153 | locate(element, visible:false)
154 | }
155 |
156 | }
157 |
158 | func positionWithIndex(_ index:Int) -> CGFloat {
159 | let position = frame.size.width * (CGFloat(index) / CGFloat(thumbnailCountFloat()))
160 | return position
161 | }
162 |
163 | func coordinateFrames() -> (upon:Set, below:Set, hidden:Set, deep:Set) {
164 |
165 | let keyDivision = Int(pow(2,Double(Int(log2(maxWidth * 2 / (self.frame.size.width + 1))))))
166 | if keyDivision <= 0 {
167 | return (Set(), Set(), Set(), Set())
168 | }
169 | let keyCount = ((frameImagesArray.count - 1) / keyDivision) + 1
170 |
171 |
172 | var visibleIndexes = Set()
173 | var uponElements = Set()
174 | for index in 0 ... (keyCount - 1) {
175 | let uponIndex = index * keyDivision
176 | uponElements.insert(uponIndex)
177 | visibleIndexes.insert(uponIndex)
178 | }
179 | var belowElements = Set()
180 | let belowDivision = keyDivision / 2
181 | if belowDivision >= 1 {
182 | for index in 0 ..< (keyCount - 1) {
183 | let belowIndex = (index * keyDivision) + (keyDivision / 2)
184 | belowElements.insert(belowIndex)
185 | visibleIndexes.insert(belowIndex)
186 | }
187 | }
188 | var deepElements = Set()
189 | let deepDivision = belowDivision / 2
190 | if deepDivision >= 1 {
191 | if let max = visibleIndexes.max() {
192 | for index in visibleIndexes {
193 | if max > index {
194 | deepElements.insert(index + deepDivision)
195 | }
196 | }
197 | }
198 | }
199 | var hiddenElements = Set()
200 | for index in 0 ..< frameImagesArray.count {
201 | if !visibleIndexes.contains(index) {
202 | let hiddenIndex = index
203 | hiddenElements.insert(hiddenIndex)
204 | }
205 | }
206 | return (uponElements,belowElements,hiddenElements, deepElements)
207 | }
208 |
209 |
210 | func setThumnailFrameSize() {
211 | if let asset = mainView!.asset {
212 | var frameSize = CGSize(width: 640,height: 480)
213 | let tracks:Array = asset.tracks(withMediaType:AVMediaType.video)
214 | if tracks.count > 0 {
215 | let track = tracks[0]
216 | frameSize = track.naturalSize.applying(track.preferredTransform)
217 | frameSize.width = abs(frameSize.width)
218 | frameSize.height = abs(frameSize.height)
219 | }
220 | thumbnailFrameSize = mainView!.resizeHeightKeepRatio(frameSize, height:self.frame.size.height)
221 | }
222 | }
223 |
224 | func assetDuration() -> Float64? {
225 | if let asset = mainView!.asset {
226 | return CMTimeGetSeconds(asset.duration)
227 | }
228 | return nil
229 | }
230 |
231 | func prepareFrameViews() {
232 | frameImagesArray = []
233 | for _ in 0 ... Int(thumbnailCountFloat()) {
234 | let view = FrameImage()
235 | view.alpha = 0
236 | frameImagesArray += [view]
237 | }
238 | }
239 |
240 |
241 |
242 | func thumbnailCountFloat() -> CGFloat {
243 | return maxWidth / thumbnailFrameSize.width
244 | }
245 |
246 |
247 | func indexWithTime(_ time:Float64) -> Int? {
248 | if let assetDuration = assetDuration() {
249 | let value = time / (assetDuration / Float64(thumbnailCountFloat()))
250 | var intValue = Int(value)
251 | if value - Float64(intValue) >= 0.5 {
252 | intValue += 1
253 | }
254 | return intValue
255 | }
256 | return nil
257 | }
258 |
259 | func timeWithIndex(_ index:Int) -> Float64 {
260 | if let assetDuration = assetDuration() {
261 | return assetDuration * ((Float64(index)) / Float64(thumbnailCountFloat()))
262 | }
263 | return 0
264 | }
265 |
266 |
267 |
268 |
269 | //MARK: - frame images
270 | func cancelImageGenerator() {
271 | if let asset = mainView!.asset {
272 | let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
273 | assetImgGenerate.cancelAllCGImageGeneration()
274 | }
275 | }
276 |
277 | func discardAllFrameImages() {
278 |
279 | for index in 0 ..< frameImagesArray.count {
280 | let view = frameImagesArray[index]
281 | UIView.animate(withDuration: 0.5,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in
282 |
283 | view.alpha = 0
284 |
285 | },completion: { finished in
286 |
287 | view.image = nil
288 | view.removeFromSuperview()
289 | view.tolerance = nil
290 | })
291 | }
292 | frameImagesArray = []
293 | }
294 |
295 |
296 |
297 | func requestAll(){
298 |
299 | var timesArray = [NSValue]()
300 | for index in 0 ..< frameImagesArray.count {
301 | timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))]
302 | }
303 | requestImageGeneration(timesArray:timesArray)
304 | }
305 |
306 |
307 | var requesting = Set()
308 | func requestVisible(depth:Int, wide:Float, direction:Float) {
309 | var timesArray = [NSValue]()
310 | func request(_ index:Int) {
311 |
312 | if self.requesting.count > 0 && self.requesting.contains(index) {
313 | return
314 | }
315 |
316 | if frameImagesArray.count > index {
317 | let imageView = frameImagesArray[index]
318 | var needsUpdate = false
319 | if let tolerance = imageView.tolerance {
320 | if tolerance > CMTimeGetSeconds(timeTolerance) * 1.2 {
321 | needsUpdate = true
322 | }
323 | }
324 | if imageView.image == nil || needsUpdate {
325 | timesArray += [NSValue(time:CMTimeMakeWithSeconds(timeWithIndex(index) , preferredTimescale:preferredTimescale))]
326 | self.requesting.insert(index)
327 | }
328 | }
329 | }
330 | let visibleSide = indexOfVisibleSide()
331 | let width = visibleSide.right - visibleSide.left
332 | var additionLeft = Int(Float(width) * wide)
333 | var additionRight = additionLeft
334 | if direction > 0 {
335 | additionLeft = Int(Float(-width) * direction)
336 | } else if direction < 0 {
337 | additionRight = Int(Float(width) * direction)
338 | }
339 |
340 |
341 | for index in uponFrames {
342 | if index >= visibleSide.left - additionLeft && index <= visibleSide.right + additionRight {
343 | request(index)
344 | }
345 | }
346 | let belowAddLeft = Int(Float(additionLeft) * 0.5) - Int(Float(width) * 0.7)
347 | let belowAddRight = Int(Float(additionRight) * 0.5) - Int(Float(width) * 0.7)
348 | if depth > 0 {
349 | for index in belowFrames {
350 | if index >= visibleSide.left - belowAddLeft && index <= visibleSide.right + belowAddRight {
351 | request(index)
352 | }
353 | }
354 | }
355 | let deepAddLeft = Int(Float(additionLeft) * 0.2) - Int(Float(width) * 0.4)
356 | let deepAddRight = Int(Float(additionRight) * 0.2) - Int(Float(width) * 0.4)
357 | if depth > 1 {
358 | for index in deepFrames {
359 | if index >= visibleSide.left - deepAddLeft && index <= visibleSide.right + deepAddRight {
360 | request(index)
361 | }
362 | }
363 | }
364 | if timesArray.count > 0 {
365 | requestImageGeneration(timesArray:timesArray)
366 | }
367 | }
368 |
369 | func indexOfVisibleSide() -> (left:Int, right:Int) {
370 | func indexWithPosition(_ position:CGFloat) -> Int{
371 | let index = position * CGFloat(thumbnailCountFloat()) / frame.size.width
372 | var indexInt = Int(index)
373 | if index - CGFloat(indexInt) > 0.5 {
374 | indexInt += 1
375 | }
376 | return indexInt
377 | }
378 | var visibleLeft = indexWithPosition(-(parentScroller!.frame.width / 2) + parentScroller!.contentOffset.x - thumbnailFrameSize.width) - 1
379 | var visibleRight = indexWithPosition(parentScroller!.contentOffset.x + (parentScroller!.frame.size.width * 0.5) + thumbnailFrameSize.width)
380 | if visibleLeft < 0 {
381 | visibleLeft = 0
382 | }
383 | let max = frameImagesArray.count - 1
384 | if visibleRight > max {
385 | visibleRight = max
386 | }
387 | return (visibleLeft, visibleRight)
388 | }
389 |
390 | func updateTolerance() {
391 | if mainView!.asset == nil {
392 | return
393 | }
394 | let thumbDuration = Float64(thumbnailFrameSize.width / self.frame.size.width) * mainView!.duration * 2
395 | timeTolerance = CMTimeMakeWithSeconds(thumbDuration , preferredTimescale:100)
396 |
397 |
398 | }
399 |
400 | func requestImageGeneration(timesArray:[NSValue]) {
401 |
402 | if let asset = mainView!.asset {
403 | let assetImgGenerate : AVAssetImageGenerator = AVAssetImageGenerator(asset: asset)
404 | assetImgGenerate.appliesPreferredTrackTransform = true
405 | let maxsize = CGSize(width: thumbnailFrameSize.width * 1.5,height: thumbnailFrameSize.height * 1.5)
406 | assetImgGenerate.maximumSize = maxsize
407 | assetImgGenerate.requestedTimeToleranceAfter = timeTolerance
408 | assetImgGenerate.requestedTimeToleranceBefore = timeTolerance
409 |
410 | assetImgGenerate.generateCGImagesAsynchronously(forTimes: timesArray,
411 | completionHandler:
412 | { time,resultImage,actualTime,result,error in
413 |
414 | let timeValue = CMTimeGetSeconds(time)
415 | if let image = resultImage {
416 | DispatchQueue.main.async {
417 | self.setFrameImage(image:UIImage(cgImage:image), time:timeValue)
418 | }
419 | }
420 | if let index = self.indexWithTime(timeValue) {
421 | self.requesting.remove(index)
422 | }
423 | })
424 | }
425 | }
426 |
427 |
428 | func setFrameImage(image:UIImage, time:Float64) {
429 |
430 | if let index = indexWithTime(time) {
431 | if frameImagesArray.count > index && index >= 0 {
432 | let imageView = frameImagesArray[index]
433 |
434 | imageView.image = image
435 | imageView.tolerance = CMTimeGetSeconds(timeTolerance)
436 | UIView.animate(withDuration: 0.2,delay:Double(0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in
437 |
438 | imageView.alpha = 1
439 |
440 | },completion: { finished in
441 |
442 |
443 | })
444 | imageView.alpha = 1
445 | imageView.backgroundColor = .red
446 | }
447 | }
448 | }
449 | }
450 |
451 |
452 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/TimelineView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TimelineView.swift
3 | // Examplay
4 | //
5 | // Created by Tomohiro Yamashita on 2020/02/18.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import Foundation
10 | import UIKit
11 | import AVFoundation
12 |
13 | class TimelineView: UIView, UIScrollViewDelegate {
14 | var mainView:VideoTimelineView? = nil
15 | let scroller = TimelineScroller()
16 | let centerLine = CenterLine()
17 | let viewForAnimate = UIScrollView()
18 |
19 | let durationPerHeight:Float64 = 0.35
20 | var animating = false
21 |
22 | override init (frame: CGRect) {
23 |
24 | super.init(frame: frame)
25 |
26 | self.backgroundColor = UIColor(hue: 0, saturation:0, brightness:0.0, alpha: 0.05)
27 |
28 | viewForAnimate.frame.origin = CGPoint.zero
29 | viewForAnimate.isScrollEnabled = false
30 | viewForAnimate.isUserInteractionEnabled = false
31 | self.addSubview(viewForAnimate)
32 |
33 | self.addSubview(scroller)
34 | scroller.delegate = self
35 |
36 | self.addSubview(scroller.measure)
37 | scroller.configure(parent: self)
38 | centerLine.configure(parent:self)
39 | coordinate()
40 |
41 | self.addSubview(scroller.trimView)
42 | self.addSubview(centerLine)
43 |
44 | scroller.measure.parentView = self
45 | }
46 |
47 | required init(coder aDecoder: NSCoder) {
48 | fatalError("TimelineView init(coder:) has not been implemented")
49 | }
50 |
51 |
52 | //MARK: - coordinate
53 | func coordinate() {
54 | if mainView == nil {
55 | return
56 | }
57 | frame = mainView!.bounds
58 | viewForAnimate.frame.size = self.frame.size
59 |
60 | scroller.frame = self.bounds
61 | scroller.frameImagesView.frame.size.height = scroller.frame.size.height
62 | scroller.measure.frame = scroller.frame
63 | scroller.measure.frame.size.height = 20
64 | scroller.coordinate()
65 |
66 | centerLine.timeLabel.frame.size.height = scroller.measure.frame.size.height - 2
67 | let centerLineWidth:CGFloat = scroller.measure.frame.size.height * 5
68 | centerLine.frame = CGRect(x: (self.frame.size.width - centerLineWidth) / 2,y: 0,width: centerLineWidth,height: self.frame.size.height)
69 |
70 | centerLine.update()
71 |
72 | guard let view = (mainView) else { return }
73 | guard let _ = (view.asset) else { return }
74 | if scroller.frameImagesView.frame.size.width <= 0 {
75 | return
76 | }
77 | let previousThumbSize = scroller.frameImagesView.thumbnailFrameSize
78 | scroller.frameImagesView.setThumnailFrameSize()
79 | let thumbSize = scroller.frameImagesView.thumbnailFrameSize
80 | let unit = (thumbSize.height / CGFloat(durationPerHeight))
81 | scroller.measure.unitSize = unit
82 | let contentMaxWidth = (unit * CGFloat(centerLine.duration))
83 | scroller.frameImagesView.maxWidth = contentMaxWidth
84 | let defineMin = scroller.frame.size.width * 0.8
85 | var contentMinWidth:CGFloat
86 |
87 | if scroller.frameImagesView.maxWidth <= defineMin {
88 | contentMinWidth = scroller.frameImagesView.maxWidth
89 | } else {
90 | contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth)
91 | }
92 |
93 | scroller.frameImagesView.minWidth = contentMinWidth
94 |
95 | var currentWidth = snapWidth((scroller.frameImagesView.thumbnailFrameSize.width / previousThumbSize.width) * scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth)
96 | if currentWidth < scroller.frameImagesView.minWidth {
97 | currentWidth = scroller.frameImagesView.minWidth
98 | } else if currentWidth > scroller.frameImagesView.maxWidth {
99 | currentWidth = scroller.frameImagesView.maxWidth
100 | }
101 |
102 | scroller.setContentWidth(currentWidth)
103 |
104 | scroller.reset()
105 |
106 | scroller.trimView.layout()
107 | if view.currentTime <= view.duration {
108 | setCurrentTime(view.currentTime, force:false)
109 | }
110 | }
111 |
112 | func snapWidth(_ width:CGFloat, max:CGFloat) -> CGFloat {
113 | let n = log2((2 * max) / width)
114 | var intN = CGFloat(Int(n))
115 | if n - intN >= 0.5 {
116 | intN += 1
117 | }
118 | let result = (2 * max) / (pow(2,intN))
119 | return result
120 | }
121 |
122 | func scrollPoint() -> CGFloat {
123 | return scroller.contentOffset.x / scroller.frameImagesView.frame.size.width
124 | }
125 |
126 |
127 |
128 | //MARK: new movie set
129 | func newMovieSet() {
130 |
131 | coordinate()
132 | if let asset = mainView!.asset{
133 | scroller.frameImagesView.setThumnailFrameSize()
134 |
135 | let duration = asset.duration
136 | let durationFloat = CMTimeGetSeconds(duration)
137 | centerLine.duration = durationFloat
138 |
139 | let detailThumbSize = scroller.frameImagesView.thumbnailFrameSize
140 |
141 | let unit = (detailThumbSize.height / CGFloat(durationPerHeight))
142 | scroller.measure.unitSize = unit
143 |
144 |
145 | let contentMaxWidth = (unit * CGFloat(centerLine.duration))
146 | scroller.frameImagesView.maxWidth = contentMaxWidth
147 |
148 |
149 | let defineMin = scroller.frame.size.width * 0.8
150 | var contentMinWidth:CGFloat
151 |
152 | if scroller.frameImagesView.maxWidth <= defineMin {
153 | contentMinWidth = scroller.frameImagesView.maxWidth
154 | } else {
155 | contentMinWidth = snapWidth(defineMin, max:scroller.frameImagesView.maxWidth)
156 | }
157 | scroller.frameImagesView.minWidth = contentMinWidth
158 | scroller.setContentWidth(scroller.frameImagesView.minWidth)
159 |
160 | scroller.reset()
161 | scroller.trimView.reset(duration:durationFloat)
162 | }
163 | }
164 |
165 | //MARK: - currentTime
166 | func setCurrentTime(_ currentTime:Float64, force:Bool) {
167 | if inAction() && force == false {
168 | return
169 | }
170 | if mainView!.asset == nil {
171 | return
172 | }
173 | var scrollPoint:CGFloat = 0
174 | scrollPoint = CGFloat(currentTime / mainView!.duration)
175 |
176 | centerLine.ignoreSendScrollToParent = true
177 | centerLine.setScrollPoint(scrollPoint)
178 | scroller.ignoreScrollViewDidScroll = true
179 | scroller.setScrollPoint(scrollPoint)
180 |
181 | scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0)
182 |
183 | scroller.frameImagesView.displayFrames()
184 | scroller.measure.setNeedsDisplay()
185 | }
186 |
187 | func moved(_ currentTime:Float64) {
188 | mainView!.timelineIsMoved(currentTime, scrub:true)
189 | }
190 |
191 |
192 | //MARK: - TrimViews
193 | func setTrimmerStatus(enabled:Bool) {
194 | if enabled {
195 | scroller.trimView.alpha = 1
196 | scroller.trimView.startKnob.isUserInteractionEnabled = true
197 | scroller.trimView.alpha = 1
198 | scroller.trimView.endKnob.isUserInteractionEnabled = true
199 |
200 | } else {
201 | scroller.trimView.alpha = 0.5
202 | scroller.trimView.startKnob.isUserInteractionEnabled = false
203 | scroller.trimView.alpha = 0.5
204 | scroller.trimView.endKnob.isUserInteractionEnabled = false
205 | }
206 | }
207 |
208 | func setTrimmerVisible(_ visible:Bool) {
209 | scroller.trimView.isHidden = !visible
210 | }
211 |
212 |
213 | func setTrim(start:Float64?, end:Float64?) {
214 | var changed = false
215 | if start != nil {
216 | scroller.trimView.startKnob.knobTimePoint = start!
217 | changed = true
218 | }
219 | if end != nil {
220 | scroller.trimView.endKnob.knobTimePoint = end!
221 | changed = true
222 | }
223 | if changed {
224 | scroller.trimView.layout()
225 | }
226 | }
227 |
228 | func setTrimWithAnimation(trim:VideoTimelineTrim, time:Float64) {
229 | scroller.trimView.moveToTimeAndTrimWithAnimation(time, trim:trim)
230 | }
231 |
232 |
233 | var manualScrolledAfterEnd = false
234 | func setTrimViewInteraction(_ active:Bool) {
235 | if mainView!.trimEnabled == false && active {
236 | return
237 | }
238 |
239 | scroller.trimView.startKnob.isUserInteractionEnabled = active
240 | scroller.trimView.endKnob.isUserInteractionEnabled = active
241 |
242 | if active {
243 | setManualScrolledAfterEnd()
244 | }
245 | }
246 |
247 | func setManualScrolledAfterEnd() {
248 | let trim = currentTrim()
249 | if mainView!.asset != nil {
250 | let currentTime = mainView!.currentTime
251 | if currentTime >= trim.end {
252 | manualScrolledAfterEnd = true
253 | } else {
254 | manualScrolledAfterEnd = false
255 | }
256 | }
257 | }
258 |
259 | func currentTrim() -> (start:Float64, end:Float64) {
260 | var start = scroller.trimView.startKnob.knobTimePoint
261 | var end = scroller.trimView.endKnob.knobTimePoint
262 | if mainView!.asset != nil {
263 | if end > mainView!.duration {
264 | end = mainView!.duration
265 | }
266 | if start < 0 {
267 | start = 0
268 | }
269 | }
270 | return (start, end)
271 | }
272 |
273 |
274 | func swapTrimKnobs() {
275 | let knob = scroller.trimView.endKnob
276 | scroller.trimView.endKnob = scroller.trimView.startKnob
277 | scroller.trimView.startKnob = knob
278 | }
279 |
280 | //MARK: - animation
281 | func startAnimation() {
282 | scroller.frameImagesView.startAnimation()
283 | scroller.measure.startAnimation()
284 | scroller.trimView.startAnimation()
285 | }
286 |
287 | func stopAnimation() {
288 | scroller.frameImagesView.stopAnimation()
289 | scroller.measure.stopAnimation()
290 | scroller.trimView.stopAnimation()
291 | }
292 |
293 |
294 |
295 |
296 |
297 | //MARK: - Gestures
298 | func inAction() -> Bool {
299 | if allTouches.count > 0 || scroller.isTracking || scroller.isDecelerating {
300 | return true
301 | } else {
302 | return false
303 | }
304 | }
305 |
306 | //MARK: - Scrolling
307 |
308 | var allTouches = [UITouch]()
309 | var pinching:Bool = false
310 |
311 | func scrollViewDidScroll(_ scrollView:UIScrollView) {
312 | scroller.trimView.layout()
313 | if scroller.ignoreScrollViewDidScroll {
314 | scroller.ignoreScrollViewDidScroll = false
315 | return
316 | }
317 | scroller.measure.setNeedsDisplay()
318 | scroller.frameImagesView.displayFrames()
319 | setTrimViewInteraction(false)
320 | let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width
321 | self.centerLine.setScrollPoint(scrollPoint)
322 |
323 | guard let mView = (mainView) else { return }
324 | if let receiver = mView.playStatusReceiver {
325 | receiver.videoTimelineMoved()
326 | }
327 | }
328 |
329 | func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
330 | scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0)
331 | setTrimViewInteraction(true)
332 | let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width
333 | self.centerLine.setScrollPoint(scrollPoint)
334 | }
335 |
336 | func scrollViewDidEndDragging(_ scrollView: UIScrollView,
337 | willDecelerate decelerate: Bool) {
338 | scroller.frameImagesView.requestVisible(depth:0, wide:0, direction:0)
339 | if decelerate == false {
340 | setTrimViewInteraction(true)
341 | }
342 | let scrollPoint = scroller.contentOffset.x / scroller.frameImagesView.frame.size.width
343 | self.centerLine.setScrollPoint(scrollPoint)
344 | }
345 |
346 |
347 |
348 | //MARK: - Zooming
349 |
350 | var pinchCenterInContent:CGFloat = 0
351 | var pinchStartDistance:CGFloat = 0
352 | var pinchStartContent:(x:CGFloat,width:CGFloat) = (0,0)
353 | func pinchCenter(_ pointA:CGPoint, pointB:CGPoint) -> CGPoint {
354 | return CGPoint(x: (pointA.x + pointB.x) / 2, y: (pointA.y + pointB.y) / 2)
355 | }
356 | func pinchDistance(_ pointA:CGPoint, pointB:CGPoint) -> CGFloat {
357 | return sqrt(pow((pointA.x - pointB.x),2) + pow((pointA.y - pointB.y),2));
358 | }
359 | func startPinch() {
360 | pinching = true
361 | scroller.isScrollEnabled = false
362 |
363 | let touch1 = allTouches[0]
364 | let touch2 = allTouches[1]
365 | let center = pinchCenter(touch1.location(in: self),pointB: touch2.location(in: self))
366 |
367 | pinchStartDistance = pinchDistance(touch1.location(in: self),pointB: touch2.location(in: self))
368 | let framewidth = scroller.frame.size.width
369 | pinchStartContent = ((framewidth / 2) - scroller.contentOffset.x,scroller.contentSize.width - framewidth)
370 | pinchCenterInContent = (center.x - pinchStartContent.x) / pinchStartContent.width
371 | }
372 |
373 | func updatePinch() {
374 | let touch1 = allTouches[0]
375 | let touch2 = allTouches[1]
376 | let center = pinchCenter(touch1.location(in: self), pointB:touch2.location(in: self))
377 | var sizeChange = (1 * pinchDistance(touch1.location(in: self), pointB: touch2.location(in: self))) / pinchStartDistance
378 |
379 | var contentWidth = pinchStartContent.width * sizeChange
380 |
381 | let sizeMin = scroller.frameImagesView.minWidth
382 | let sizeMax = scroller.frameImagesView.maxWidth
383 |
384 | if contentWidth < sizeMin {
385 | let sizeUnit = sizeMin / pinchStartContent.width
386 | sizeChange = ((pow(sizeChange/sizeUnit,2)/4) + 0.75) * sizeUnit
387 | contentWidth = pinchStartContent.width * sizeChange
388 | contentWidth = sizeMin
389 | } else if contentWidth > sizeMax {
390 | sizeChange = sizeMax
391 | contentWidth = sizeMax
392 | } else {
393 | let startRatio = pinchStartContent.width / sizeMax
394 | let currentRatio = startRatio * sizeChange
395 | let effect = ((sin(CGFloat.pi * 2 * log2(2/currentRatio)) * 0.108) - (sin(CGFloat.pi * 6 * log2(2/currentRatio)) * 0.009)) * currentRatio
396 | let resultWidth = sizeMax * (currentRatio + effect)
397 | contentWidth = resultWidth
398 | }
399 | let contentOrigin = center.x - (contentWidth * pinchCenterInContent)
400 | scroller.contentOffset.x = (scroller.frame.size.width / 2) - contentOrigin
401 | scroller.setContentWidth(contentWidth)
402 | scroller.frameImagesView.layout()
403 |
404 | scroller.frameImagesView.requestVisible(depth:2, wide:0, direction:0)
405 | self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width)
406 | scroller.trimView.layout()
407 | }
408 |
409 | func endPinch() {
410 | scroller.frameImagesView.requestVisible(depth:0, wide:1, direction:0)
411 |
412 | let width = snapWidth(scroller.frameImagesView.frame.size.width, max:scroller.frameImagesView.maxWidth)
413 |
414 | let offset = self.resizedPositionWithKeepOrigin(width:scroller.frameImagesView.frame.size.width, origin:scroller.contentOffset.x, destinationWidth:width)
415 | //startAnimation()
416 |
417 | UIView.animate(withDuration: 0.1,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in
418 |
419 | self.scroller.setContentWidth(width, setOrigin:false)
420 | self.scroller.contentOffset.x = offset
421 | self.scroller.frameImagesView.layout()
422 | self.scroller.trimView.layout()
423 | },completion: { finished in
424 | self.pinching = false
425 | self.scroller.isScrollEnabled = true
426 | })
427 |
428 | self.scroller.frameImagesView.updateTolerance()
429 |
430 | self.centerLine.setScrollPoint(scroller.contentOffset.x / scroller.frameImagesView.frame.size.width)
431 |
432 | setTrimViewInteraction(true)
433 |
434 | guard let mView = (mainView) else { return }
435 | if let receiver = mView.playStatusReceiver {
436 | receiver.videoTimelineMoved()
437 | }
438 | }
439 |
440 | func resizedPositionWithKeepOrigin(width:CGFloat, origin:CGFloat, destinationWidth:CGFloat) -> CGFloat {
441 | let originPoint = origin / width
442 | let result = originPoint * destinationWidth
443 | return result
444 | }
445 |
446 |
447 | }
448 |
449 |
450 |
451 |
452 |
453 |
454 |
455 |
456 |
457 |
458 |
459 |
460 |
461 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 50;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E29DE0E9242F19CF0046D62C /* VideoTimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E29DE0E8242F19CF0046D62C /* VideoTimelineView.swift */; };
11 | E2C43F1A24346FDF00C03ED5 /* movie.mov in Resources */ = {isa = PBXBuildFile; fileRef = E2C43F1924346FDF00C03ED5 /* movie.mov */; };
12 | E2D72532242E1E4F00C31BAD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72531242E1E4F00C31BAD /* AppDelegate.swift */; };
13 | E2D72534242E1E4F00C31BAD /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72533242E1E4F00C31BAD /* SceneDelegate.swift */; };
14 | E2D72536242E1E4F00C31BAD /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72535242E1E4F00C31BAD /* ViewController.swift */; };
15 | E2D72539242E1E4F00C31BAD /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2D72537242E1E4F00C31BAD /* Main.storyboard */; };
16 | E2D7253B242E1E5A00C31BAD /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E2D7253A242E1E5A00C31BAD /* Assets.xcassets */; };
17 | E2D7253E242E1E5A00C31BAD /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = E2D7253C242E1E5A00C31BAD /* LaunchScreen.storyboard */; };
18 | E2D72549242E1E5A00C31BAD /* VideoTimelineViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72548242E1E5A00C31BAD /* VideoTimelineViewTests.swift */; };
19 | E2D72554242E1E5A00C31BAD /* VideoTimelineViewUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72553242E1E5A00C31BAD /* VideoTimelineViewUITests.swift */; };
20 | E2D72565242E21EC00C31BAD /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72564242E21EC00C31BAD /* TimelineView.swift */; };
21 | E2D7256B242E223E00C31BAD /* CenterLine.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72566242E223D00C31BAD /* CenterLine.swift */; };
22 | E2D7256C242E223E00C31BAD /* TimelineMeasure.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72567242E223E00C31BAD /* TimelineMeasure.swift */; };
23 | E2D7256D242E223E00C31BAD /* TimelineScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72568242E223E00C31BAD /* TimelineScroller.swift */; };
24 | E2D7256E242E223E00C31BAD /* TrimView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D72569242E223E00C31BAD /* TrimView.swift */; };
25 | E2D7256F242E223E00C31BAD /* FrameImagesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2D7256A242E223E00C31BAD /* FrameImagesView.swift */; };
26 | /* End PBXBuildFile section */
27 |
28 | /* Begin PBXContainerItemProxy section */
29 | E2D72545242E1E5A00C31BAD /* PBXContainerItemProxy */ = {
30 | isa = PBXContainerItemProxy;
31 | containerPortal = E2D72526242E1E4F00C31BAD /* Project object */;
32 | proxyType = 1;
33 | remoteGlobalIDString = E2D7252D242E1E4F00C31BAD;
34 | remoteInfo = VideoTimelineView;
35 | };
36 | E2D72550242E1E5A00C31BAD /* PBXContainerItemProxy */ = {
37 | isa = PBXContainerItemProxy;
38 | containerPortal = E2D72526242E1E4F00C31BAD /* Project object */;
39 | proxyType = 1;
40 | remoteGlobalIDString = E2D7252D242E1E4F00C31BAD;
41 | remoteInfo = VideoTimelineView;
42 | };
43 | /* End PBXContainerItemProxy section */
44 |
45 | /* Begin PBXFileReference section */
46 | E29DE0E8242F19CF0046D62C /* VideoTimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoTimelineView.swift; sourceTree = ""; };
47 | E2C43F1924346FDF00C03ED5 /* movie.mov */ = {isa = PBXFileReference; lastKnownFileType = video.quicktime; path = movie.mov; sourceTree = ""; };
48 | E2D7252E242E1E4F00C31BAD /* VideoTimelineView.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VideoTimelineView.app; sourceTree = BUILT_PRODUCTS_DIR; };
49 | E2D72531242E1E4F00C31BAD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
50 | E2D72533242E1E4F00C31BAD /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
51 | E2D72535242E1E4F00C31BAD /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; };
52 | E2D72538242E1E4F00C31BAD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
53 | E2D7253A242E1E5A00C31BAD /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
54 | E2D7253D242E1E5A00C31BAD /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
55 | E2D7253F242E1E5A00C31BAD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
56 | E2D72544242E1E5A00C31BAD /* VideoTimelineViewTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VideoTimelineViewTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
57 | E2D72548242E1E5A00C31BAD /* VideoTimelineViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoTimelineViewTests.swift; sourceTree = ""; };
58 | E2D7254A242E1E5A00C31BAD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
59 | E2D7254F242E1E5A00C31BAD /* VideoTimelineViewUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = VideoTimelineViewUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
60 | E2D72553242E1E5A00C31BAD /* VideoTimelineViewUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoTimelineViewUITests.swift; sourceTree = ""; };
61 | E2D72555242E1E5A00C31BAD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
62 | E2D72564242E21EC00C31BAD /* TimelineView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; };
63 | E2D72566242E223D00C31BAD /* CenterLine.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CenterLine.swift; sourceTree = ""; };
64 | E2D72567242E223E00C31BAD /* TimelineMeasure.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineMeasure.swift; sourceTree = ""; };
65 | E2D72568242E223E00C31BAD /* TimelineScroller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TimelineScroller.swift; sourceTree = ""; };
66 | E2D72569242E223E00C31BAD /* TrimView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrimView.swift; sourceTree = ""; };
67 | E2D7256A242E223E00C31BAD /* FrameImagesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FrameImagesView.swift; sourceTree = ""; };
68 | /* End PBXFileReference section */
69 |
70 | /* Begin PBXFrameworksBuildPhase section */
71 | E2D7252B242E1E4F00C31BAD /* Frameworks */ = {
72 | isa = PBXFrameworksBuildPhase;
73 | buildActionMask = 2147483647;
74 | files = (
75 | );
76 | runOnlyForDeploymentPostprocessing = 0;
77 | };
78 | E2D72541242E1E5A00C31BAD /* Frameworks */ = {
79 | isa = PBXFrameworksBuildPhase;
80 | buildActionMask = 2147483647;
81 | files = (
82 | );
83 | runOnlyForDeploymentPostprocessing = 0;
84 | };
85 | E2D7254C242E1E5A00C31BAD /* Frameworks */ = {
86 | isa = PBXFrameworksBuildPhase;
87 | buildActionMask = 2147483647;
88 | files = (
89 | );
90 | runOnlyForDeploymentPostprocessing = 0;
91 | };
92 | /* End PBXFrameworksBuildPhase section */
93 |
94 | /* Begin PBXGroup section */
95 | E2D72525242E1E4F00C31BAD = {
96 | isa = PBXGroup;
97 | children = (
98 | E2C43F1924346FDF00C03ED5 /* movie.mov */,
99 | E2D72530242E1E4F00C31BAD /* VideoTimelineView */,
100 | E2D72547242E1E5A00C31BAD /* VideoTimelineViewTests */,
101 | E2D72552242E1E5A00C31BAD /* VideoTimelineViewUITests */,
102 | E2D7252F242E1E4F00C31BAD /* Products */,
103 | );
104 | sourceTree = "";
105 | };
106 | E2D7252F242E1E4F00C31BAD /* Products */ = {
107 | isa = PBXGroup;
108 | children = (
109 | E2D7252E242E1E4F00C31BAD /* VideoTimelineView.app */,
110 | E2D72544242E1E5A00C31BAD /* VideoTimelineViewTests.xctest */,
111 | E2D7254F242E1E5A00C31BAD /* VideoTimelineViewUITests.xctest */,
112 | );
113 | name = Products;
114 | sourceTree = "";
115 | };
116 | E2D72530242E1E4F00C31BAD /* VideoTimelineView */ = {
117 | isa = PBXGroup;
118 | children = (
119 | E2D72531242E1E4F00C31BAD /* AppDelegate.swift */,
120 | E2D72533242E1E4F00C31BAD /* SceneDelegate.swift */,
121 | E2D72535242E1E4F00C31BAD /* ViewController.swift */,
122 | E2D72561242E211F00C31BAD /* VideoTimelineView */,
123 | E2D72537242E1E4F00C31BAD /* Main.storyboard */,
124 | E2D7253A242E1E5A00C31BAD /* Assets.xcassets */,
125 | E2D7253C242E1E5A00C31BAD /* LaunchScreen.storyboard */,
126 | E2D7253F242E1E5A00C31BAD /* Info.plist */,
127 | );
128 | path = VideoTimelineView;
129 | sourceTree = "";
130 | };
131 | E2D72547242E1E5A00C31BAD /* VideoTimelineViewTests */ = {
132 | isa = PBXGroup;
133 | children = (
134 | E2D72548242E1E5A00C31BAD /* VideoTimelineViewTests.swift */,
135 | E2D7254A242E1E5A00C31BAD /* Info.plist */,
136 | );
137 | path = VideoTimelineViewTests;
138 | sourceTree = "";
139 | };
140 | E2D72552242E1E5A00C31BAD /* VideoTimelineViewUITests */ = {
141 | isa = PBXGroup;
142 | children = (
143 | E2D72553242E1E5A00C31BAD /* VideoTimelineViewUITests.swift */,
144 | E2D72555242E1E5A00C31BAD /* Info.plist */,
145 | );
146 | path = VideoTimelineViewUITests;
147 | sourceTree = "";
148 | };
149 | E2D72561242E211F00C31BAD /* VideoTimelineView */ = {
150 | isa = PBXGroup;
151 | children = (
152 | E29DE0E8242F19CF0046D62C /* VideoTimelineView.swift */,
153 | E2D72564242E21EC00C31BAD /* TimelineView.swift */,
154 | E2D72568242E223E00C31BAD /* TimelineScroller.swift */,
155 | E2D72566242E223D00C31BAD /* CenterLine.swift */,
156 | E2D7256A242E223E00C31BAD /* FrameImagesView.swift */,
157 | E2D72567242E223E00C31BAD /* TimelineMeasure.swift */,
158 | E2D72569242E223E00C31BAD /* TrimView.swift */,
159 | );
160 | path = VideoTimelineView;
161 | sourceTree = "";
162 | };
163 | /* End PBXGroup section */
164 |
165 | /* Begin PBXNativeTarget section */
166 | E2D7252D242E1E4F00C31BAD /* VideoTimelineView */ = {
167 | isa = PBXNativeTarget;
168 | buildConfigurationList = E2D72558242E1E5B00C31BAD /* Build configuration list for PBXNativeTarget "VideoTimelineView" */;
169 | buildPhases = (
170 | E2D7252A242E1E4F00C31BAD /* Sources */,
171 | E2D7252B242E1E4F00C31BAD /* Frameworks */,
172 | E2D7252C242E1E4F00C31BAD /* Resources */,
173 | );
174 | buildRules = (
175 | );
176 | dependencies = (
177 | );
178 | name = VideoTimelineView;
179 | productName = VideoTimelineView;
180 | productReference = E2D7252E242E1E4F00C31BAD /* VideoTimelineView.app */;
181 | productType = "com.apple.product-type.application";
182 | };
183 | E2D72543242E1E5A00C31BAD /* VideoTimelineViewTests */ = {
184 | isa = PBXNativeTarget;
185 | buildConfigurationList = E2D7255B242E1E5B00C31BAD /* Build configuration list for PBXNativeTarget "VideoTimelineViewTests" */;
186 | buildPhases = (
187 | E2D72540242E1E5A00C31BAD /* Sources */,
188 | E2D72541242E1E5A00C31BAD /* Frameworks */,
189 | E2D72542242E1E5A00C31BAD /* Resources */,
190 | );
191 | buildRules = (
192 | );
193 | dependencies = (
194 | E2D72546242E1E5A00C31BAD /* PBXTargetDependency */,
195 | );
196 | name = VideoTimelineViewTests;
197 | productName = VideoTimelineViewTests;
198 | productReference = E2D72544242E1E5A00C31BAD /* VideoTimelineViewTests.xctest */;
199 | productType = "com.apple.product-type.bundle.unit-test";
200 | };
201 | E2D7254E242E1E5A00C31BAD /* VideoTimelineViewUITests */ = {
202 | isa = PBXNativeTarget;
203 | buildConfigurationList = E2D7255E242E1E5B00C31BAD /* Build configuration list for PBXNativeTarget "VideoTimelineViewUITests" */;
204 | buildPhases = (
205 | E2D7254B242E1E5A00C31BAD /* Sources */,
206 | E2D7254C242E1E5A00C31BAD /* Frameworks */,
207 | E2D7254D242E1E5A00C31BAD /* Resources */,
208 | );
209 | buildRules = (
210 | );
211 | dependencies = (
212 | E2D72551242E1E5A00C31BAD /* PBXTargetDependency */,
213 | );
214 | name = VideoTimelineViewUITests;
215 | productName = VideoTimelineViewUITests;
216 | productReference = E2D7254F242E1E5A00C31BAD /* VideoTimelineViewUITests.xctest */;
217 | productType = "com.apple.product-type.bundle.ui-testing";
218 | };
219 | /* End PBXNativeTarget section */
220 |
221 | /* Begin PBXProject section */
222 | E2D72526242E1E4F00C31BAD /* Project object */ = {
223 | isa = PBXProject;
224 | attributes = {
225 | LastSwiftUpdateCheck = 1130;
226 | LastUpgradeCheck = 1130;
227 | ORGANIZATIONNAME = Tom;
228 | TargetAttributes = {
229 | E2D7252D242E1E4F00C31BAD = {
230 | CreatedOnToolsVersion = 11.3.1;
231 | };
232 | E2D72543242E1E5A00C31BAD = {
233 | CreatedOnToolsVersion = 11.3.1;
234 | TestTargetID = E2D7252D242E1E4F00C31BAD;
235 | };
236 | E2D7254E242E1E5A00C31BAD = {
237 | CreatedOnToolsVersion = 11.3.1;
238 | TestTargetID = E2D7252D242E1E4F00C31BAD;
239 | };
240 | };
241 | };
242 | buildConfigurationList = E2D72529242E1E4F00C31BAD /* Build configuration list for PBXProject "VideoTimelineView" */;
243 | compatibilityVersion = "Xcode 9.3";
244 | developmentRegion = en;
245 | hasScannedForEncodings = 0;
246 | knownRegions = (
247 | en,
248 | Base,
249 | );
250 | mainGroup = E2D72525242E1E4F00C31BAD;
251 | productRefGroup = E2D7252F242E1E4F00C31BAD /* Products */;
252 | projectDirPath = "";
253 | projectRoot = "";
254 | targets = (
255 | E2D7252D242E1E4F00C31BAD /* VideoTimelineView */,
256 | E2D72543242E1E5A00C31BAD /* VideoTimelineViewTests */,
257 | E2D7254E242E1E5A00C31BAD /* VideoTimelineViewUITests */,
258 | );
259 | };
260 | /* End PBXProject section */
261 |
262 | /* Begin PBXResourcesBuildPhase section */
263 | E2D7252C242E1E4F00C31BAD /* Resources */ = {
264 | isa = PBXResourcesBuildPhase;
265 | buildActionMask = 2147483647;
266 | files = (
267 | E2D7253E242E1E5A00C31BAD /* LaunchScreen.storyboard in Resources */,
268 | E2C43F1A24346FDF00C03ED5 /* movie.mov in Resources */,
269 | E2D7253B242E1E5A00C31BAD /* Assets.xcassets in Resources */,
270 | E2D72539242E1E4F00C31BAD /* Main.storyboard in Resources */,
271 | );
272 | runOnlyForDeploymentPostprocessing = 0;
273 | };
274 | E2D72542242E1E5A00C31BAD /* Resources */ = {
275 | isa = PBXResourcesBuildPhase;
276 | buildActionMask = 2147483647;
277 | files = (
278 | );
279 | runOnlyForDeploymentPostprocessing = 0;
280 | };
281 | E2D7254D242E1E5A00C31BAD /* Resources */ = {
282 | isa = PBXResourcesBuildPhase;
283 | buildActionMask = 2147483647;
284 | files = (
285 | );
286 | runOnlyForDeploymentPostprocessing = 0;
287 | };
288 | /* End PBXResourcesBuildPhase section */
289 |
290 | /* Begin PBXSourcesBuildPhase section */
291 | E2D7252A242E1E4F00C31BAD /* Sources */ = {
292 | isa = PBXSourcesBuildPhase;
293 | buildActionMask = 2147483647;
294 | files = (
295 | E2D72536242E1E4F00C31BAD /* ViewController.swift in Sources */,
296 | E2D7256E242E223E00C31BAD /* TrimView.swift in Sources */,
297 | E2D7256F242E223E00C31BAD /* FrameImagesView.swift in Sources */,
298 | E2D7256B242E223E00C31BAD /* CenterLine.swift in Sources */,
299 | E2D72532242E1E4F00C31BAD /* AppDelegate.swift in Sources */,
300 | E2D7256C242E223E00C31BAD /* TimelineMeasure.swift in Sources */,
301 | E2D72565242E21EC00C31BAD /* TimelineView.swift in Sources */,
302 | E2D7256D242E223E00C31BAD /* TimelineScroller.swift in Sources */,
303 | E2D72534242E1E4F00C31BAD /* SceneDelegate.swift in Sources */,
304 | E29DE0E9242F19CF0046D62C /* VideoTimelineView.swift in Sources */,
305 | );
306 | runOnlyForDeploymentPostprocessing = 0;
307 | };
308 | E2D72540242E1E5A00C31BAD /* Sources */ = {
309 | isa = PBXSourcesBuildPhase;
310 | buildActionMask = 2147483647;
311 | files = (
312 | E2D72549242E1E5A00C31BAD /* VideoTimelineViewTests.swift in Sources */,
313 | );
314 | runOnlyForDeploymentPostprocessing = 0;
315 | };
316 | E2D7254B242E1E5A00C31BAD /* Sources */ = {
317 | isa = PBXSourcesBuildPhase;
318 | buildActionMask = 2147483647;
319 | files = (
320 | E2D72554242E1E5A00C31BAD /* VideoTimelineViewUITests.swift in Sources */,
321 | );
322 | runOnlyForDeploymentPostprocessing = 0;
323 | };
324 | /* End PBXSourcesBuildPhase section */
325 |
326 | /* Begin PBXTargetDependency section */
327 | E2D72546242E1E5A00C31BAD /* PBXTargetDependency */ = {
328 | isa = PBXTargetDependency;
329 | target = E2D7252D242E1E4F00C31BAD /* VideoTimelineView */;
330 | targetProxy = E2D72545242E1E5A00C31BAD /* PBXContainerItemProxy */;
331 | };
332 | E2D72551242E1E5A00C31BAD /* PBXTargetDependency */ = {
333 | isa = PBXTargetDependency;
334 | target = E2D7252D242E1E4F00C31BAD /* VideoTimelineView */;
335 | targetProxy = E2D72550242E1E5A00C31BAD /* PBXContainerItemProxy */;
336 | };
337 | /* End PBXTargetDependency section */
338 |
339 | /* Begin PBXVariantGroup section */
340 | E2D72537242E1E4F00C31BAD /* Main.storyboard */ = {
341 | isa = PBXVariantGroup;
342 | children = (
343 | E2D72538242E1E4F00C31BAD /* Base */,
344 | );
345 | name = Main.storyboard;
346 | sourceTree = "";
347 | };
348 | E2D7253C242E1E5A00C31BAD /* LaunchScreen.storyboard */ = {
349 | isa = PBXVariantGroup;
350 | children = (
351 | E2D7253D242E1E5A00C31BAD /* Base */,
352 | );
353 | name = LaunchScreen.storyboard;
354 | sourceTree = "";
355 | };
356 | /* End PBXVariantGroup section */
357 |
358 | /* Begin XCBuildConfiguration section */
359 | E2D72556242E1E5A00C31BAD /* Debug */ = {
360 | isa = XCBuildConfiguration;
361 | buildSettings = {
362 | ALWAYS_SEARCH_USER_PATHS = NO;
363 | CLANG_ANALYZER_NONNULL = YES;
364 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
365 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
366 | CLANG_CXX_LIBRARY = "libc++";
367 | CLANG_ENABLE_MODULES = YES;
368 | CLANG_ENABLE_OBJC_ARC = YES;
369 | CLANG_ENABLE_OBJC_WEAK = YES;
370 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
371 | CLANG_WARN_BOOL_CONVERSION = YES;
372 | CLANG_WARN_COMMA = YES;
373 | CLANG_WARN_CONSTANT_CONVERSION = YES;
374 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
375 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
376 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
377 | CLANG_WARN_EMPTY_BODY = YES;
378 | CLANG_WARN_ENUM_CONVERSION = YES;
379 | CLANG_WARN_INFINITE_RECURSION = YES;
380 | CLANG_WARN_INT_CONVERSION = YES;
381 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
382 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
383 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
384 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
385 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
386 | CLANG_WARN_STRICT_PROTOTYPES = YES;
387 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
388 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
389 | CLANG_WARN_UNREACHABLE_CODE = YES;
390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
391 | COPY_PHASE_STRIP = NO;
392 | DEBUG_INFORMATION_FORMAT = dwarf;
393 | ENABLE_STRICT_OBJC_MSGSEND = YES;
394 | ENABLE_TESTABILITY = YES;
395 | GCC_C_LANGUAGE_STANDARD = gnu11;
396 | GCC_DYNAMIC_NO_PIC = NO;
397 | GCC_NO_COMMON_BLOCKS = YES;
398 | GCC_OPTIMIZATION_LEVEL = 0;
399 | GCC_PREPROCESSOR_DEFINITIONS = (
400 | "DEBUG=1",
401 | "$(inherited)",
402 | );
403 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
404 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
405 | GCC_WARN_UNDECLARED_SELECTOR = YES;
406 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
407 | GCC_WARN_UNUSED_FUNCTION = YES;
408 | GCC_WARN_UNUSED_VARIABLE = YES;
409 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
410 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
411 | MTL_FAST_MATH = YES;
412 | ONLY_ACTIVE_ARCH = YES;
413 | SDKROOT = iphoneos;
414 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
415 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
416 | };
417 | name = Debug;
418 | };
419 | E2D72557242E1E5A00C31BAD /* Release */ = {
420 | isa = XCBuildConfiguration;
421 | buildSettings = {
422 | ALWAYS_SEARCH_USER_PATHS = NO;
423 | CLANG_ANALYZER_NONNULL = YES;
424 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
425 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
426 | CLANG_CXX_LIBRARY = "libc++";
427 | CLANG_ENABLE_MODULES = YES;
428 | CLANG_ENABLE_OBJC_ARC = YES;
429 | CLANG_ENABLE_OBJC_WEAK = YES;
430 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
431 | CLANG_WARN_BOOL_CONVERSION = YES;
432 | CLANG_WARN_COMMA = YES;
433 | CLANG_WARN_CONSTANT_CONVERSION = YES;
434 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
435 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
436 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
437 | CLANG_WARN_EMPTY_BODY = YES;
438 | CLANG_WARN_ENUM_CONVERSION = YES;
439 | CLANG_WARN_INFINITE_RECURSION = YES;
440 | CLANG_WARN_INT_CONVERSION = YES;
441 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
442 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
443 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
444 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
445 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
446 | CLANG_WARN_STRICT_PROTOTYPES = YES;
447 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
448 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
449 | CLANG_WARN_UNREACHABLE_CODE = YES;
450 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
451 | COPY_PHASE_STRIP = NO;
452 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
453 | ENABLE_NS_ASSERTIONS = NO;
454 | ENABLE_STRICT_OBJC_MSGSEND = YES;
455 | GCC_C_LANGUAGE_STANDARD = gnu11;
456 | GCC_NO_COMMON_BLOCKS = YES;
457 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
458 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
459 | GCC_WARN_UNDECLARED_SELECTOR = YES;
460 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
461 | GCC_WARN_UNUSED_FUNCTION = YES;
462 | GCC_WARN_UNUSED_VARIABLE = YES;
463 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
464 | MTL_ENABLE_DEBUG_INFO = NO;
465 | MTL_FAST_MATH = YES;
466 | SDKROOT = iphoneos;
467 | SWIFT_COMPILATION_MODE = wholemodule;
468 | SWIFT_OPTIMIZATION_LEVEL = "-O";
469 | VALIDATE_PRODUCT = YES;
470 | };
471 | name = Release;
472 | };
473 | E2D72559242E1E5B00C31BAD /* Debug */ = {
474 | isa = XCBuildConfiguration;
475 | buildSettings = {
476 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
477 | CODE_SIGN_STYLE = Automatic;
478 | DEVELOPMENT_TEAM = TS6L5G84J8;
479 | INFOPLIST_FILE = VideoTimelineView/Info.plist;
480 | LD_RUNPATH_SEARCH_PATHS = (
481 | "$(inherited)",
482 | "@executable_path/Frameworks",
483 | );
484 | PRODUCT_BUNDLE_IDENTIFIER = com.tomo.VideoTimelineView;
485 | PRODUCT_NAME = "$(TARGET_NAME)";
486 | SWIFT_VERSION = 5.0;
487 | TARGETED_DEVICE_FAMILY = "1,2";
488 | };
489 | name = Debug;
490 | };
491 | E2D7255A242E1E5B00C31BAD /* Release */ = {
492 | isa = XCBuildConfiguration;
493 | buildSettings = {
494 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
495 | CODE_SIGN_STYLE = Automatic;
496 | DEVELOPMENT_TEAM = TS6L5G84J8;
497 | INFOPLIST_FILE = VideoTimelineView/Info.plist;
498 | LD_RUNPATH_SEARCH_PATHS = (
499 | "$(inherited)",
500 | "@executable_path/Frameworks",
501 | );
502 | PRODUCT_BUNDLE_IDENTIFIER = com.tomo.VideoTimelineView;
503 | PRODUCT_NAME = "$(TARGET_NAME)";
504 | SWIFT_VERSION = 5.0;
505 | TARGETED_DEVICE_FAMILY = "1,2";
506 | };
507 | name = Release;
508 | };
509 | E2D7255C242E1E5B00C31BAD /* Debug */ = {
510 | isa = XCBuildConfiguration;
511 | buildSettings = {
512 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
513 | BUNDLE_LOADER = "$(TEST_HOST)";
514 | CODE_SIGN_STYLE = Automatic;
515 | DEVELOPMENT_TEAM = TS6L5G84J8;
516 | INFOPLIST_FILE = VideoTimelineViewTests/Info.plist;
517 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
518 | LD_RUNPATH_SEARCH_PATHS = (
519 | "$(inherited)",
520 | "@executable_path/Frameworks",
521 | "@loader_path/Frameworks",
522 | );
523 | PRODUCT_BUNDLE_IDENTIFIER = com.tomo.VideoTimelineViewTests;
524 | PRODUCT_NAME = "$(TARGET_NAME)";
525 | SWIFT_VERSION = 5.0;
526 | TARGETED_DEVICE_FAMILY = "1,2";
527 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VideoTimelineView.app/VideoTimelineView";
528 | };
529 | name = Debug;
530 | };
531 | E2D7255D242E1E5B00C31BAD /* Release */ = {
532 | isa = XCBuildConfiguration;
533 | buildSettings = {
534 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
535 | BUNDLE_LOADER = "$(TEST_HOST)";
536 | CODE_SIGN_STYLE = Automatic;
537 | DEVELOPMENT_TEAM = TS6L5G84J8;
538 | INFOPLIST_FILE = VideoTimelineViewTests/Info.plist;
539 | IPHONEOS_DEPLOYMENT_TARGET = 13.2;
540 | LD_RUNPATH_SEARCH_PATHS = (
541 | "$(inherited)",
542 | "@executable_path/Frameworks",
543 | "@loader_path/Frameworks",
544 | );
545 | PRODUCT_BUNDLE_IDENTIFIER = com.tomo.VideoTimelineViewTests;
546 | PRODUCT_NAME = "$(TARGET_NAME)";
547 | SWIFT_VERSION = 5.0;
548 | TARGETED_DEVICE_FAMILY = "1,2";
549 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/VideoTimelineView.app/VideoTimelineView";
550 | };
551 | name = Release;
552 | };
553 | E2D7255F242E1E5B00C31BAD /* Debug */ = {
554 | isa = XCBuildConfiguration;
555 | buildSettings = {
556 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
557 | CODE_SIGN_STYLE = Automatic;
558 | DEVELOPMENT_TEAM = TS6L5G84J8;
559 | INFOPLIST_FILE = VideoTimelineViewUITests/Info.plist;
560 | LD_RUNPATH_SEARCH_PATHS = (
561 | "$(inherited)",
562 | "@executable_path/Frameworks",
563 | "@loader_path/Frameworks",
564 | );
565 | PRODUCT_BUNDLE_IDENTIFIER = com.tomo.VideoTimelineViewUITests;
566 | PRODUCT_NAME = "$(TARGET_NAME)";
567 | SWIFT_VERSION = 5.0;
568 | TARGETED_DEVICE_FAMILY = "1,2";
569 | TEST_TARGET_NAME = VideoTimelineView;
570 | };
571 | name = Debug;
572 | };
573 | E2D72560242E1E5B00C31BAD /* Release */ = {
574 | isa = XCBuildConfiguration;
575 | buildSettings = {
576 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
577 | CODE_SIGN_STYLE = Automatic;
578 | DEVELOPMENT_TEAM = TS6L5G84J8;
579 | INFOPLIST_FILE = VideoTimelineViewUITests/Info.plist;
580 | LD_RUNPATH_SEARCH_PATHS = (
581 | "$(inherited)",
582 | "@executable_path/Frameworks",
583 | "@loader_path/Frameworks",
584 | );
585 | PRODUCT_BUNDLE_IDENTIFIER = com.tomo.VideoTimelineViewUITests;
586 | PRODUCT_NAME = "$(TARGET_NAME)";
587 | SWIFT_VERSION = 5.0;
588 | TARGETED_DEVICE_FAMILY = "1,2";
589 | TEST_TARGET_NAME = VideoTimelineView;
590 | };
591 | name = Release;
592 | };
593 | /* End XCBuildConfiguration section */
594 |
595 | /* Begin XCConfigurationList section */
596 | E2D72529242E1E4F00C31BAD /* Build configuration list for PBXProject "VideoTimelineView" */ = {
597 | isa = XCConfigurationList;
598 | buildConfigurations = (
599 | E2D72556242E1E5A00C31BAD /* Debug */,
600 | E2D72557242E1E5A00C31BAD /* Release */,
601 | );
602 | defaultConfigurationIsVisible = 0;
603 | defaultConfigurationName = Release;
604 | };
605 | E2D72558242E1E5B00C31BAD /* Build configuration list for PBXNativeTarget "VideoTimelineView" */ = {
606 | isa = XCConfigurationList;
607 | buildConfigurations = (
608 | E2D72559242E1E5B00C31BAD /* Debug */,
609 | E2D7255A242E1E5B00C31BAD /* Release */,
610 | );
611 | defaultConfigurationIsVisible = 0;
612 | defaultConfigurationName = Release;
613 | };
614 | E2D7255B242E1E5B00C31BAD /* Build configuration list for PBXNativeTarget "VideoTimelineViewTests" */ = {
615 | isa = XCConfigurationList;
616 | buildConfigurations = (
617 | E2D7255C242E1E5B00C31BAD /* Debug */,
618 | E2D7255D242E1E5B00C31BAD /* Release */,
619 | );
620 | defaultConfigurationIsVisible = 0;
621 | defaultConfigurationName = Release;
622 | };
623 | E2D7255E242E1E5B00C31BAD /* Build configuration list for PBXNativeTarget "VideoTimelineViewUITests" */ = {
624 | isa = XCConfigurationList;
625 | buildConfigurations = (
626 | E2D7255F242E1E5B00C31BAD /* Debug */,
627 | E2D72560242E1E5B00C31BAD /* Release */,
628 | );
629 | defaultConfigurationIsVisible = 0;
630 | defaultConfigurationName = Release;
631 | };
632 | /* End XCConfigurationList section */
633 | };
634 | rootObject = E2D72526242E1E4F00C31BAD /* Project object */;
635 | }
636 |
--------------------------------------------------------------------------------
/VideoTimelineView/VideoTimelineView/VideoTimelineView/TrimView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TrimView.swift
3 | // Examplay
4 | //
5 | // Created by Tomohiro Yamashita on 2020/03/12.
6 | // Copyright © 2020 Tom. All rights reserved.
7 | //
8 |
9 | import UIKit
10 |
11 | class TrimView: UIView {
12 | var mainView:VideoTimelineView!
13 |
14 | var timelineView:TimelineView!
15 | var parentScroller:TimelineScroller!
16 | var startKnob = TrimKnob()
17 | var endKnob = TrimKnob()
18 | var movieDuration:Float64 = 0
19 |
20 | let canPassThroughEachKnobs = true
21 |
22 | override init (frame: CGRect) {
23 | super.init(frame: frame)
24 | self.isUserInteractionEnabled = false
25 | self.backgroundColor = .clear
26 |
27 | }
28 |
29 | required init(coder aDecoder: NSCoder) {
30 | fatalError("TrimView init(coder:) has not been implemented")
31 | }
32 |
33 | func configure(_ timeline:TimelineView, scroller:TimelineScroller) {
34 | timelineView = timeline
35 | parentScroller = scroller
36 | self.frame = timeline.frame
37 | startKnob.configure(timeline, trimmer:self)
38 | endKnob.configure(timeline, trimmer:self)
39 |
40 | timelineView.addSubview(startKnob)
41 | timelineView.addSubview(endKnob)
42 | }
43 |
44 |
45 | func reset(duration:Float64) {
46 | movieDuration = duration
47 | startKnob.knobTimePoint = 0
48 | endKnob.knobTimePoint = 3
49 | if duration < endKnob.knobTimePoint {
50 | endKnob.knobTimePoint = duration
51 | }
52 | layout()
53 | }
54 |
55 |
56 |
57 | let knobWidth:CGFloat = 20
58 | let knobWidthExtend:CGFloat = 5
59 | func layout() {
60 | if self.isHidden {
61 | return
62 | }
63 |
64 | swapKnobs()
65 |
66 | let knobPositions = knobPositionsAsVisible()
67 | let startPosition = knobPositions.start
68 | let endPosition = knobPositions.end
69 |
70 | startKnob.knobPositionOnScreen = startPosition
71 | endKnob.knobPositionOnScreen = endPosition
72 | startKnob.isOutOfScreen = knobPositions.startFixed
73 | endKnob.isOutOfScreen = knobPositions.endFixed
74 |
75 | startKnob.frame = CGRect(x:startPosition - knobWidth - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height)
76 | endKnob.frame = CGRect(x:endPosition - knobWidthExtend, y:self.frame.origin.y, width:knobWidth + knobWidthExtend * 2, height:self.frame.size.height)
77 |
78 | self.setNeedsDisplay()
79 | }
80 |
81 | //MARK: - draw
82 |
83 | override func draw(_ rect: CGRect) {
84 |
85 |
86 | var startRect = startKnob.frame
87 | var endRect = endKnob.frame
88 | if startRect.size.height <= 0 {
89 | return
90 | }
91 | if animating {
92 | if let layer = startKnob.layer.presentation() {
93 | startRect = layer.frame
94 | }
95 | if let layer = endKnob.layer.presentation() {
96 | endRect = layer.frame
97 | }
98 | }
99 | startRect.origin.x += knobWidthExtend
100 | startRect.size.width -= knobWidthExtend * 2
101 | endRect.origin.x += knobWidthExtend
102 | endRect.size.width -= knobWidthExtend * 2
103 | if startRect.origin.x > endRect.origin.x + endRect.size.width {
104 | let swapRect = startRect
105 | startRect = endRect
106 | endRect = swapRect
107 | }
108 |
109 |
110 | let beamWidth:CGFloat = 3
111 | var outerRect = CGRect(x: startRect.origin.x,y: 0,width: endRect.origin.x + endRect.size.width - startRect.origin.x,height:startRect.size.height)
112 | var innerRect = CGRect(x:startRect.origin.x + startRect.size.width,y:beamWidth,width: endRect.origin.x - startRect.origin.x - startRect.size.width,height:startRect.size.height - (beamWidth * 2))
113 |
114 | let screenLeft = cgToTime(screenToTimelinePosition(0))
115 | let screenRight = cgToTime(screenToTimelinePosition(timelineView.frame.size.width))
116 | var color = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 1)
117 | let outColor = UIColor(hue: 0.1, saturation:0.8, brightness:1, alpha: 0.3)
118 | if (endKnob.knobTimePoint < screenLeft || startKnob.knobTimePoint > screenRight) {
119 | color = outColor
120 | } else {
121 | let addition = knobWidth + 10
122 | let knobWidthTime = cgToTime(knobWidth)
123 | if endKnob.knobTimePoint + knobWidthTime * 0.9 > screenRight {
124 | outerRect.size.width += addition
125 | innerRect.size.width += addition
126 | let outRect = CGRect(x: timelineView.frame.size.width - knobWidth, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2)
127 | let path = UIBezierPath(rect:outRect)
128 | outColor.setFill()
129 | path.fill()
130 | }
131 | if startKnob.knobTimePoint - knobWidthTime * 0.9 < screenLeft {
132 | outerRect.origin.x -= addition
133 | innerRect.origin.x -= addition
134 | outerRect.size.width += addition
135 | innerRect.size.width += addition
136 | let outRect = CGRect(x: 0, y: beamWidth,width: knobWidth, height: endRect.size.height - beamWidth * 2)
137 | let path = UIBezierPath(rect:outRect)
138 | outColor.setFill()
139 | path.fill()
140 | }
141 | }
142 |
143 | let path = UIBezierPath(roundedRect:outerRect, cornerRadius:5)
144 | path.usesEvenOddFillRule = true
145 | let innerPath = UIBezierPath(roundedRect:innerRect, cornerRadius:2)
146 | path.append(innerPath)
147 | color.setFill()
148 | path.fill()
149 | }
150 |
151 |
152 |
153 | //MARK: - timer for animation
154 | var animationTimer = Timer()
155 | var animating = false
156 | func startAnimation() {
157 | animationTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.animate(_:)), userInfo: nil, repeats: true)
158 | RunLoop.main.add(animationTimer, forMode:RunLoop.Mode.common)
159 | animating = true
160 | }
161 |
162 | func stopAnimation() {
163 | animating = false
164 | self.setNeedsDisplay()
165 | animationTimer.invalidate()
166 | }
167 |
168 | @objc func animate(_ timer:Timer) {
169 | if animating == false {
170 | return
171 | }
172 | self.setNeedsDisplay()
173 | }
174 |
175 |
176 | //MARK: - positioning
177 | func swapKnobs() {
178 | if startKnob.knobTimePoint > endKnob.knobTimePoint {
179 | if canPassThroughEachKnobs {
180 | //timelineView.swapTrimKnobs()
181 | } else {
182 | let start = endKnob.knobTimePoint
183 | endKnob.knobTimePoint = startKnob.knobTimePoint
184 | startKnob.knobTimePoint = start
185 | }
186 | }
187 | }
188 |
189 | func anotherKnob(_ knob:TrimKnob) -> TrimKnob {
190 | if knob == startKnob {
191 | return endKnob
192 | }
193 | return startKnob
194 | }
195 |
196 | func knobOnScreen(_ knob:TrimKnob) -> CGFloat {
197 | let maxWidth = scrollMaxWidth()
198 | let offset = scrollOffset()
199 | let position = CGFloat(knob.knobTimePoint / movieDuration) * maxWidth
200 | return position - offset + (timelineView.frame.size.width / 2)
201 | }
202 |
203 | func knobsMinDistanceTime() -> Float64 {
204 | return Float64(0.1)
205 | }
206 |
207 | func knobsMinDistanceFloat() -> CGFloat {
208 | return timeToCG(knobsMinDistanceTime())
209 | }
210 |
211 | func timeToCG(_ time:Float64) -> CGFloat {
212 | return CGFloat(time / movieDuration) * scrollMaxWidth()
213 | }
214 |
215 | func cgToTime(_ cgFloat:CGFloat) -> Float64 {
216 | return Float64(cgFloat / scrollMaxWidth()) * movieDuration
217 | }
218 |
219 | func screenToTimelinePosition(_ onScreen:CGFloat) -> CGFloat {
220 | return onScreen + scrollOffset() - (timelineView.frame.size.width / 2)
221 | }
222 |
223 | func timelineToScreenPosition(_ position:CGFloat) -> CGFloat {
224 | return position - scrollOffset() + (timelineView.frame.size.width / 2)
225 | }
226 |
227 | func knobPositionsAsVisible() -> (start:CGFloat, end:CGFloat, startFixed:Bool, endFixed:Bool) {
228 |
229 | let positionStart = knobOnScreen(startKnob)
230 | let positionEnd = knobOnScreen(endKnob)
231 | var resultStart = positionStart
232 | var resultEnd = positionEnd
233 | var startFixed:Bool = false
234 | var endFixed:Bool = false
235 | let screenRight = timelineView.frame.size.width// + offset
236 | let minDistance = knobsMinDistanceFloat()
237 |
238 | if positionStart < knobWidth {
239 | if (positionEnd - minDistance - knobWidth) < 0 {
240 | resultStart = positionEnd - minDistance
241 | } else {
242 | resultStart = knobWidth
243 | }
244 | startFixed = true
245 | } else if positionStart > screenRight {
246 | resultStart = screenRight
247 | startFixed = true
248 | }
249 | if positionEnd < 0 {
250 | resultEnd = 0
251 | endFixed = true
252 | } else if positionEnd + knobWidth > screenRight {
253 | if positionStart + minDistance + knobWidth > screenRight {
254 | resultEnd = positionStart + minDistance
255 | } else {
256 | resultEnd = screenRight - knobWidth
257 | }
258 | }
259 | if true {
260 | let distance = abs(resultStart - resultEnd)
261 | if distance < minDistance {
262 | let value = (minDistance - distance) / 2
263 | resultStart -= value
264 | resultEnd += value
265 | startFixed = true
266 | endFixed = true
267 | }
268 | }
269 | return (resultStart, resultEnd, startFixed, endFixed)
270 | }
271 |
272 | func scrollOffset() -> CGFloat {
273 | return parentScroller.contentOffset.x
274 | }
275 |
276 | func scrollMaxWidth() -> CGFloat {
277 | return parentScroller.frameImagesView.frame.size.width
278 | }
279 |
280 | func knobTimeOnScreen(_ knob:TrimKnob) -> Float64 {
281 | let offset = scrollOffset()
282 | let position = offset + knob.knobPositionOnScreen - (timelineView.frame.size.width / 2)
283 | let maxWidth = scrollMaxWidth()
284 | let time = Float64(position / maxWidth) * movieDuration
285 | return time
286 | }
287 |
288 |
289 | func knobMoveRange(_ knob:TrimKnob) -> (min:Float64, max:Float64) {
290 | var min:Float64 = 0
291 | var max:Float64 = movieDuration
292 | let minDistance = knobsMinDistanceTime()
293 | if canPassThroughEachKnobs {
294 | if knob == startKnob {
295 | max -= minDistance
296 | } else if knob == endKnob {
297 | min += minDistance
298 | }
299 | } else {
300 | if knob == startKnob {
301 | max = endKnob.knobTimePoint - minDistance
302 | } else if knob == endKnob {
303 | min = startKnob.knobTimePoint + minDistance
304 | }
305 | }
306 | return (min, max)
307 | }
308 |
309 | func visibleKnobMoveLimit(_ knob:TrimKnob, margin:CGFloat) -> (min:Bool, max:Bool) {
310 | let range = knobMoveRange(knob)
311 | let minOnScreen = timelineToScreenPosition(timeToCG(range.min))
312 | let maxOnScreen = timelineToScreenPosition(timeToCG(range.max))
313 | var resultMin = false
314 | var resultMax = false
315 | if minOnScreen >= margin && minOnScreen <= timelineView.frame.width - margin {
316 | resultMin = true
317 | }
318 | if maxOnScreen >= margin && maxOnScreen <= timelineView.frame.width - margin {
319 | resultMax = true
320 | }
321 | return (resultMin , resultMax )
322 | }
323 |
324 | func directionReachesEnd(_ knob:TrimKnob, direction:CGFloat) -> Bool {
325 | let visibleLimit = visibleKnobMoveLimit(knob, margin:knobWidth)
326 | if direction > 0 {
327 | if visibleLimit.max {
328 | return true
329 | }
330 | } else if direction < 0 {
331 | if visibleLimit.min {
332 | return true
333 | }
334 | }
335 | return false
336 | }
337 |
338 | func fixKnobPoint(_ knob:TrimKnob, move:CGFloat, startKnobPoint:Float64) -> (knobPoint:Float64, fixed:Bool) {
339 |
340 | var timePoint = startKnobPoint + cgToTime(move)
341 |
342 | let moveRange = knobMoveRange(knob)
343 | var fixed = false
344 | if timePoint < moveRange.min {
345 | timePoint = moveRange.min
346 | fixed = true
347 | }
348 | if timePoint > moveRange.max {
349 | timePoint = moveRange.max
350 | fixed = true
351 | }
352 | return (timePoint, fixed)
353 | }
354 |
355 | func updateKnob(_ knob:TrimKnob, timePoint:Float64) {
356 | knob.knobTimePoint = timePoint
357 | layout()
358 |
359 | timelineView.moved(knob.knobTimePoint)
360 | }
361 |
362 | func resetSeek(_ time:Float64) {
363 | if edgeScrolled {
364 | moveToTimeWithAnimation(time)
365 | } else {
366 | mainView!.accurateSeek(time, scrub:true)
367 | }
368 | edgeScrolled = false
369 | }
370 |
371 | func moveToTimeWithAnimation(_ time:Float64) {
372 | moveToTimeAndTrimWithAnimation(time, trim:nil)
373 | }
374 |
375 | func moveToTimeAndTrimWithAnimation(_ time:Float64, trim:VideoTimelineTrim?) {
376 | let player = mainView!.player
377 | if player == nil {
378 | return
379 | }
380 | if player!.timeControlStatus == .playing {
381 | player!.pause()
382 | }
383 | timelineView.animating = true
384 | mainView!.isUserInteractionEnabled = false
385 | timelineView.startAnimation()
386 |
387 | UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in
388 | if let pinnedTrim = trim {
389 | self.timelineView.setTrim(start:pinnedTrim.start,end:pinnedTrim.end)
390 | }
391 | self.timelineView.setCurrentTime(time,force:false)
392 | },completion: { finished in
393 |
394 | self.mainView!.isUserInteractionEnabled = true
395 | if self.mainView!.playing {
396 | self.mainView!.accurateSeek(time, scrub:false)
397 | player!.play()
398 | } else {
399 | self.mainView!.accurateSeek(time, scrub:true)
400 | }
401 | self.timelineView.animating = false
402 | self.timelineView.setManualScrolledAfterEnd()
403 | self.timelineView.stopAnimation()
404 | if let receiver = self.mainView!.playStatusReceiver {
405 | receiver.videoTimelineMoved()
406 | }
407 | })
408 | }
409 |
410 | //MARK: - edgeScroll
411 | var edgeScrollTimer = Timer()
412 | var edgeScrolled = false
413 | var edgeScrollingKnob:TrimKnob? = nil
414 | var edgeScrollStrength:CGFloat = 0
415 | var edgeScrollingKnobPosition:CGFloat = 0
416 | var edgeScrollLastChangedTime:Date = Date()
417 | var edgeScrollLastChangedPosition:CGFloat = 0
418 |
419 |
420 | func updateEdgeScroll(_ knob:TrimKnob, strength:CGFloat, position:CGFloat) {
421 | var changed = false
422 | if edgeScrollStrength != strength {
423 | edgeScrollStrength = strength
424 | changed = true
425 | }
426 | if edgeScrolling() == false {
427 | startEdgeScroll(knob)
428 | edgeScrolled = true
429 | } else if changed {
430 | edgeScrollLastChangedPosition = scrollOffset()
431 | edgeScrollLastChangedTime = Date()
432 | }
433 | edgeScrollingKnobPosition = position
434 | edgeScrollingKnob = knob
435 | }
436 |
437 | func startEdgeScroll(_ knob:TrimKnob) {
438 | edgeScrollTimer = Timer.scheduledTimer(timeInterval: 0.01, target: self, selector: #selector(self.edgeScrollTimer(_:)), userInfo: nil, repeats: true)
439 | RunLoop.main.add(edgeScrollTimer, forMode:RunLoop.Mode.common)
440 | edgeScrollLastChangedTime = Date()
441 | edgeScrollLastChangedPosition = scrollOffset()
442 | }
443 |
444 | @objc func edgeScrollTimer(_ timer:Timer) {
445 | if let knob = edgeScrollingKnob {
446 | let movedPosition = currentEdgeScrollMovedPosition()
447 | let destination = currentEdgeScrollPosition(movedPosition)
448 | let moveRange = knobMoveRange(knob)
449 | var knobPoint = cgToTime(edgeScrollingKnobPosition - (timelineView.frame.size.width / 2) + destination)
450 |
451 | var overLimit:Float64 = 0
452 | if knobPoint > moveRange.max {
453 | overLimit = knobPoint - moveRange.max
454 | knobPoint = moveRange.max
455 |
456 | } else if knobPoint < moveRange.min {
457 | overLimit = knobPoint - moveRange.min
458 | knobPoint = moveRange.min
459 |
460 | }
461 | updateKnob(knob, timePoint:knobPoint)
462 | if (knob == startKnob && knobPoint > endKnob.knobTimePoint) || (knob == endKnob && knobPoint < startKnob.knobTimePoint) {
463 | timelineView.swapTrimKnobs()
464 | }
465 | timelineView.setCurrentTime(cgToTime(destination) - overLimit,force:true)
466 | }
467 | }
468 |
469 | func stopEdgeScrollTimer() {
470 | edgeScrollTimer.invalidate()
471 | edgeScrollStrength = 0
472 | edgeScrollingKnob = nil
473 | }
474 |
475 | func edgeScrolling() -> Bool {
476 | return edgeScrollTimer.isValid
477 | }
478 |
479 | func currentEdgeScrollPosition(_ moved:CGFloat) -> CGFloat {
480 | var result:CGFloat = 0
481 |
482 | let maxWidth = scrollMaxWidth()
483 | result = edgeScrollLastChangedPosition + moved
484 | if result < 0 {
485 | result = 0
486 | } else if result > maxWidth {
487 | result = maxWidth
488 | }
489 | return result
490 | }
491 |
492 | func currentEdgeScrollMovedPosition() -> CGFloat {
493 | let pastTime = -edgeScrollLastChangedTime.timeIntervalSinceNow
494 | return CGFloat(pastTime) * edgeScrollStrength * 5
495 | }
496 | }
497 |
498 |
499 | //MARK: - TrimKnob
500 | class TrimKnob:UIView {
501 | var timelineView:TimelineView!
502 | var knobPositionOnScreen:CGFloat = 0
503 | var trimView:TrimView!
504 | var knobTimePoint:Float64 = 0
505 | var isOutOfScreen:Bool = false
506 |
507 |
508 | override init (frame: CGRect) {
509 | super.init(frame: frame)
510 |
511 | self.isMultipleTouchEnabled = true
512 | self.isUserInteractionEnabled = true
513 |
514 | }
515 |
516 | required init(coder aDecoder: NSCoder) {
517 | fatalError("TrimKnob init(coder:) has not been implemented")
518 | }
519 |
520 | func configure(_ timeline:TimelineView, trimmer:TrimView) {
521 | timelineView = timeline
522 | trimView = trimmer
523 | }
524 |
525 | //MARK: - Touch Events
526 | var allTouches = [UITouch]()
527 |
528 | override open func touchesBegan(_ touches: Set, with event: UIEvent?)
529 | {
530 | for touch in touches {
531 | if !allTouches.contains(touch) {
532 | allTouches += [touch]
533 | }
534 | if !timelineView!.allTouches.contains(touch) {
535 | timelineView!.allTouches += [touch]
536 | }
537 | }
538 | if timelineView!.allTouches.count == 1 && allTouches.count == 1 {
539 | if dragging == false {
540 | startDrag()
541 | }
542 | evaluateTap = true
543 | } else {
544 | evaluateTap = false
545 | }
546 | }
547 |
548 |
549 | override open func touchesMoved(_ touches: Set, with event: UIEvent?)
550 | {
551 | if timelineView!.allTouches.count > 1 {
552 | if dragging {
553 | cancelDrag()
554 | }
555 | }
556 | if timelineView!.allTouches.count == 2 {
557 | if timelineView!.pinching {
558 | timelineView!.updatePinch()
559 | } else {
560 | timelineView!.startPinch()
561 | }
562 | }
563 | if dragging && timelineView!.allTouches.count == 1 && allTouches.count == 1 {
564 | updateDrag()
565 | }
566 | evaluateTap = false
567 | }
568 |
569 | override open func touchesEnded(_ touches: Set, with event: UIEvent?)
570 | {
571 | for touch in touches {
572 | if let index = allTouches.firstIndex(of:touch) {
573 | allTouches.remove(at: index)
574 | }
575 | if let index = timelineView!.allTouches.firstIndex(of:touch) {
576 | timelineView!.allTouches.remove(at: index)
577 | }
578 | }
579 | if timelineView!.pinching && timelineView!.allTouches.count < 2 {
580 | timelineView!.endPinch()
581 | }
582 | if dragging {
583 | endDrag()
584 | }
585 |
586 | if evaluateTap && timelineView!.allTouches.count == 0 {
587 | tapped()
588 | }
589 | evaluateTap = false
590 | }
591 |
592 | override open func touchesCancelled(_ touches: Set, with event: UIEvent?)
593 | {
594 | for touch in touches {
595 | if let index = allTouches.firstIndex(of:touch) {
596 | allTouches.remove(at: index)
597 | }
598 | if let index = timelineView!.allTouches.firstIndex(of:touch) {
599 | timelineView!.allTouches.remove(at: index)
600 | }
601 | }
602 |
603 | if timelineView!.pinching {
604 | timelineView!.endPinch()
605 | }
606 | if dragging {
607 | endDrag()
608 | }
609 | evaluateTap = false
610 | }
611 |
612 |
613 | //MARK: - actions
614 |
615 | var dragging:Bool = false
616 | var dragStartPoint = CGPoint.zero
617 | var startKnobTimePoint:Float64 = 0
618 | var dragStartOffset:CGFloat = 0
619 | var startTimeOutOfScreen:Float64 = 0
620 | var scrolling = false
621 | var startCurrentTime:Float64 = 0
622 | var evaluateTap:Bool = false
623 | var ignoreEdgeScroll = false
624 |
625 | func startDrag() {
626 |
627 | dragging = true
628 | let touch = allTouches[0]
629 | dragStartPoint = touch.location(in: timelineView)
630 | startKnobTimePoint = knobTimePoint
631 | dragStartOffset = trimView.scrollOffset()
632 | startTimeOutOfScreen = trimView.knobTimeOnScreen(self) - startKnobTimePoint
633 |
634 |
635 | startCurrentTime = timelineView.mainView!.currentTime
636 |
637 | if edgeScrollStrength(dragStartPoint.x) != 0 {
638 | ignoreEdgeScroll = true
639 | } else {
640 | ignoreEdgeScroll = false
641 | }
642 | }
643 |
644 | func updateDrag() {
645 | let touch = allTouches[0]
646 | let currentPoint = touch.location(in: timelineView)
647 | let scrolled = trimView.scrollOffset() - dragStartOffset
648 | let move = currentPoint.x - dragStartPoint.x + scrolled
649 | let startKnobPoint = startKnobTimePoint + startTimeOutOfScreen
650 |
651 | let timePoint = startKnobPoint + trimView.cgToTime(move)
652 | let dragPoint = trimView.cgToTime(trimView.screenToTimelinePosition(currentPoint.x))
653 | let onKnob = trimView.timeToCG(timePoint - dragPoint)
654 | let anotherKnob = trimView.anotherKnob(self)
655 | let anotherTimePoint = anotherKnob.knobTimePoint
656 | let knobWidth = trimView.knobWidth
657 | var swapping:CGFloat = 0
658 |
659 |
660 | if anotherTimePoint < timePoint && startKnobPoint < anotherTimePoint
661 | {
662 | swapping = trimView.timeToCG(anotherTimePoint - timePoint)
663 | if -swapping > knobWidth {
664 | swapping = -knobWidth
665 | }
666 | } else if anotherTimePoint > timePoint && startKnobPoint > anotherTimePoint
667 | {
668 | swapping = trimView.timeToCG(anotherTimePoint - timePoint)
669 | if swapping > knobWidth {
670 | swapping = knobWidth
671 | }
672 | }
673 | var rangeOut = false
674 | let range = trimView.knobMoveRange(self)
675 | if timePoint > range.max || timePoint < range.min {
676 | rangeOut = true
677 | }
678 |
679 | if rangeOut == false && ((self == trimView.startKnob && dragPoint > anotherTimePoint) || (self == trimView.endKnob && dragPoint < anotherTimePoint)) {
680 | timelineView.swapTrimKnobs()
681 | }
682 |
683 |
684 | let strength = edgeScrollStrength(currentPoint.x)
685 | let reachedEnd = trimView.directionReachesEnd(self, direction:strength)
686 | if strength != 0 && ignoreEdgeScroll == false && reachedEnd == false {
687 | trimView.updateEdgeScroll(self, strength:strength, position:currentPoint.x + onKnob + swapping)
688 | } else {
689 | let fixedKnobPoint = trimView.fixKnobPoint(self, move:move + swapping, startKnobPoint:startKnobPoint)
690 |
691 | trimView.updateKnob(self, timePoint:fixedKnobPoint.knobPoint)
692 | if strength == 0 {
693 | ignoreEdgeScroll = false
694 |
695 | }
696 | if strength == 0 || reachedEnd {
697 | if trimView.edgeScrolling() {
698 | trimView.stopEdgeScrollTimer()
699 | }
700 | }
701 | }
702 |
703 | guard let mainView = (timelineView.mainView) else { return }
704 | if let receiver = mainView.playStatusReceiver {
705 | receiver.videoTimelineTrimChanged()
706 | }
707 | }
708 |
709 | func endDrag() {
710 | let anotherKnob = trimView.anotherKnob(self)
711 | let distance = abs(anotherKnob.knobTimePoint - knobTimePoint)
712 | let minDistance = trimView.knobsMinDistanceTime()
713 | if distance < minDistance {
714 | if self == trimView.startKnob {
715 | knobTimePoint -= (minDistance - distance)
716 | } else if self == trimView.endKnob {
717 | knobTimePoint += (minDistance - distance)
718 | }
719 |
720 | trimView.timelineView.animating = true
721 | trimView.startAnimation()
722 | UIView.animate(withDuration: 0.2,delay:Double(0.0),options:UIView.AnimationOptions.curveEaseOut, animations: { () -> Void in
723 |
724 | self.trimView.layout()
725 |
726 | },completion: { finished in
727 | self.trimView.stopAnimation()
728 | self.timelineView.animating = false
729 | })
730 | }
731 | timelineView.setManualScrolledAfterEnd()
732 | trimView.resetSeek(startCurrentTime)
733 | dragging = false
734 | trimView.stopEdgeScrollTimer()
735 |
736 | if evaluateTap == false {
737 | guard let mainView = (timelineView.mainView) else { return }
738 | if let receiver = mainView.playStatusReceiver {
739 | receiver.videoTimelineTrimChanged()
740 | }
741 | }
742 | }
743 |
744 | func cancelDrag() {
745 | knobTimePoint = startKnobTimePoint
746 |
747 | timelineView.setManualScrolledAfterEnd()
748 | dragging = false
749 | trimView.layout()
750 | trimView.resetSeek(startCurrentTime)
751 | trimView.stopEdgeScrollTimer()
752 | }
753 |
754 | func tapped() {
755 | trimView.moveToTimeWithAnimation(knobTimePoint)
756 | timelineView.setManualScrolledAfterEnd()
757 | }
758 |
759 |
760 | func edgeScrollStrength(_ position:CGFloat) -> CGFloat {
761 | var strength:CGFloat = 0
762 | let edgeWidth:CGFloat = 40
763 | if position >= timelineView.frame.size.width - edgeWidth {
764 | strength = position + edgeWidth - timelineView.frame.size.width
765 | } else if position <= edgeWidth {
766 | strength = position - edgeWidth
767 | }
768 | return strength
769 | }
770 |
771 |
772 |
773 | }
774 |
--------------------------------------------------------------------------------