├── .gitignore ├── LICENSE ├── README.md ├── VITimelineView.podspec ├── VITimelineViewDemo.xcodeproj ├── project.pbxproj └── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ └── IDEWorkspaceChecks.plist └── VITimelineViewDemo ├── AppDelegate.h ├── AppDelegate.m ├── Assets.xcassets ├── AppIcon.appiconset │ └── Contents.json └── Contents.json ├── Base.lproj ├── LaunchScreen.storyboard └── Main.storyboard ├── Info.plist ├── Source ├── Convenient │ ├── VIRangeView+Creator.h │ ├── VIRangeView+Creator.m │ ├── VITimelineView+Creator.h │ └── VITimelineView+Creator.m ├── DataSource │ ├── VIRangeContentAssetImageDataSource.h │ └── VIRangeContentAssetImageDataSource.m ├── Utils │ ├── CachedAssetImageGenerator.h │ ├── CachedAssetImageGenerator.m │ ├── UIView+ConstraintHolder.h │ ├── VIAutoScroller.h │ ├── VIAutoScroller.m │ ├── VIDisplayTriggerMachine.h │ └── VIDisplayTriggerMachine.m ├── VIRangeContentView.h ├── VIRangeContentView.m ├── VIRangeEarView.h ├── VIRangeEarView.m ├── VIRangeView.h ├── VIRangeView.m ├── VITimelineView.h ├── VITimelineView.m ├── VIVideoRangeContentView.h └── VIVideoRangeContentView.m ├── ViewController.h ├── ViewController.m ├── bamboo.mp4 ├── main.m └── water.mp4 /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/Objective-C 3 | # Edit at https://www.gitignore.io/?templates=Objective-C 4 | 5 | ### Objective-C ### 6 | # Xcode 7 | # 8 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 9 | 10 | ## Build generated 11 | build/ 12 | DerivedData/ 13 | 14 | ## Various settings 15 | *.pbxuser 16 | !default.pbxuser 17 | *.mode1v3 18 | !default.mode1v3 19 | *.mode2v3 20 | !default.mode2v3 21 | *.perspectivev3 22 | !default.perspectivev3 23 | xcuserdata/ 24 | 25 | ## Other 26 | *.moved-aside 27 | *.xccheckout 28 | *.xcscmblueprint 29 | 30 | ## Obj-C/Swift specific 31 | *.hmap 32 | *.ipa 33 | *.dSYM.zip 34 | *.dSYM 35 | 36 | # CocoaPods 37 | # 38 | # We recommend against adding the Pods directory to your .gitignore. However 39 | # you should judge for yourself, the pros and cons are mentioned at: 40 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 41 | # 42 | # Pods/ 43 | # 44 | # Add this line if you want to avoid checking in source code from the Xcode workspace 45 | # *.xcworkspace 46 | 47 | # Carthage 48 | # 49 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 50 | # Carthage/Checkouts 51 | 52 | Carthage/Build 53 | 54 | # fastlane 55 | # 56 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 57 | # screenshots whenever they are needed. 58 | # For more information about the recommended setup visit: 59 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 60 | 61 | fastlane/report.xml 62 | fastlane/Preview.html 63 | fastlane/screenshots/**/*.png 64 | fastlane/test_output 65 | 66 | # Code Injection 67 | # 68 | # After new code Injection tools there's a generated folder /iOSInjectionProject 69 | # https://github.com/johnno1962/injectionforxcode 70 | 71 | iOSInjectionProject/ 72 | 73 | ### Objective-C Patch ### 74 | 75 | # End of https://www.gitignore.io/api/Objective-C 76 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright © 2018 Vito Zhang 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the “Software”), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in 12 | all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VITimelineView 2 | 3 | VITimelineView can represent any time base things. Made with fully customizable & extendable. 4 | 5 | ![](https://s1.ax1x.com/2018/12/01/FnETDf.jpg) 6 | 7 | ## Usage 8 | 9 | **Simple demo** 10 | 11 | Represent video frame's timeline using AVAsset 12 | 13 | ``` 14 | AVAsset *asset1 = ...; 15 | AVAsset *asset2 = ...; 16 | 17 | CGFloat widthPerSecond = 40; 18 | CGSize imageSize = CGSizeMake(30, 45); 19 | 20 | VITimelineView *timelineView = 21 | [VITimelineView timelineViewWithAssets:@[asset1, asset2] 22 | imageSize:imageSize 23 | widthPerSecond:widthPerSecond]; 24 | [self.view addSubview:timelineView]; 25 | ``` 26 | 27 | **Customize** 28 | 29 | 1. Customize TimelineView see VITimelineView.h 30 | 2. Customize single source's control view, see VIRangeView.h 31 | 3. Customize source's content view, you can subclass VIRangeContentView, then add to VIRangeView. 32 | 33 | ``` 34 | VIRangeView *rangeView = ...; 35 | rangeView.contentView = ; 36 | ``` 37 | 38 | VIVideoRangeContentView is a subclass of VIRangeContentView. 39 | 40 | ## Install 41 | 42 | **Cocoapods** 43 | 44 | ``` 45 | pod 'VITimelineView' 46 | ``` 47 | 48 | **Manually** 49 | 50 | Simplely drag `Source` folder to you project 51 | 52 | ## LICENSE 53 | 54 | Under MIT 55 | -------------------------------------------------------------------------------- /VITimelineView.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | 3 | s.name = 'VITimelineView' 4 | s.version = '0.1' 5 | s.summary = 'VITimelineView can represent any time base things. Made with fully customizable & extendable.' 6 | 7 | s.license = { :type => "MIT", :file => "LICENSE" } 8 | 9 | s.homepage = 'https://github.com/VideoFlint/VITimelineView' 10 | 11 | s.author = { 'Vito' => 'vvitozhang@gmail.com' } 12 | 13 | s.platform = :ios, '9.0' 14 | 15 | s.source = { :git => 'https://github.com/VideoFlint/VITimelineView.git', :tag => s.version.to_s } 16 | s.source_files = ['VITimelineViewDemo/Source/**/*.{h,m}'] 17 | 18 | s.requires_arc = true 19 | 20 | end 21 | 22 | -------------------------------------------------------------------------------- /VITimelineViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 50; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5F2F55A8219D0E5F006DC9F2 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55A7219D0E5F006DC9F2 /* AppDelegate.m */; }; 11 | 5F2F55AB219D0E5F006DC9F2 /* ViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55AA219D0E5F006DC9F2 /* ViewController.m */; }; 12 | 5F2F55AE219D0E5F006DC9F2 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5F2F55AC219D0E5F006DC9F2 /* Main.storyboard */; }; 13 | 5F2F55B0219D0E60006DC9F2 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 5F2F55AF219D0E60006DC9F2 /* Assets.xcassets */; }; 14 | 5F2F55B3219D0E60006DC9F2 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5F2F55B1219D0E60006DC9F2 /* LaunchScreen.storyboard */; }; 15 | 5F2F55B6219D0E60006DC9F2 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55B5219D0E60006DC9F2 /* main.m */; }; 16 | 5F2F55C9219D0E89006DC9F2 /* VIRangeContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55BD219D0E88006DC9F2 /* VIRangeContentView.m */; }; 17 | 5F2F55CA219D0E89006DC9F2 /* VIAutoScroller.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55BE219D0E88006DC9F2 /* VIAutoScroller.m */; }; 18 | 5F2F55CB219D0E89006DC9F2 /* VIRangeView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55C0219D0E88006DC9F2 /* VIRangeView.m */; }; 19 | 5F2F55CC219D0E89006DC9F2 /* VIVideoRangeContentView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55C2219D0E88006DC9F2 /* VIVideoRangeContentView.m */; }; 20 | 5F2F55CD219D0E89006DC9F2 /* VIRangeEarView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55C6219D0E89006DC9F2 /* VIRangeEarView.m */; }; 21 | 5F2F55CE219D0E89006DC9F2 /* VIDisplayTriggerMachine.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55C8219D0E89006DC9F2 /* VIDisplayTriggerMachine.m */; }; 22 | 5F2F55D1219D0E8E006DC9F2 /* VITimelineView.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F2F55D0219D0E8E006DC9F2 /* VITimelineView.m */; }; 23 | 5F365F4621B0164B00E5DE39 /* CachedAssetImageGenerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F365F4521B0164B00E5DE39 /* CachedAssetImageGenerator.m */; }; 24 | 5F47802821B0C86C00C062CA /* VIRangeContentAssetImageDataSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F47802721B0C86C00C062CA /* VIRangeContentAssetImageDataSource.m */; }; 25 | 5F47802C21B0CC0C00C062CA /* VIRangeView+Creator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F47802B21B0CC0C00C062CA /* VIRangeView+Creator.m */; }; 26 | 5F47802F21B0CCFF00C062CA /* VITimelineView+Creator.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F47802E21B0CCFF00C062CA /* VITimelineView+Creator.m */; }; 27 | 5F47803221B220EC00C062CA /* water.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 5F47803021B220EC00C062CA /* water.mp4 */; }; 28 | 5F47803321B220EC00C062CA /* bamboo.mp4 in Resources */ = {isa = PBXBuildFile; fileRef = 5F47803121B220EC00C062CA /* bamboo.mp4 */; }; 29 | /* End PBXBuildFile section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 5F2F55A3219D0E5F006DC9F2 /* VITimelineViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VITimelineViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | 5F2F55A6219D0E5F006DC9F2 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 34 | 5F2F55A7219D0E5F006DC9F2 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 35 | 5F2F55A9219D0E5F006DC9F2 /* ViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ViewController.h; sourceTree = ""; }; 36 | 5F2F55AA219D0E5F006DC9F2 /* ViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ViewController.m; sourceTree = ""; }; 37 | 5F2F55AD219D0E5F006DC9F2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 38 | 5F2F55AF219D0E60006DC9F2 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 39 | 5F2F55B2219D0E60006DC9F2 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 40 | 5F2F55B4219D0E60006DC9F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 41 | 5F2F55B5219D0E60006DC9F2 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 42 | 5F2F55BD219D0E88006DC9F2 /* VIRangeContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIRangeContentView.m; sourceTree = ""; }; 43 | 5F2F55BE219D0E88006DC9F2 /* VIAutoScroller.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIAutoScroller.m; sourceTree = ""; }; 44 | 5F2F55BF219D0E88006DC9F2 /* VIDisplayTriggerMachine.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIDisplayTriggerMachine.h; sourceTree = ""; }; 45 | 5F2F55C0219D0E88006DC9F2 /* VIRangeView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIRangeView.m; sourceTree = ""; }; 46 | 5F2F55C1219D0E88006DC9F2 /* VIVideoRangeContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIVideoRangeContentView.h; sourceTree = ""; }; 47 | 5F2F55C2219D0E88006DC9F2 /* VIVideoRangeContentView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIVideoRangeContentView.m; sourceTree = ""; }; 48 | 5F2F55C3219D0E89006DC9F2 /* VIRangeView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIRangeView.h; sourceTree = ""; }; 49 | 5F2F55C4219D0E89006DC9F2 /* VIAutoScroller.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIAutoScroller.h; sourceTree = ""; }; 50 | 5F2F55C5219D0E89006DC9F2 /* VIRangeEarView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIRangeEarView.h; sourceTree = ""; }; 51 | 5F2F55C6219D0E89006DC9F2 /* VIRangeEarView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIRangeEarView.m; sourceTree = ""; }; 52 | 5F2F55C7219D0E89006DC9F2 /* VIRangeContentView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VIRangeContentView.h; sourceTree = ""; }; 53 | 5F2F55C8219D0E89006DC9F2 /* VIDisplayTriggerMachine.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VIDisplayTriggerMachine.m; sourceTree = ""; }; 54 | 5F2F55CF219D0E8E006DC9F2 /* VITimelineView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = VITimelineView.h; sourceTree = ""; }; 55 | 5F2F55D0219D0E8E006DC9F2 /* VITimelineView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = VITimelineView.m; sourceTree = ""; }; 56 | 5F2F563021A436AC006DC9F2 /* UIView+ConstraintHolder.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIView+ConstraintHolder.h"; sourceTree = ""; }; 57 | 5F365F4421B0164B00E5DE39 /* CachedAssetImageGenerator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CachedAssetImageGenerator.h; sourceTree = ""; }; 58 | 5F365F4521B0164B00E5DE39 /* CachedAssetImageGenerator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = CachedAssetImageGenerator.m; sourceTree = ""; }; 59 | 5F47802621B0C86C00C062CA /* VIRangeContentAssetImageDataSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = VIRangeContentAssetImageDataSource.h; sourceTree = ""; }; 60 | 5F47802721B0C86C00C062CA /* VIRangeContentAssetImageDataSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = VIRangeContentAssetImageDataSource.m; sourceTree = ""; }; 61 | 5F47802A21B0CC0C00C062CA /* VIRangeView+Creator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VIRangeView+Creator.h"; sourceTree = ""; }; 62 | 5F47802B21B0CC0C00C062CA /* VIRangeView+Creator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "VIRangeView+Creator.m"; sourceTree = ""; }; 63 | 5F47802D21B0CCFF00C062CA /* VITimelineView+Creator.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "VITimelineView+Creator.h"; sourceTree = ""; }; 64 | 5F47802E21B0CCFF00C062CA /* VITimelineView+Creator.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "VITimelineView+Creator.m"; sourceTree = ""; }; 65 | 5F47803021B220EC00C062CA /* water.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = water.mp4; sourceTree = ""; }; 66 | 5F47803121B220EC00C062CA /* bamboo.mp4 */ = {isa = PBXFileReference; lastKnownFileType = file; path = bamboo.mp4; sourceTree = ""; }; 67 | /* End PBXFileReference section */ 68 | 69 | /* Begin PBXFrameworksBuildPhase section */ 70 | 5F2F55A0219D0E5F006DC9F2 /* Frameworks */ = { 71 | isa = PBXFrameworksBuildPhase; 72 | buildActionMask = 2147483647; 73 | files = ( 74 | ); 75 | runOnlyForDeploymentPostprocessing = 0; 76 | }; 77 | /* End PBXFrameworksBuildPhase section */ 78 | 79 | /* Begin PBXGroup section */ 80 | 5F2F559A219D0E5F006DC9F2 = { 81 | isa = PBXGroup; 82 | children = ( 83 | 5F2F55A5219D0E5F006DC9F2 /* VITimelineViewDemo */, 84 | 5F2F55A4219D0E5F006DC9F2 /* Products */, 85 | ); 86 | sourceTree = ""; 87 | }; 88 | 5F2F55A4219D0E5F006DC9F2 /* Products */ = { 89 | isa = PBXGroup; 90 | children = ( 91 | 5F2F55A3219D0E5F006DC9F2 /* VITimelineViewDemo.app */, 92 | ); 93 | name = Products; 94 | sourceTree = ""; 95 | }; 96 | 5F2F55A5219D0E5F006DC9F2 /* VITimelineViewDemo */ = { 97 | isa = PBXGroup; 98 | children = ( 99 | 5F2F55BC219D0E7A006DC9F2 /* Source */, 100 | 5F2F55A6219D0E5F006DC9F2 /* AppDelegate.h */, 101 | 5F2F55A7219D0E5F006DC9F2 /* AppDelegate.m */, 102 | 5F2F55A9219D0E5F006DC9F2 /* ViewController.h */, 103 | 5F2F55AA219D0E5F006DC9F2 /* ViewController.m */, 104 | 5F2F55AC219D0E5F006DC9F2 /* Main.storyboard */, 105 | 5F2F55AF219D0E60006DC9F2 /* Assets.xcassets */, 106 | 5F2F55B1219D0E60006DC9F2 /* LaunchScreen.storyboard */, 107 | 5F47803121B220EC00C062CA /* bamboo.mp4 */, 108 | 5F47803021B220EC00C062CA /* water.mp4 */, 109 | 5F2F55B4219D0E60006DC9F2 /* Info.plist */, 110 | 5F2F55B5219D0E60006DC9F2 /* main.m */, 111 | ); 112 | path = VITimelineViewDemo; 113 | sourceTree = ""; 114 | }; 115 | 5F2F55BC219D0E7A006DC9F2 /* Source */ = { 116 | isa = PBXGroup; 117 | children = ( 118 | 5F47802921B0CBEC00C062CA /* Convenient */, 119 | 5F47802521B0C82800C062CA /* DataSource */, 120 | 5F2F55D2219D0ED4006DC9F2 /* Utils */, 121 | 5F2F55CF219D0E8E006DC9F2 /* VITimelineView.h */, 122 | 5F2F55D0219D0E8E006DC9F2 /* VITimelineView.m */, 123 | 5F2F55C7219D0E89006DC9F2 /* VIRangeContentView.h */, 124 | 5F2F55BD219D0E88006DC9F2 /* VIRangeContentView.m */, 125 | 5F2F55C5219D0E89006DC9F2 /* VIRangeEarView.h */, 126 | 5F2F55C6219D0E89006DC9F2 /* VIRangeEarView.m */, 127 | 5F2F55C3219D0E89006DC9F2 /* VIRangeView.h */, 128 | 5F2F55C0219D0E88006DC9F2 /* VIRangeView.m */, 129 | 5F2F55C1219D0E88006DC9F2 /* VIVideoRangeContentView.h */, 130 | 5F2F55C2219D0E88006DC9F2 /* VIVideoRangeContentView.m */, 131 | ); 132 | path = Source; 133 | sourceTree = ""; 134 | }; 135 | 5F2F55D2219D0ED4006DC9F2 /* Utils */ = { 136 | isa = PBXGroup; 137 | children = ( 138 | 5F2F55C4219D0E89006DC9F2 /* VIAutoScroller.h */, 139 | 5F2F55BE219D0E88006DC9F2 /* VIAutoScroller.m */, 140 | 5F2F55BF219D0E88006DC9F2 /* VIDisplayTriggerMachine.h */, 141 | 5F2F55C8219D0E89006DC9F2 /* VIDisplayTriggerMachine.m */, 142 | 5F2F563021A436AC006DC9F2 /* UIView+ConstraintHolder.h */, 143 | 5F365F4421B0164B00E5DE39 /* CachedAssetImageGenerator.h */, 144 | 5F365F4521B0164B00E5DE39 /* CachedAssetImageGenerator.m */, 145 | ); 146 | path = Utils; 147 | sourceTree = ""; 148 | }; 149 | 5F47802521B0C82800C062CA /* DataSource */ = { 150 | isa = PBXGroup; 151 | children = ( 152 | 5F47802621B0C86C00C062CA /* VIRangeContentAssetImageDataSource.h */, 153 | 5F47802721B0C86C00C062CA /* VIRangeContentAssetImageDataSource.m */, 154 | ); 155 | path = DataSource; 156 | sourceTree = ""; 157 | }; 158 | 5F47802921B0CBEC00C062CA /* Convenient */ = { 159 | isa = PBXGroup; 160 | children = ( 161 | 5F47802A21B0CC0C00C062CA /* VIRangeView+Creator.h */, 162 | 5F47802B21B0CC0C00C062CA /* VIRangeView+Creator.m */, 163 | 5F47802D21B0CCFF00C062CA /* VITimelineView+Creator.h */, 164 | 5F47802E21B0CCFF00C062CA /* VITimelineView+Creator.m */, 165 | ); 166 | path = Convenient; 167 | sourceTree = ""; 168 | }; 169 | /* End PBXGroup section */ 170 | 171 | /* Begin PBXNativeTarget section */ 172 | 5F2F55A2219D0E5F006DC9F2 /* VITimelineViewDemo */ = { 173 | isa = PBXNativeTarget; 174 | buildConfigurationList = 5F2F55B9219D0E60006DC9F2 /* Build configuration list for PBXNativeTarget "VITimelineViewDemo" */; 175 | buildPhases = ( 176 | 5F2F559F219D0E5F006DC9F2 /* Sources */, 177 | 5F2F55A0219D0E5F006DC9F2 /* Frameworks */, 178 | 5F2F55A1219D0E5F006DC9F2 /* Resources */, 179 | ); 180 | buildRules = ( 181 | ); 182 | dependencies = ( 183 | ); 184 | name = VITimelineViewDemo; 185 | productName = VITimelineViewDemo; 186 | productReference = 5F2F55A3219D0E5F006DC9F2 /* VITimelineViewDemo.app */; 187 | productType = "com.apple.product-type.application"; 188 | }; 189 | /* End PBXNativeTarget section */ 190 | 191 | /* Begin PBXProject section */ 192 | 5F2F559B219D0E5F006DC9F2 /* Project object */ = { 193 | isa = PBXProject; 194 | attributes = { 195 | LastUpgradeCheck = 1010; 196 | ORGANIZATIONNAME = vito; 197 | TargetAttributes = { 198 | 5F2F55A2219D0E5F006DC9F2 = { 199 | CreatedOnToolsVersion = 10.1; 200 | }; 201 | }; 202 | }; 203 | buildConfigurationList = 5F2F559E219D0E5F006DC9F2 /* Build configuration list for PBXProject "VITimelineViewDemo" */; 204 | compatibilityVersion = "Xcode 9.3"; 205 | developmentRegion = en; 206 | hasScannedForEncodings = 0; 207 | knownRegions = ( 208 | en, 209 | Base, 210 | ); 211 | mainGroup = 5F2F559A219D0E5F006DC9F2; 212 | productRefGroup = 5F2F55A4219D0E5F006DC9F2 /* Products */; 213 | projectDirPath = ""; 214 | projectRoot = ""; 215 | targets = ( 216 | 5F2F55A2219D0E5F006DC9F2 /* VITimelineViewDemo */, 217 | ); 218 | }; 219 | /* End PBXProject section */ 220 | 221 | /* Begin PBXResourcesBuildPhase section */ 222 | 5F2F55A1219D0E5F006DC9F2 /* Resources */ = { 223 | isa = PBXResourcesBuildPhase; 224 | buildActionMask = 2147483647; 225 | files = ( 226 | 5F2F55B3219D0E60006DC9F2 /* LaunchScreen.storyboard in Resources */, 227 | 5F2F55B0219D0E60006DC9F2 /* Assets.xcassets in Resources */, 228 | 5F2F55AE219D0E5F006DC9F2 /* Main.storyboard in Resources */, 229 | 5F47803221B220EC00C062CA /* water.mp4 in Resources */, 230 | 5F47803321B220EC00C062CA /* bamboo.mp4 in Resources */, 231 | ); 232 | runOnlyForDeploymentPostprocessing = 0; 233 | }; 234 | /* End PBXResourcesBuildPhase section */ 235 | 236 | /* Begin PBXSourcesBuildPhase section */ 237 | 5F2F559F219D0E5F006DC9F2 /* Sources */ = { 238 | isa = PBXSourcesBuildPhase; 239 | buildActionMask = 2147483647; 240 | files = ( 241 | 5F365F4621B0164B00E5DE39 /* CachedAssetImageGenerator.m in Sources */, 242 | 5F2F55CC219D0E89006DC9F2 /* VIVideoRangeContentView.m in Sources */, 243 | 5F2F55AB219D0E5F006DC9F2 /* ViewController.m in Sources */, 244 | 5F47802F21B0CCFF00C062CA /* VITimelineView+Creator.m in Sources */, 245 | 5F2F55CA219D0E89006DC9F2 /* VIAutoScroller.m in Sources */, 246 | 5F47802821B0C86C00C062CA /* VIRangeContentAssetImageDataSource.m in Sources */, 247 | 5F2F55C9219D0E89006DC9F2 /* VIRangeContentView.m in Sources */, 248 | 5F2F55D1219D0E8E006DC9F2 /* VITimelineView.m in Sources */, 249 | 5F2F55CB219D0E89006DC9F2 /* VIRangeView.m in Sources */, 250 | 5F2F55CE219D0E89006DC9F2 /* VIDisplayTriggerMachine.m in Sources */, 251 | 5F2F55B6219D0E60006DC9F2 /* main.m in Sources */, 252 | 5F47802C21B0CC0C00C062CA /* VIRangeView+Creator.m in Sources */, 253 | 5F2F55CD219D0E89006DC9F2 /* VIRangeEarView.m in Sources */, 254 | 5F2F55A8219D0E5F006DC9F2 /* AppDelegate.m in Sources */, 255 | ); 256 | runOnlyForDeploymentPostprocessing = 0; 257 | }; 258 | /* End PBXSourcesBuildPhase section */ 259 | 260 | /* Begin PBXVariantGroup section */ 261 | 5F2F55AC219D0E5F006DC9F2 /* Main.storyboard */ = { 262 | isa = PBXVariantGroup; 263 | children = ( 264 | 5F2F55AD219D0E5F006DC9F2 /* Base */, 265 | ); 266 | name = Main.storyboard; 267 | sourceTree = ""; 268 | }; 269 | 5F2F55B1219D0E60006DC9F2 /* LaunchScreen.storyboard */ = { 270 | isa = PBXVariantGroup; 271 | children = ( 272 | 5F2F55B2219D0E60006DC9F2 /* Base */, 273 | ); 274 | name = LaunchScreen.storyboard; 275 | sourceTree = ""; 276 | }; 277 | /* End PBXVariantGroup section */ 278 | 279 | /* Begin XCBuildConfiguration section */ 280 | 5F2F55B7219D0E60006DC9F2 /* Debug */ = { 281 | isa = XCBuildConfiguration; 282 | buildSettings = { 283 | ALWAYS_SEARCH_USER_PATHS = NO; 284 | CLANG_ANALYZER_NONNULL = YES; 285 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 286 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 287 | CLANG_CXX_LIBRARY = "libc++"; 288 | CLANG_ENABLE_MODULES = YES; 289 | CLANG_ENABLE_OBJC_ARC = YES; 290 | CLANG_ENABLE_OBJC_WEAK = YES; 291 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 292 | CLANG_WARN_BOOL_CONVERSION = YES; 293 | CLANG_WARN_COMMA = YES; 294 | CLANG_WARN_CONSTANT_CONVERSION = YES; 295 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 296 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 297 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 298 | CLANG_WARN_EMPTY_BODY = YES; 299 | CLANG_WARN_ENUM_CONVERSION = YES; 300 | CLANG_WARN_INFINITE_RECURSION = YES; 301 | CLANG_WARN_INT_CONVERSION = YES; 302 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 303 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 304 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 305 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 306 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 307 | CLANG_WARN_STRICT_PROTOTYPES = YES; 308 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 309 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 310 | CLANG_WARN_UNREACHABLE_CODE = YES; 311 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 312 | CODE_SIGN_IDENTITY = "iPhone Developer"; 313 | COPY_PHASE_STRIP = NO; 314 | DEBUG_INFORMATION_FORMAT = dwarf; 315 | ENABLE_STRICT_OBJC_MSGSEND = YES; 316 | ENABLE_TESTABILITY = YES; 317 | GCC_C_LANGUAGE_STANDARD = gnu11; 318 | GCC_DYNAMIC_NO_PIC = NO; 319 | GCC_NO_COMMON_BLOCKS = YES; 320 | GCC_OPTIMIZATION_LEVEL = 0; 321 | GCC_PREPROCESSOR_DEFINITIONS = ( 322 | "DEBUG=1", 323 | "$(inherited)", 324 | ); 325 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 326 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 327 | GCC_WARN_UNDECLARED_SELECTOR = YES; 328 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 329 | GCC_WARN_UNUSED_FUNCTION = YES; 330 | GCC_WARN_UNUSED_VARIABLE = YES; 331 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 332 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 333 | MTL_FAST_MATH = YES; 334 | ONLY_ACTIVE_ARCH = YES; 335 | SDKROOT = iphoneos; 336 | }; 337 | name = Debug; 338 | }; 339 | 5F2F55B8219D0E60006DC9F2 /* Release */ = { 340 | isa = XCBuildConfiguration; 341 | buildSettings = { 342 | ALWAYS_SEARCH_USER_PATHS = NO; 343 | CLANG_ANALYZER_NONNULL = YES; 344 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 345 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 346 | CLANG_CXX_LIBRARY = "libc++"; 347 | CLANG_ENABLE_MODULES = YES; 348 | CLANG_ENABLE_OBJC_ARC = YES; 349 | CLANG_ENABLE_OBJC_WEAK = YES; 350 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 351 | CLANG_WARN_BOOL_CONVERSION = YES; 352 | CLANG_WARN_COMMA = YES; 353 | CLANG_WARN_CONSTANT_CONVERSION = YES; 354 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 355 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 356 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 357 | CLANG_WARN_EMPTY_BODY = YES; 358 | CLANG_WARN_ENUM_CONVERSION = YES; 359 | CLANG_WARN_INFINITE_RECURSION = YES; 360 | CLANG_WARN_INT_CONVERSION = YES; 361 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 362 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 363 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 364 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 365 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 366 | CLANG_WARN_STRICT_PROTOTYPES = YES; 367 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 368 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 369 | CLANG_WARN_UNREACHABLE_CODE = YES; 370 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 371 | CODE_SIGN_IDENTITY = "iPhone Developer"; 372 | COPY_PHASE_STRIP = NO; 373 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 374 | ENABLE_NS_ASSERTIONS = NO; 375 | ENABLE_STRICT_OBJC_MSGSEND = YES; 376 | GCC_C_LANGUAGE_STANDARD = gnu11; 377 | GCC_NO_COMMON_BLOCKS = YES; 378 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 379 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 380 | GCC_WARN_UNDECLARED_SELECTOR = YES; 381 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 382 | GCC_WARN_UNUSED_FUNCTION = YES; 383 | GCC_WARN_UNUSED_VARIABLE = YES; 384 | IPHONEOS_DEPLOYMENT_TARGET = 9.0; 385 | MTL_ENABLE_DEBUG_INFO = NO; 386 | MTL_FAST_MATH = YES; 387 | SDKROOT = iphoneos; 388 | VALIDATE_PRODUCT = YES; 389 | }; 390 | name = Release; 391 | }; 392 | 5F2F55BA219D0E60006DC9F2 /* Debug */ = { 393 | isa = XCBuildConfiguration; 394 | buildSettings = { 395 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 396 | CODE_SIGN_STYLE = Automatic; 397 | DEVELOPMENT_TEAM = ZZX7396L9W; 398 | INFOPLIST_FILE = VITimelineViewDemo/Info.plist; 399 | LD_RUNPATH_SEARCH_PATHS = ( 400 | "$(inherited)", 401 | "@executable_path/Frameworks", 402 | ); 403 | PRODUCT_BUNDLE_IDENTIFIER = com.vito.VITimelineViewDemo; 404 | PRODUCT_NAME = "$(TARGET_NAME)"; 405 | TARGETED_DEVICE_FAMILY = "1,2"; 406 | }; 407 | name = Debug; 408 | }; 409 | 5F2F55BB219D0E60006DC9F2 /* Release */ = { 410 | isa = XCBuildConfiguration; 411 | buildSettings = { 412 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 413 | CODE_SIGN_STYLE = Automatic; 414 | DEVELOPMENT_TEAM = ZZX7396L9W; 415 | INFOPLIST_FILE = VITimelineViewDemo/Info.plist; 416 | LD_RUNPATH_SEARCH_PATHS = ( 417 | "$(inherited)", 418 | "@executable_path/Frameworks", 419 | ); 420 | PRODUCT_BUNDLE_IDENTIFIER = com.vito.VITimelineViewDemo; 421 | PRODUCT_NAME = "$(TARGET_NAME)"; 422 | TARGETED_DEVICE_FAMILY = "1,2"; 423 | }; 424 | name = Release; 425 | }; 426 | /* End XCBuildConfiguration section */ 427 | 428 | /* Begin XCConfigurationList section */ 429 | 5F2F559E219D0E5F006DC9F2 /* Build configuration list for PBXProject "VITimelineViewDemo" */ = { 430 | isa = XCConfigurationList; 431 | buildConfigurations = ( 432 | 5F2F55B7219D0E60006DC9F2 /* Debug */, 433 | 5F2F55B8219D0E60006DC9F2 /* Release */, 434 | ); 435 | defaultConfigurationIsVisible = 0; 436 | defaultConfigurationName = Release; 437 | }; 438 | 5F2F55B9219D0E60006DC9F2 /* Build configuration list for PBXNativeTarget "VITimelineViewDemo" */ = { 439 | isa = XCConfigurationList; 440 | buildConfigurations = ( 441 | 5F2F55BA219D0E60006DC9F2 /* Debug */, 442 | 5F2F55BB219D0E60006DC9F2 /* Release */, 443 | ); 444 | defaultConfigurationIsVisible = 0; 445 | defaultConfigurationName = Release; 446 | }; 447 | /* End XCConfigurationList section */ 448 | }; 449 | rootObject = 5F2F559B219D0E5F006DC9F2 /* Project object */; 450 | } 451 | -------------------------------------------------------------------------------- /VITimelineViewDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /VITimelineViewDemo.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /VITimelineViewDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/15. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (strong, nonatomic) UIWindow *window; 14 | 15 | 16 | @end 17 | 18 | -------------------------------------------------------------------------------- /VITimelineViewDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/15. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "AppDelegate.h" 10 | 11 | @interface AppDelegate () 12 | 13 | @end 14 | 15 | @implementation AppDelegate 16 | 17 | 18 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 19 | // Override point for customization after application launch. 20 | return YES; 21 | } 22 | 23 | 24 | - (void)applicationWillResignActive:(UIApplication *)application { 25 | // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. 26 | // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. 27 | } 28 | 29 | 30 | - (void)applicationDidEnterBackground:(UIApplication *)application { 31 | // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. 32 | // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. 33 | } 34 | 35 | 36 | - (void)applicationWillEnterForeground:(UIApplication *)application { 37 | // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. 38 | } 39 | 40 | 41 | - (void)applicationDidBecomeActive:(UIApplication *)application { 42 | // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. 43 | } 44 | 45 | 46 | - (void)applicationWillTerminate:(UIApplication *)application { 47 | // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. 48 | } 49 | 50 | 51 | @end 52 | -------------------------------------------------------------------------------- /VITimelineViewDemo/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 | } -------------------------------------------------------------------------------- /VITimelineViewDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /VITimelineViewDemo/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 | -------------------------------------------------------------------------------- /VITimelineViewDemo/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 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /VITimelineViewDemo/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 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIMainStoryboardFile 26 | Main 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Convenient/VIRangeView+Creator.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeView+Creator.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/30. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIRangeView.h" 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface VIRangeView (Creator) 15 | 16 | + (instancetype)imageRangeViewWithAsset:(AVAsset *)asset imageSize:(CGSize)imageSize; 17 | 18 | @end 19 | 20 | NS_ASSUME_NONNULL_END 21 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Convenient/VIRangeView+Creator.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeView+Creator.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/30. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIRangeView+Creator.h" 10 | #import "VIRangeContentAssetImageDataSource.h" 11 | #import "VIVideoRangeContentView.h" 12 | 13 | @implementation VIRangeView (Creator) 14 | 15 | + (instancetype)imageRangeViewWithAsset:(AVAsset *)asset imageSize:(CGSize)imageSize { 16 | VIRangeView *rangeView = [[VIRangeView alloc] init]; 17 | VIRangeContentAssetImageDataSource *dataSource = 18 | [[VIRangeContentAssetImageDataSource alloc] initWithAsset:asset 19 | imageSize:imageSize 20 | widthPerSecond:rangeView.widthPerSecond]; 21 | 22 | VIVideoRangeContentView *videoRangeContentView = [[VIVideoRangeContentView alloc] init]; 23 | videoRangeContentView.dataSource = dataSource; 24 | videoRangeContentView.imageSize = imageSize; 25 | 26 | rangeView.contentView = videoRangeContentView; 27 | rangeView.startTime = kCMTimeZero; 28 | rangeView.endTime = asset.duration; 29 | rangeView.maxDuration = asset.duration; 30 | 31 | UIEdgeInsets insets = rangeView.contentInset; 32 | insets.top = 1; 33 | insets.bottom = 1; 34 | rangeView.contentInset = insets; 35 | 36 | return rangeView; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Convenient/VITimelineView+Creator.h: -------------------------------------------------------------------------------- 1 | // 2 | // VITimelineView+Creator.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/30. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VITimelineView.h" 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface VITimelineView (Creator) 15 | 16 | + (instancetype)timelineViewWithAssets:(NSArray *)assets 17 | imageSize:(CGSize)imageSize 18 | widthPerSecond:(CGFloat)widthPerSecond; 19 | 20 | @end 21 | 22 | NS_ASSUME_NONNULL_END 23 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Convenient/VITimelineView+Creator.m: -------------------------------------------------------------------------------- 1 | // 2 | // VITimelineView+Creator.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/30. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VITimelineView+Creator.h" 10 | #import "VIRangeView+Creator.h" 11 | 12 | @implementation VITimelineView (Creator) 13 | 14 | + (instancetype)timelineViewWithAssets:(NSArray *)assets 15 | imageSize:(CGSize)imageSize 16 | widthPerSecond:(CGFloat)widthPerSecond { 17 | VITimelineView *timelineView = [[VITimelineView alloc] init]; 18 | timelineView.contentWidthPerSecond = widthPerSecond; 19 | timelineView.rangeViewLeftInset = 1; 20 | timelineView.rangeViewRightInset = 1; 21 | 22 | for (AVAsset *asset in assets) { 23 | VIRangeView *rangeView = [VIRangeView imageRangeViewWithAsset:asset imageSize:imageSize]; 24 | [timelineView insertRangeView:rangeView atIndex:timelineView.rangeViews.count]; 25 | } 26 | 27 | return timelineView; 28 | } 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/DataSource/VIRangeContentAssetImageDataSource.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeContentAssetImageDataSource.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/30. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "VIVideoRangeContentView.h" 11 | #import 12 | 13 | NS_ASSUME_NONNULL_BEGIN 14 | 15 | @interface VIRangeContentAssetImageDataSource : NSObject 16 | 17 | @property (nonatomic, strong, readonly) AVAsset *asset; 18 | 19 | - (instancetype)initWithAsset:(AVAsset *)asset imageSize:(CGSize)imageSize widthPerSecond:(CGFloat)widthPerSecond; 20 | 21 | @property (nonatomic) CGSize imageSize; 22 | @property (nonatomic) CGFloat widthPerSecond; 23 | 24 | @end 25 | 26 | NS_ASSUME_NONNULL_END 27 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/DataSource/VIRangeContentAssetImageDataSource.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeContentAssetImageDataSource.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/30. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIRangeContentAssetImageDataSource.h" 10 | #import "CachedAssetImageGenerator.h" 11 | 12 | @interface VIRangeContentAssetImageDataSource() 13 | 14 | @property (nonatomic, strong, readwrite) AVAsset *asset; 15 | @property (nonatomic, strong) CachedAssetImageGenerator *imageGenerator; 16 | 17 | @end 18 | 19 | @implementation VIRangeContentAssetImageDataSource 20 | 21 | - (instancetype)initWithAsset:(AVAsset *)asset imageSize:(CGSize)imageSize widthPerSecond:(CGFloat)widthPerSecond { 22 | self = [super init]; 23 | if (self) { 24 | _asset = asset; 25 | CachedAssetImageGenerator *imageGenerator = [CachedAssetImageGenerator assetImageGeneratorWithAsset:self.asset]; 26 | imageGenerator.requestedTimeToleranceBefore = CMTimeMake(600, 600); 27 | imageGenerator.requestedTimeToleranceAfter = CMTimeMake(600, 600); 28 | imageGenerator.appliesPreferredTrackTransform = YES; 29 | _imageGenerator = imageGenerator; 30 | [self setImageSize:imageSize]; 31 | _widthPerSecond = widthPerSecond; 32 | } 33 | return self; 34 | } 35 | 36 | - (void)setImageSize:(CGSize)imageSize { 37 | _imageSize = imageSize; 38 | imageSize = CGSizeMake(imageSize.width * UIScreen.mainScreen.scale, imageSize.height * UIScreen.mainScreen.scale); 39 | AVAssetTrack *track = [[self.imageGenerator.asset tracksWithMediaType:AVMediaTypeVideo] firstObject]; 40 | if (track) { 41 | CGSize size = CGSizeMake(imageSize.width, imageSize.height); 42 | if (track) { 43 | CGSize naturalSize = CGSizeApplyAffineTransform(track.naturalSize, track.preferredTransform); 44 | naturalSize.width = fabs(naturalSize.width); 45 | naturalSize.height = fabs(naturalSize.height); 46 | if (naturalSize.width / imageSize.width > naturalSize.height / imageSize.height) { 47 | size = CGSizeMake(0, imageSize.height); 48 | } else { 49 | size = CGSizeMake(imageSize.width, 0); 50 | } 51 | } 52 | self.imageGenerator.maximumSize = size; 53 | } else { 54 | self.imageGenerator.maximumSize = imageSize; 55 | } 56 | } 57 | 58 | #pragma mark - VIVideoRangeContentViewDataSource 59 | 60 | - (NSInteger)videoRangeContentViewNumberOfImages:(VIVideoRangeContentView *)view { 61 | NSTimeInterval sourceSeconds = CMTimeGetSeconds(self.asset.duration); 62 | return ceil(sourceSeconds / (self.imageSize.width / self.widthPerSecond)); 63 | } 64 | 65 | - (UIImage *)videoRangeContent:(VIVideoRangeContentView *)view imageAtIndex:(NSInteger)index preferredSize:(CGSize)size { 66 | CGFloat offset = view.imageSize.width * index; 67 | NSTimeInterval time = offset / self.widthPerSecond; 68 | 69 | UIImage *image; 70 | CGImageRef cgimage = [self.imageGenerator copyCGImageAtTime:CMTimeMakeWithSeconds(time, 600) actualTime:nil error:nil]; 71 | if (cgimage) { 72 | image = [[UIImage alloc] initWithCGImage:cgimage]; 73 | } 74 | return image; 75 | } 76 | 77 | - (BOOL)videoRangeContent:(VIVideoRangeContentView *)view hasCacheAtIndex:(NSInteger)index { 78 | CGFloat offset = view.imageSize.width * index; 79 | NSTimeInterval second = offset / self.widthPerSecond; 80 | CMTime time = CMTimeMakeWithSeconds(second, 600); 81 | return [self.imageGenerator hasCacheAtTime:time]; 82 | } 83 | 84 | @end 85 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/CachedAssetImageGenerator.h: -------------------------------------------------------------------------------- 1 | // 2 | // CachedAssetImageGenerator.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/29. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface CachedAssetImageGenerator : AVAssetImageGenerator 14 | 15 | - (BOOL)hasCacheAtTime:(CMTime)time; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/CachedAssetImageGenerator.m: -------------------------------------------------------------------------------- 1 | // 2 | // CachedAssetImageGenerator.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/29. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "CachedAssetImageGenerator.h" 10 | #import 11 | 12 | @interface CachedAssetImageGenerator () 13 | 14 | @property (nonatomic, strong) NSMutableDictionary *imageCache; 15 | 16 | @end 17 | 18 | @implementation CachedAssetImageGenerator 19 | 20 | 21 | - (instancetype)initWithAsset:(AVAsset *)asset { 22 | self = [super initWithAsset:asset]; 23 | if (self) { 24 | _imageCache = [NSMutableDictionary dictionary]; 25 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; 26 | } 27 | return self; 28 | } 29 | 30 | - (void)didReceiveMemoryWarning:(NSNotification *)notification { 31 | [self.imageCache removeAllObjects]; 32 | } 33 | 34 | - (CGImageRef)copyCGImageAtTime:(CMTime)requestedTime actualTime:(CMTime *)actualTime error:(NSError *__autoreleasing _Nullable *)outError { 35 | NSString *key = [self keyAtTime:requestedTime]; 36 | CGImageRef image = (__bridge CGImageRef)(self.imageCache[key]); 37 | if (!image) { 38 | image = [super copyCGImageAtTime:requestedTime actualTime:actualTime error:outError]; 39 | self.imageCache[key] = (__bridge id _Nullable)(image); 40 | } 41 | 42 | return image; 43 | } 44 | 45 | 46 | - (BOOL)hasCacheAtTime:(CMTime)time { 47 | NSString *key = [self keyAtTime:time]; 48 | return self.imageCache[key] != nil; 49 | } 50 | 51 | - (NSString *)keyAtTime:(CMTime)time { 52 | return [NSString stringWithFormat:@"time: %@, size: %@", @((NSInteger)(CMTimeGetSeconds(time) * 30)), NSStringFromCGSize(self.maximumSize)]; 53 | } 54 | 55 | @end 56 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/UIView+ConstraintHolder.h: -------------------------------------------------------------------------------- 1 | // 2 | // UIView+ConstraintHolder.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/20. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | NS_ASSUME_NONNULL_BEGIN 13 | 14 | @interface UIView (ConstraintHolder) 15 | 16 | - (void)setVi_constraints:(NSArray *)constraints; 17 | - (NSArray *)vi_constraints; 18 | 19 | - (void)updateConstraintWithAttribute:(NSLayoutAttribute)attribute maker:(NSLayoutConstraint *(^)(void))maker; 20 | - (void)updateLeftConstraint:(NSLayoutConstraint *(^)(void))maker; 21 | - (void)updateRightConstraint:(NSLayoutConstraint *(^)(void))maker; 22 | 23 | @end 24 | 25 | @implementation UIView (ConstraintHolder) 26 | 27 | static const char VIConstaintsKey = 'c'; 28 | - (void)setVi_constraints:(NSArray *)constraints { 29 | objc_setAssociatedObject(self, &VIConstaintsKey, constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 30 | } 31 | 32 | - (NSArray *)vi_constraints { 33 | return objc_getAssociatedObject(self, &VIConstaintsKey); 34 | } 35 | 36 | - (void)updateConstraintWithAttribute:(NSLayoutAttribute)attribute maker:(NSLayoutConstraint *(^)(void))maker { 37 | NSLayoutConstraint *constraint; 38 | for (NSLayoutConstraint *c in self.vi_constraints) { 39 | if (c.firstItem == self && c.firstAttribute == attribute) { 40 | constraint = c; 41 | break; 42 | } 43 | } 44 | [NSLayoutConstraint deactivateConstraints:@[constraint]]; 45 | 46 | NSMutableArray *mutableConstraints = [self.vi_constraints mutableCopy]; 47 | [mutableConstraints removeObject:constraint]; 48 | 49 | if (maker) { 50 | NSLayoutConstraint *newConstraint = maker(); 51 | if (newConstraint.firstItem == self && newConstraint.firstAttribute == attribute) { 52 | [mutableConstraints addObject:constraint]; 53 | [NSLayoutConstraint activateConstraints:@[newConstraint]]; 54 | } 55 | } 56 | self.vi_constraints = mutableConstraints; 57 | } 58 | 59 | - (void)updateLeftConstraint:(NSLayoutConstraint *(^)(void))maker { 60 | [self updateConstraintWithAttribute:NSLayoutAttributeLeft maker:maker]; 61 | } 62 | 63 | - (void)updateRightConstraint:(NSLayoutConstraint *(^)(void))maker { 64 | [self updateConstraintWithAttribute:NSLayoutAttributeRight maker:maker]; 65 | } 66 | 67 | @end 68 | 69 | NS_ASSUME_NONNULL_END 70 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/VIAutoScroller.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIAutoScroller.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | #import "VIDisplayTriggerMachine.h" 12 | 13 | typedef NS_ENUM(NSInteger, VIAutoScrollerType) { 14 | VIAutoScrollerTypeNone = 0, 15 | VIAutoScrollerTypeLeft, 16 | VIAutoScrollerTypeRight 17 | }; 18 | 19 | @interface VIAutoScroller : NSObject 20 | 21 | @property (nonatomic, strong, readonly) VIDisplayTriggerMachine *triggerMachine; 22 | 23 | @property (nonatomic) VIAutoScrollerType autoScrollType; 24 | @property (nonatomic) float autoScrollSpeed; 25 | @property (nonatomic) CGFloat autoScrollInset; 26 | @property (nonatomic) CGFloat earEdgeInset; 27 | 28 | - (void)cleanUpAutoScrollValues; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/VIAutoScroller.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIAutoScroller.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIAutoScroller.h" 10 | 11 | @interface VIAutoScroller() 12 | 13 | @property (nonatomic, strong, readwrite) VIDisplayTriggerMachine *triggerMachine; 14 | 15 | @end 16 | 17 | @implementation VIAutoScroller 18 | 19 | - (instancetype)init 20 | { 21 | self = [super init]; 22 | if (self) { 23 | _autoScrollInset = 100; 24 | _earEdgeInset = 30; 25 | _triggerMachine = [[VIDisplayTriggerMachine alloc] init]; 26 | } 27 | return self; 28 | } 29 | 30 | - (void)cleanUpAutoScrollValues { 31 | self.autoScrollSpeed = 0; 32 | self.autoScrollType = VIAutoScrollerTypeNone; 33 | [self.triggerMachine pause]; 34 | } 35 | 36 | @end 37 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/VIDisplayTriggerMachine.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIDisplayTriggerMachine.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | typedef void(^TriggerOperation)(void); 12 | 13 | @interface VIDisplayTriggerMachine : NSObject 14 | 15 | @property (nonatomic, copy) TriggerOperation triggerOperation; 16 | - (instancetype)initWithTriggerOperation:(TriggerOperation)triggerOperation; 17 | 18 | @property (nonatomic) NSInteger preferredFramesPerSecond; 19 | 20 | - (void)start; 21 | - (void)pause; 22 | 23 | @end 24 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/Utils/VIDisplayTriggerMachine.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIDisplayTriggerMachine.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIDisplayTriggerMachine.h" 10 | #import 11 | 12 | 13 | @interface VIDisplayTriggerObject: NSObject 14 | @property (nonatomic, copy) TriggerOperation triggerOperation; 15 | @end 16 | 17 | @implementation VIDisplayTriggerObject 18 | 19 | - (void)trigger { 20 | if (self.triggerOperation) { 21 | self.triggerOperation(); 22 | } 23 | } 24 | 25 | @end 26 | 27 | @interface VIDisplayTriggerMachine() 28 | 29 | @property (nonatomic, strong) CADisplayLink *displayLink; 30 | @property (nonatomic, strong) VIDisplayTriggerObject *triggerObject; 31 | 32 | @end 33 | 34 | @implementation VIDisplayTriggerMachine 35 | 36 | - (void)dealloc { 37 | [_displayLink invalidate]; 38 | } 39 | 40 | - (instancetype)initWithTriggerOperation:(TriggerOperation)triggerOperation 41 | { 42 | self = [super init]; 43 | if (self) { 44 | _triggerObject.triggerOperation = triggerOperation; 45 | } 46 | return self; 47 | } 48 | 49 | - (instancetype)init 50 | { 51 | self = [super init]; 52 | if (self) { 53 | _triggerObject = [VIDisplayTriggerObject new]; 54 | 55 | _displayLink = [CADisplayLink displayLinkWithTarget:_triggerObject selector:@selector(trigger)]; 56 | [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; 57 | _preferredFramesPerSecond = 30; 58 | } 59 | return self; 60 | } 61 | 62 | - (void)setTriggerOperation:(TriggerOperation)triggerOperation { 63 | _triggerOperation = [triggerOperation copy]; 64 | self.triggerObject.triggerOperation = triggerOperation; 65 | } 66 | 67 | - (void)setPreferredFramesPerSecond:(NSInteger)preferredFramesPerSecond { 68 | _preferredFramesPerSecond = preferredFramesPerSecond; 69 | if (@available(iOS 10.0, *)) { 70 | self.displayLink.preferredFramesPerSecond = preferredFramesPerSecond; 71 | } else { 72 | self.displayLink.frameInterval = preferredFramesPerSecond; 73 | } 74 | } 75 | 76 | - (void)start { 77 | self.displayLink.paused = NO; 78 | } 79 | 80 | - (void)pause { 81 | self.displayLink.paused = YES; 82 | } 83 | 84 | @end 85 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIRangeContentView.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeContentView.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | @import CoreMedia; 11 | 12 | @interface VIRangeContentView : UIView 13 | 14 | - (void)reloadData; 15 | - (void)updateDataIfNeed; 16 | 17 | @end 18 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIRangeContentView.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeContentView.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIRangeContentView.h" 10 | 11 | @implementation VIRangeContentView 12 | 13 | - (void)reloadData { 14 | NSAssert(NO, @"Subclass implement"); 15 | } 16 | 17 | - (void)updateDataIfNeed { 18 | NSAssert(NO, @"Subclass implement"); 19 | } 20 | 21 | @end 22 | 23 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIRangeEarView.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeEarView.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface VIRangeEarView : UIView 12 | 13 | @property (nonatomic, strong, readonly) UIImageView *imageView; 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIRangeEarView.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeEarView.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIRangeEarView.h" 10 | 11 | @interface VIRangeEarView() 12 | 13 | @property (nonatomic, strong, readwrite) UIImageView *imageView; 14 | 15 | @end 16 | 17 | @implementation VIRangeEarView 18 | 19 | - (instancetype)initWithFrame:(CGRect)frame 20 | { 21 | self = [super initWithFrame:frame]; 22 | if (self) { 23 | [self commonInit]; 24 | } 25 | return self; 26 | } 27 | 28 | - (instancetype)initWithCoder:(NSCoder *)coder 29 | { 30 | self = [super initWithCoder:coder]; 31 | if (self) { 32 | [self commonInit]; 33 | } 34 | return self; 35 | } 36 | 37 | - (void)commonInit { 38 | self.clipsToBounds = NO; 39 | UIImageView *imageView = [[UIImageView alloc] init]; 40 | imageView.translatesAutoresizingMaskIntoConstraints = NO; 41 | [self addSubview:imageView]; 42 | self.imageView = imageView; 43 | 44 | [imageView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES; 45 | [imageView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES; 46 | } 47 | 48 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { 49 | CGRect frame = self.bounds; 50 | UIEdgeInsets insets = UIEdgeInsetsMake(10, 15, 15, 10); 51 | CGRect extendFrame = CGRectMake(-insets.left, -insets.top, frame.size.width + insets.left + insets.right, frame.size.height + insets.top + insets.bottom); 52 | if (CGRectContainsPoint(extendFrame, point)) { 53 | return YES; 54 | } 55 | 56 | return NO; 57 | } 58 | 59 | @end 60 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIRangeView.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeView.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | @import CoreMedia; 11 | #import "VIRangeContentView.h" 12 | #import "VIRangeEarView.h" 13 | 14 | @class VIRangeView; 15 | 16 | @protocol VIRangeViewDelegate 17 | 18 | - (void)rangeView:(VIRangeView *)rangeView didChangeActive:(BOOL)isActive; 19 | 20 | - (void)rangeViewBeginUpdateLeft:(VIRangeView *)rangeView; 21 | - (void)rangeView:(VIRangeView *)rangeView updateLeftOffset:(CGFloat)offset isAuto:(BOOL)isAuto; 22 | - (void)rangeViewEndUpdateLeftOffset:(VIRangeView *)rangeView; 23 | 24 | - (void)rangeViewBeginUpdateRight:(VIRangeView *)rangeView; 25 | - (void)rangeView:(VIRangeView *)rangeView updateRightOffset:(CGFloat)offset isAuto:(BOOL)isAuto; 26 | - (void)rangeViewEndUpdateRightOffset:(VIRangeView *)rangeView; 27 | 28 | @end 29 | 30 | @interface VIRangeView : UIView 31 | 32 | - (instancetype)initWithContentView:(VIRangeContentView *)contentView startTime:(CMTime)startTime endTime:(CMTime)endTime; 33 | 34 | @property (nonatomic, weak) id delegate; 35 | 36 | // Selection 37 | @property (nonatomic, strong, readonly) VIRangeEarView *leftEarView; 38 | @property (nonatomic, strong, readonly) VIRangeEarView *rightEarView; 39 | @property (nonatomic, strong, readonly) UIView *backgroundView; 40 | @property (nonatomic, strong, readonly) UIImageView *coverTopLineView; 41 | @property (nonatomic, strong, readonly) UIImageView *coverBottomLineView; 42 | @property (nonatomic, strong, readonly) UIView *disableCoverView; 43 | 44 | // Content 45 | @property (nonatomic) CGFloat contentHeight; 46 | - (CGFloat)contentWidth; 47 | @property (nonatomic) UIEdgeInsets contentInset; 48 | 49 | @property (nonatomic, strong) VIRangeContentView *contentView; 50 | @property (nonatomic) CMTime startTime; 51 | @property (nonatomic) CMTime endTime; 52 | 53 | @property (nonatomic) CGFloat widthPerSecond; 54 | 55 | @property (nonatomic) CMTime minDuration; 56 | @property (nonatomic) CMTime maxDuration; 57 | 58 | // 裁剪掉部分时间,应用场景:设置了转场,可能会有部分时间被吃掉 59 | @property (nonatomic) CMTime leftInsetDuration; 60 | @property (nonatomic) CMTime rightInsetDuration; 61 | 62 | // 处理 rangeView 的间距 63 | @property (nonatomic) CGFloat leftContentWidthInset; 64 | @property (nonatomic) CGFloat rightContentWidthInset; 65 | 66 | @property (nonatomic, getter=isActived, readonly) BOOL active; 67 | - (void)activeEarAnimated:(BOOL)animated; 68 | - (void)inactiveEarAnimated:(BOOL)animated; 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIRangeView.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIRangeView.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIRangeView.h" 10 | #import "VIAutoScroller.h" 11 | #import "UIView+ConstraintHolder.h" 12 | 13 | @interface VIRangeView() 14 | 15 | @property (nonatomic, strong) UIView *contentContainerView; 16 | 17 | @property (nonatomic, strong, readwrite) VIRangeEarView *leftEarView; 18 | @property (nonatomic, strong, readwrite) VIRangeEarView *rightEarView; 19 | @property (nonatomic, strong, readwrite) UIView *backgroundView; 20 | @property (nonatomic, strong, readwrite) UIView *coverView; 21 | @property (nonatomic, strong, readwrite) UIImageView *coverTopLineView; 22 | @property (nonatomic, strong, readwrite) UIImageView *coverBottomLineView; 23 | 24 | // Helper 25 | @property (nonatomic, strong) VIAutoScroller *autoScroller; 26 | @property (nonatomic) CGPoint panGesturePreviousTranslation; 27 | 28 | @end 29 | 30 | @implementation VIRangeView 31 | 32 | #pragma mark - Life Cycle 33 | 34 | - (instancetype)initWithContentView:(VIRangeContentView *)contentView startTime:(CMTime)startTime endTime:(CMTime)endTime { 35 | self = [self initWithFrame:CGRectZero]; 36 | if (self) { 37 | _startTime = startTime; 38 | _endTime = endTime; 39 | _maxDuration = endTime; 40 | [self setContentView:contentView]; 41 | } 42 | return self; 43 | } 44 | 45 | - (instancetype)initWithFrame:(CGRect)frame 46 | { 47 | self = [super initWithFrame:frame]; 48 | if (self) { 49 | [self commonInit]; 50 | } 51 | return self; 52 | } 53 | 54 | - (instancetype)initWithCoder:(NSCoder *)coder 55 | { 56 | self = [super initWithCoder:coder]; 57 | if (self) { 58 | [self commonInit]; 59 | } 60 | return self; 61 | } 62 | 63 | - (void)commonInit { 64 | _contentHeight = 45; 65 | _widthPerSecond = 50; 66 | _startTime = kCMTimeZero; 67 | _endTime = kCMTimeZero; 68 | _minDuration = CMTimeMake(600, 600); 69 | _maxDuration = kCMTimeIndefinite; 70 | _leftInsetDuration = kCMTimeZero; 71 | _rightInsetDuration = kCMTimeZero; 72 | _autoScroller = [VIAutoScroller new]; 73 | _contentInset = UIEdgeInsetsMake(0, 15, 0, 15); 74 | 75 | 76 | UIView *backgroundView = [UIView new]; 77 | backgroundView.userInteractionEnabled = NO; 78 | backgroundView.translatesAutoresizingMaskIntoConstraints = NO; 79 | [self addSubview:backgroundView]; 80 | self.backgroundView = backgroundView; 81 | 82 | UIView *contentContainerView = [UIView new]; 83 | contentContainerView.clipsToBounds = YES; 84 | contentContainerView.userInteractionEnabled = NO; 85 | contentContainerView.translatesAutoresizingMaskIntoConstraints = NO; 86 | [self addSubview:contentContainerView]; 87 | self.contentContainerView = contentContainerView; 88 | 89 | VIRangeEarView *leftEarView = [VIRangeEarView new]; 90 | leftEarView.translatesAutoresizingMaskIntoConstraints = NO; 91 | [self addSubview:leftEarView]; 92 | self.leftEarView = leftEarView; 93 | 94 | VIRangeEarView *rightEarView = [VIRangeEarView new]; 95 | rightEarView.translatesAutoresizingMaskIntoConstraints = NO; 96 | [self addSubview:rightEarView]; 97 | self.rightEarView = rightEarView; 98 | 99 | 100 | UIView *coverView = [UIView new]; 101 | coverView.translatesAutoresizingMaskIntoConstraints = NO; 102 | coverView.userInteractionEnabled = NO; 103 | coverView.clipsToBounds = NO; 104 | [self addSubview:coverView]; 105 | self.coverView = coverView; 106 | 107 | UIImageView *coverTopLineView = [UIImageView new]; 108 | coverTopLineView.translatesAutoresizingMaskIntoConstraints = NO; 109 | [self.coverView addSubview:coverTopLineView]; 110 | self.coverTopLineView = coverTopLineView; 111 | 112 | UIImageView *coverBottomLineView = [UIImageView new]; 113 | coverBottomLineView.translatesAutoresizingMaskIntoConstraints = NO; 114 | [self.coverView addSubview:coverBottomLineView]; 115 | self.coverBottomLineView = coverBottomLineView; 116 | 117 | // Layout 118 | leftEarView.vi_constraints = 119 | @[[leftEarView.leftAnchor constraintEqualToAnchor:self.leftAnchor], 120 | [leftEarView.topAnchor constraintEqualToAnchor:self.topAnchor], 121 | [leftEarView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], 122 | [leftEarView.widthAnchor constraintEqualToConstant:self.contentInset.left] 123 | ]; 124 | [NSLayoutConstraint activateConstraints:leftEarView.vi_constraints]; 125 | 126 | rightEarView.vi_constraints = 127 | @[[rightEarView.rightAnchor constraintEqualToAnchor:self.rightAnchor], 128 | [rightEarView.topAnchor constraintEqualToAnchor:self.topAnchor], 129 | [rightEarView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], 130 | [rightEarView.widthAnchor constraintEqualToConstant:self.contentInset.right] 131 | ]; 132 | [NSLayoutConstraint activateConstraints:rightEarView.vi_constraints]; 133 | 134 | [NSLayoutConstraint activateConstraints: 135 | @[[backgroundView.leftAnchor constraintEqualToAnchor:leftEarView.rightAnchor], 136 | [backgroundView.topAnchor constraintEqualToAnchor:self.topAnchor], 137 | [backgroundView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], 138 | [backgroundView.rightAnchor constraintEqualToAnchor:rightEarView.leftAnchor] 139 | ]]; 140 | 141 | contentContainerView.vi_constraints = 142 | @[[contentContainerView.leftAnchor constraintEqualToAnchor:leftEarView.rightAnchor], 143 | [contentContainerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:self.contentInset.top], 144 | [contentContainerView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.contentInset.bottom], 145 | [contentContainerView.rightAnchor constraintEqualToAnchor:rightEarView.leftAnchor] 146 | ]; 147 | [NSLayoutConstraint activateConstraints:contentContainerView.vi_constraints]; 148 | 149 | [NSLayoutConstraint activateConstraints: 150 | @[[coverView.leftAnchor constraintEqualToAnchor:leftEarView.rightAnchor], 151 | [coverView.topAnchor constraintEqualToAnchor:self.topAnchor], 152 | [coverView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], 153 | [coverView.rightAnchor constraintEqualToAnchor:rightEarView.leftAnchor] 154 | ]]; 155 | 156 | [NSLayoutConstraint activateConstraints: 157 | @[[coverTopLineView.leftAnchor constraintEqualToAnchor:coverView.leftAnchor], 158 | [coverTopLineView.bottomAnchor constraintEqualToAnchor:coverView.topAnchor], 159 | [coverTopLineView.rightAnchor constraintEqualToAnchor:coverView.rightAnchor] 160 | ]]; 161 | 162 | [NSLayoutConstraint activateConstraints: 163 | @[[coverBottomLineView.leftAnchor constraintEqualToAnchor:coverView.leftAnchor], 164 | [coverBottomLineView.topAnchor constraintEqualToAnchor:coverView.bottomAnchor], 165 | [coverBottomLineView.rightAnchor constraintEqualToAnchor:coverView.rightAnchor] 166 | ]]; 167 | 168 | // Gesture 169 | UIPanGestureRecognizer *panLeftGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panEarAction:)]; 170 | [leftEarView addGestureRecognizer:panLeftGesture]; 171 | UIPanGestureRecognizer *panRightGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panEarAction:)]; 172 | [rightEarView addGestureRecognizer:panRightGesture]; 173 | 174 | [self inactiveEarAnimated:NO]; 175 | } 176 | 177 | - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { 178 | CGRect frame = self.bounds; 179 | UIEdgeInsets insets = UIEdgeInsetsMake(10, 15, 15, 10); 180 | CGRect extendFrame = CGRectMake(-insets.left, -insets.top, frame.size.width + insets.left + insets.right, frame.size.height + insets.top + insets.bottom); 181 | if (CGRectContainsPoint(extendFrame, point)) { 182 | return YES; 183 | } 184 | 185 | return NO; 186 | } 187 | 188 | - (CGSize)intrinsicContentSize { 189 | CGFloat width = [self contentWidth] + self.contentInset.left + self.contentInset.right; 190 | return CGSizeMake(width, self.contentHeight); 191 | } 192 | 193 | #pragma mark - Setter 194 | 195 | - (void)setContentHeight:(CGFloat)contentHeight { 196 | [self willChangeValueForKey:@"contentHeight"]; 197 | _contentHeight = contentHeight; 198 | [self didChangeValueForKey:@"contentHeight"]; 199 | [self invalidateIntrinsicContentSize]; 200 | } 201 | 202 | - (void)setContentInset:(UIEdgeInsets)contentInset { 203 | _contentInset = contentInset; 204 | 205 | [NSLayoutConstraint deactivateConstraints:self.contentContainerView.vi_constraints]; 206 | self.contentContainerView.vi_constraints = 207 | @[[self.contentContainerView.leftAnchor constraintEqualToAnchor:self.leftEarView.rightAnchor], 208 | [self.contentContainerView.topAnchor constraintEqualToAnchor:self.topAnchor constant:self.contentInset.top], 209 | [self.contentContainerView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor constant:-self.contentInset.bottom], 210 | [self.contentContainerView.rightAnchor constraintEqualToAnchor:self.rightEarView.leftAnchor] 211 | ]; 212 | [NSLayoutConstraint activateConstraints:self.contentContainerView.vi_constraints]; 213 | 214 | [NSLayoutConstraint deactivateConstraints:self.leftEarView.vi_constraints]; 215 | self.leftEarView.vi_constraints = 216 | @[[self.leftEarView.leftAnchor constraintEqualToAnchor:self.leftAnchor], 217 | [self.leftEarView.topAnchor constraintEqualToAnchor:self.topAnchor], 218 | [self.leftEarView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], 219 | [self.leftEarView.widthAnchor constraintEqualToConstant:self.contentInset.left] 220 | ]; 221 | [NSLayoutConstraint activateConstraints:self.leftEarView.vi_constraints]; 222 | 223 | [NSLayoutConstraint deactivateConstraints:self.rightEarView.vi_constraints]; 224 | self.rightEarView.vi_constraints = 225 | @[[self.rightEarView.rightAnchor constraintEqualToAnchor:self.rightAnchor], 226 | [self.rightEarView.topAnchor constraintEqualToAnchor:self.topAnchor], 227 | [self.rightEarView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor], 228 | [self.rightEarView.widthAnchor constraintEqualToConstant:self.contentInset.right] 229 | ]; 230 | [NSLayoutConstraint activateConstraints:self.rightEarView.vi_constraints]; 231 | } 232 | 233 | - (void)setStartTime:(CMTime)startTime { 234 | [self willChangeValueForKey:@"startTime"]; 235 | _startTime = startTime; 236 | [self didChangeValueForKey:@"startTime"]; 237 | [self timeDidChange]; 238 | [self invalidateIntrinsicContentSize]; 239 | } 240 | 241 | - (void)setEndTime:(CMTime)endTime { 242 | [self willChangeValueForKey:@"endTime"]; 243 | _endTime = endTime; 244 | [self didChangeValueForKey:@"endTime"]; 245 | [self timeDidChange]; 246 | [self invalidateIntrinsicContentSize]; 247 | } 248 | 249 | - (void)setWidthPerSecond:(CGFloat)widthPerSecond { 250 | [self willChangeValueForKey:@"widthPerSecond"]; 251 | _widthPerSecond = widthPerSecond; 252 | [self didChangeValueForKey:@"widthPerSecond"]; 253 | [self invalidateIntrinsicContentSize]; 254 | } 255 | 256 | - (void)setContentView:(VIRangeContentView *)contentView { 257 | contentView.translatesAutoresizingMaskIntoConstraints = NO; 258 | [self.contentView removeFromSuperview]; 259 | _contentView = contentView; 260 | [self.contentContainerView addSubview:contentView]; 261 | 262 | CGFloat xOffset = (CMTimeGetSeconds(self.startTime) + CMTimeGetSeconds(self.leftInsetDuration)) * self.widthPerSecond; 263 | contentView.vi_constraints = 264 | @[[contentView.topAnchor constraintEqualToAnchor:self.contentContainerView.topAnchor], 265 | [contentView.bottomAnchor constraintEqualToAnchor:self.contentContainerView.bottomAnchor], 266 | [contentView.leftAnchor constraintEqualToAnchor:self.contentContainerView.leftAnchor constant:xOffset], // 处理 rangeView 的间距 267 | [contentView.rightAnchor constraintEqualToAnchor:self.contentContainerView.rightAnchor constant:self.contentInset.right] // 处理 rangeView 的间距 268 | ]; 269 | [NSLayoutConstraint activateConstraints:contentView.vi_constraints]; 270 | } 271 | 272 | #pragma mark - Public 273 | 274 | - (CGFloat)contentWidth { 275 | CMTime duration = CMTimeSubtract(self.endTime, self.startTime); 276 | CGFloat width = CMTimeGetSeconds(duration) * self.widthPerSecond; 277 | width = width - self.leftContentWidthInset - self.rightContentWidthInset; // 处理 rangeView 的间距 278 | return width; 279 | } 280 | 281 | - (BOOL)isActived { 282 | return self.leftEarView.userInteractionEnabled; 283 | } 284 | 285 | - (void)activeEarAnimated:(BOOL)animated { 286 | // Move to top layer 287 | [self.superview addSubview:self]; 288 | 289 | self.leftEarView.userInteractionEnabled = YES; 290 | self.rightEarView.userInteractionEnabled = YES; 291 | 292 | if ([self.delegate respondsToSelector:@selector(rangeView:didChangeActive:)]) { 293 | [self.delegate rangeView:self didChangeActive:self.isActived]; 294 | } 295 | 296 | void(^operations)(void) = ^{ 297 | self.coverView.alpha = 1.0; 298 | self.backgroundView.alpha = 1.0; 299 | self.leftEarView.alpha = 1.0; 300 | self.rightEarView.alpha = 1.0; 301 | }; 302 | if (animated) { 303 | [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:operations completion:nil]; 304 | } else { 305 | operations(); 306 | } 307 | } 308 | 309 | - (void)inactiveEarAnimated:(BOOL)animated { 310 | self.leftEarView.userInteractionEnabled = NO; 311 | self.rightEarView.userInteractionEnabled = NO; 312 | 313 | if ([self.delegate respondsToSelector:@selector(rangeView:didChangeActive:)]) { 314 | [self.delegate rangeView:self didChangeActive:self.isActived]; 315 | } 316 | 317 | void(^operations)(void) = ^{ 318 | self.coverView.alpha = 0.0; 319 | self.backgroundView.alpha = 0.0; 320 | self.leftEarView.alpha = 0.0; 321 | self.rightEarView.alpha = 0.0; 322 | }; 323 | if (animated) { 324 | [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState | UIViewAnimationOptionCurveEaseInOut animations:operations completion:nil]; 325 | } else { 326 | operations(); 327 | } 328 | } 329 | 330 | #pragma mark - Gesture Action Helper 331 | 332 | - (void)panEarAction:(UIPanGestureRecognizer *)gesture { 333 | if (gesture.state == UIGestureRecognizerStateBegan) { 334 | if (gesture.view == self.rightEarView) { 335 | [self.delegate rangeViewBeginUpdateRight:self]; 336 | } else { 337 | [self.delegate rangeViewBeginUpdateLeft:self]; 338 | } 339 | } 340 | 341 | CGPoint windowLocation = [[gesture.view superview] convertPoint:gesture.view.center toView:self.window]; 342 | CGRect windowBounds = self.window.bounds; 343 | BOOL shouldAutoScroll = (windowLocation.x < self.autoScroller.autoScrollInset && self.panGesturePreviousTranslation.x < 0) || 344 | (windowLocation.x > (windowBounds.size.width - self.autoScroller.autoScrollInset) && self.panGesturePreviousTranslation.x > 0); 345 | if (shouldAutoScroll) { 346 | [self autoScrollEar:gesture]; 347 | [self.autoScroller.triggerMachine start]; 348 | } else { 349 | [self.autoScroller.triggerMachine pause]; 350 | [self.autoScroller cleanUpAutoScrollValues]; 351 | } 352 | 353 | CGPoint translation = [gesture translationInView:gesture.view]; 354 | BOOL outOfControl = (windowLocation.x < self.autoScroller.earEdgeInset && self.panGesturePreviousTranslation.x > translation.x) || 355 | (windowLocation.x > (windowBounds.size.width - self.autoScroller.earEdgeInset) && self.panGesturePreviousTranslation.x < translation.x); 356 | if (!outOfControl) { 357 | [self normalPanEar:gesture]; 358 | } 359 | 360 | if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) { 361 | [self.autoScroller cleanUpAutoScrollValues]; 362 | if (gesture.view == self.rightEarView) { 363 | [self.delegate rangeViewEndUpdateRightOffset:self]; 364 | } else { 365 | [self.delegate rangeViewEndUpdateLeftOffset:self]; 366 | } 367 | } 368 | } 369 | 370 | - (void)autoScrollEar:(UIPanGestureRecognizer *)gesture { 371 | CGPoint windowLocation = [gesture.view.superview convertPoint:gesture.view.center toView:self.window]; 372 | CGRect windowBounds = self.window.bounds; 373 | 374 | if (windowLocation.x > (windowBounds.size.width - self.autoScroller.autoScrollInset)) { 375 | CGFloat scrollInset = self.autoScroller.autoScrollInset - (windowBounds.size.width - windowLocation.x); 376 | self.autoScroller.autoScrollSpeed = MIN(scrollInset, self.autoScroller.autoScrollInset) * 0.1; 377 | } else if (windowLocation.x < self.autoScroller.autoScrollInset) { 378 | CGFloat scrollInset = self.autoScroller.autoScrollInset - windowLocation.x; 379 | self.autoScroller.autoScrollSpeed = -MIN(scrollInset, self.autoScroller.autoScrollInset) * 0.1; 380 | } 381 | 382 | if (gesture.view == self.rightEarView) { 383 | if (self.autoScroller.autoScrollType != VIAutoScrollerTypeRight) { 384 | self.autoScroller.autoScrollType = VIAutoScrollerTypeRight; 385 | __weak typeof(self)weakSelf = self; 386 | self.autoScroller.triggerMachine.triggerOperation = ^{ 387 | __strong __typeof(weakSelf)strongSelf = weakSelf; 388 | if (!strongSelf) { return; } 389 | [strongSelf expandRightEarWithWidth:strongSelf.autoScroller.autoScrollSpeed isAuto:YES]; 390 | }; 391 | } 392 | } else { 393 | if (self.autoScroller.autoScrollType != VIAutoScrollerTypeLeft) { 394 | self.autoScroller.autoScrollType = VIAutoScrollerTypeLeft; 395 | __weak typeof(self)weakSelf = self; 396 | self.autoScroller.triggerMachine.triggerOperation = ^{ 397 | __strong __typeof(weakSelf)strongSelf = weakSelf; 398 | if (!strongSelf) { return; } 399 | [strongSelf expandLeftEarWithWidth:strongSelf.autoScroller.autoScrollSpeed isAuto:YES]; 400 | }; 401 | } 402 | } 403 | } 404 | 405 | - (void)normalPanEar:(UIPanGestureRecognizer *)gesture { 406 | CGPoint translation = [gesture translationInView:gesture.view]; 407 | CGFloat offset = translation.x - self.panGesturePreviousTranslation.x; 408 | CGFloat actulOffset = offset; 409 | if (gesture.view == self.rightEarView) { 410 | actulOffset = [self expandRightEarWithWidth:offset isAuto:NO]; 411 | } else { 412 | actulOffset = -[self expandLeftEarWithWidth:offset isAuto:NO]; 413 | } 414 | 415 | CGPoint previousTranslation = self.panGesturePreviousTranslation; 416 | previousTranslation.x += actulOffset; 417 | self.panGesturePreviousTranslation = previousTranslation; 418 | 419 | if (gesture.state == UIGestureRecognizerStateEnded || gesture.state == UIGestureRecognizerStateCancelled) { 420 | self.panGesturePreviousTranslation = CGPointZero; 421 | } 422 | } 423 | 424 | - (CGFloat)expandLeftEarWithWidth:(CGFloat)width isAuto:(BOOL)isAuto { 425 | CGFloat previousWidth = [self contentWidth]; 426 | [self expandWithContentWidth:-width isLeft:YES]; 427 | CGFloat offset = [self contentWidth] - previousWidth; 428 | [self invalidateIntrinsicContentSize]; 429 | [self.delegate rangeView:self updateLeftOffset:-offset isAuto:isAuto]; 430 | self.leftEarView.imageView.highlighted = [self isReachHead]; 431 | //[self changeTimeLabelIsLeft:YES]; 432 | return offset; 433 | } 434 | 435 | - (CGFloat)expandRightEarWithWidth:(CGFloat)width isAuto:(BOOL)isAuto { 436 | CGFloat previousWidth = [self contentWidth]; 437 | [self expandWithContentWidth:width isLeft:NO]; 438 | CGFloat offset = [self contentWidth] - previousWidth; 439 | [self invalidateIntrinsicContentSize]; 440 | [self.delegate rangeView:self updateRightOffset:offset isAuto:isAuto]; 441 | self.leftEarView.imageView.highlighted = [self isReachEnd]; 442 | //[self changeTimeLabelIsLeft:NO]; 443 | return offset; 444 | } 445 | 446 | - (BOOL)isReachHead { 447 | if (CMTimeCompare(self.startTime, CMTimeMake(1, 30)) <= 0) { 448 | return YES; 449 | } 450 | return NO; 451 | } 452 | 453 | - (BOOL)isReachEnd { 454 | CMTime maxDuration = CMTimeSubtract(self.maxDuration, CMTimeMake(1, 30)); 455 | if (CMTimeCompare(self.endTime, maxDuration) >= 0) { 456 | return YES; 457 | } 458 | return NO; 459 | } 460 | 461 | - (void)expandWithContentWidth:(CGFloat)width isLeft:(BOOL)isLeft { 462 | NSTimeInterval seconds = width / self.widthPerSecond; 463 | if (isLeft) { 464 | NSTimeInterval startSeconds = MAX(0, CMTimeGetSeconds(self.startTime) - seconds); 465 | startSeconds = MIN(startSeconds, CMTimeGetSeconds(self.endTime) - CMTimeGetSeconds(self.minDuration)); 466 | self.startTime = CMTimeMakeWithSeconds(startSeconds, 600); 467 | } else { 468 | NSTimeInterval maxSeconds = CMTimeGetSeconds(self.maxDuration); 469 | NSTimeInterval endSeconds = MAX(MIN(CMTimeGetSeconds(self.endTime) + seconds, maxSeconds), CMTimeGetSeconds(self.startTime) + CMTimeGetSeconds(self.minDuration)); 470 | self.endTime = CMTimeMakeWithSeconds(endSeconds, 600); 471 | } 472 | } 473 | 474 | - (void)timeDidChange { 475 | [NSLayoutConstraint deactivateConstraints:self.contentView.vi_constraints]; 476 | CGFloat xOffset = (CMTimeGetSeconds(self.startTime) + CMTimeGetSeconds(self.leftInsetDuration)) * self.widthPerSecond; 477 | self.contentView.vi_constraints = 478 | @[[self.contentView.topAnchor constraintEqualToAnchor:self.contentContainerView.topAnchor], 479 | [self.contentView.bottomAnchor constraintEqualToAnchor:self.contentContainerView.bottomAnchor], 480 | [self.contentView.leftAnchor constraintEqualToAnchor:self.contentContainerView.leftAnchor constant:-xOffset], // 处理 rangeView 的间距 481 | [self.contentView.rightAnchor constraintEqualToAnchor:self.contentContainerView.rightAnchor constant:self.contentInset.right] // 处理 rangeView 的间距 482 | ]; 483 | [NSLayoutConstraint activateConstraints:self.contentView.vi_constraints]; 484 | } 485 | 486 | @end 487 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VITimelineView.h: -------------------------------------------------------------------------------- 1 | // 2 | // VITimelineView.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/28. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "VIRangeView.h" 11 | 12 | @class VITimelineView, VIEditPlayButton; 13 | 14 | NS_ASSUME_NONNULL_BEGIN 15 | 16 | @protocol VITimelineViewDelegate 17 | 18 | - (void)timelineView:(VITimelineView *)view didChangeActive:(BOOL)isActive; 19 | 20 | @end 21 | 22 | @interface VITimelineView : UIView 23 | 24 | @property (nonatomic, strong, readonly) UIScrollView *scrollView; 25 | @property (nonatomic, strong, readonly) UIImageView *centerLineView; 26 | 27 | @property (nonatomic, weak) id delegate; 28 | @property (nonatomic, weak) id rangeViewDelegate; 29 | 30 | @property (nonatomic, strong, readonly) NSMutableArray *rangeViews; 31 | - (void)reloadWithRangeViews:(NSArray *)rangeViews; 32 | - (void)insertRangeView:(VIRangeView *)view atIndex:(NSInteger)index; 33 | - (void)removeRangeViewAtIndex:(NSInteger)index animated:(BOOL)animated completion:(void(^)(void))completion; 34 | - (void)removeCurrentActivedRangeViewCompletion:(void(^)(void))completion; 35 | 36 | @property (nonatomic) CGFloat rangeViewLeftInset; 37 | @property (nonatomic) CGFloat rangeViewRightInset; 38 | @property (nonatomic) CGFloat contentWidthPerSecond; 39 | 40 | 41 | // 真实的 widthPerSeconds, 和 contentWidthPerSecond 的区别在于,会用上 rangeViewLeftInset 重新计算 42 | - (CGFloat)timelineWidthPerSeconds; 43 | - (void)adjustScrollViewOffsetAtTime:(CMTime)time; 44 | 45 | - (void)scrollToStartOfRangeView:(VIRangeView *)rangeView animated:(BOOL)animated completion:(nullable void(^)(void))completion; 46 | - (void)scrollToEndOfRangeView:(VIRangeView *)rangeView animated:(BOOL)animated completion:(nullable void(^)(void))completion; 47 | - (void)scrollToContentOffset:(CGPoint)contentOffset animated:(BOOL)animated completion:(void(^)(void))completion; 48 | 49 | - (CGFloat)calculateOffsetXAtTime:(CMTime)time; 50 | - (NSInteger)getRangeViewIndexAtTime:(CMTime)time; 51 | - (CMTime)calculateTimeAtOffsetX:(CGFloat)offsetX; 52 | - (NSInteger)getRangeViewIndexAtOffsetX:(CGFloat)offsetX; 53 | 54 | - (void)resignVideoRangeView; 55 | 56 | @end 57 | 58 | NS_ASSUME_NONNULL_END 59 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VITimelineView.m: -------------------------------------------------------------------------------- 1 | // 2 | // VITimelineView.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/28. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VITimelineView.h" 10 | #import "UIView+ConstraintHolder.h" 11 | 12 | @interface VITimelineView() 13 | 14 | @property (nonatomic, strong) UIScrollView *scrollView; 15 | @property (nonatomic, strong) UIView *scrollContentView; 16 | @property (nonatomic, strong) UIView *scrollRangeContentView; 17 | @property (nonatomic, strong) UIImageView *centerLineView; 18 | 19 | 20 | @property (nonatomic, strong) NSMutableArray *rangeViews; 21 | 22 | 23 | // Configuration 24 | @property (nonatomic) CGFloat videoRangeViewEarWidth; 25 | 26 | // Helper 27 | 28 | @property (nonatomic, weak) id previousScrollViewDelegate; 29 | 30 | @end 31 | 32 | @implementation VITimelineView 33 | 34 | #pragma mark - Life Cycle 35 | 36 | - (void)dealloc { 37 | [self.scrollView removeObserver:self forKeyPath:@"contentOffset"]; 38 | } 39 | 40 | - (instancetype)initWithFrame:(CGRect)frame 41 | { 42 | self = [super initWithFrame:frame]; 43 | if (self) { 44 | [self commonInit]; 45 | } 46 | return self; 47 | } 48 | 49 | - (instancetype)initWithCoder:(NSCoder *)coder 50 | { 51 | self = [super initWithCoder:coder]; 52 | if (self) { 53 | [self commonInit]; 54 | } 55 | return self; 56 | } 57 | 58 | - (void)commonInit { 59 | _videoRangeViewEarWidth = 15; 60 | _rangeViews = [NSMutableArray array]; 61 | _contentWidthPerSecond = 50; 62 | 63 | UIView *contentBackgroundView = [UIView new]; 64 | contentBackgroundView.translatesAutoresizingMaskIntoConstraints = NO; 65 | [self addSubview:contentBackgroundView]; 66 | 67 | UIScrollView *scrollView = [UIScrollView new]; 68 | scrollView.translatesAutoresizingMaskIntoConstraints = NO; 69 | scrollView.showsHorizontalScrollIndicator = NO; 70 | scrollView.showsVerticalScrollIndicator = NO; 71 | [self addSubview:scrollView]; 72 | self.scrollView = scrollView; 73 | 74 | UIView *scrollContentView = [UIView new]; 75 | scrollContentView.translatesAutoresizingMaskIntoConstraints = NO; 76 | [scrollView addSubview:scrollContentView]; 77 | self.scrollContentView = scrollContentView; 78 | 79 | UIView *scrollRangeContentView = [UIView new]; 80 | scrollRangeContentView.translatesAutoresizingMaskIntoConstraints = NO; 81 | [scrollContentView addSubview:scrollRangeContentView]; 82 | self.scrollRangeContentView = scrollRangeContentView; 83 | 84 | UIImageView *centerLineView = [[UIImageView alloc] init]; 85 | centerLineView.translatesAutoresizingMaskIntoConstraints = NO; 86 | centerLineView.backgroundColor = [UIColor whiteColor]; 87 | [self addSubview:centerLineView]; 88 | self.centerLineView = centerLineView; 89 | 90 | // Layout 91 | 92 | [scrollView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES; 93 | [scrollView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES; 94 | [scrollView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES; 95 | [scrollView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES; 96 | 97 | [scrollContentView.leftAnchor constraintEqualToAnchor:scrollView.leftAnchor].active = YES; 98 | [scrollContentView.rightAnchor constraintEqualToAnchor:scrollView.rightAnchor].active = YES; 99 | [scrollContentView.topAnchor constraintEqualToAnchor:scrollView.topAnchor].active = YES; 100 | [scrollContentView.bottomAnchor constraintEqualToAnchor:scrollView.bottomAnchor].active = YES; 101 | [scrollContentView.heightAnchor constraintEqualToAnchor:scrollView.heightAnchor].active = YES; 102 | 103 | [scrollRangeContentView.leftAnchor constraintEqualToAnchor:scrollContentView.leftAnchor].active = YES; 104 | [scrollRangeContentView.rightAnchor constraintEqualToAnchor:scrollContentView.rightAnchor].active = YES; 105 | [scrollRangeContentView.centerYAnchor constraintEqualToAnchor:scrollContentView.centerYAnchor].active = YES; 106 | [scrollRangeContentView.heightAnchor constraintEqualToConstant:45].active = YES; 107 | 108 | [contentBackgroundView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES; 109 | [contentBackgroundView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES; 110 | [contentBackgroundView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES; 111 | [contentBackgroundView.heightAnchor constraintEqualToAnchor:scrollRangeContentView.heightAnchor].active = YES; 112 | 113 | [centerLineView.centerXAnchor constraintEqualToAnchor:self.centerXAnchor].active = YES; 114 | [centerLineView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor].active = YES; 115 | 116 | // Gesture 117 | UITapGestureRecognizer *tapContentGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapContentAction:)]; 118 | [self addGestureRecognizer:tapContentGesture]; 119 | 120 | [self.scrollView addObserver:self forKeyPath:@"contentOffset" options:NSKeyValueObservingOptionInitial|NSKeyValueObservingOptionNew context:nil]; 121 | } 122 | 123 | - (void)layoutSubviews { 124 | [super layoutSubviews]; 125 | CGFloat leftInset = self.bounds.size.width * 0.5 - self.rangeViews.firstObject.contentInset.left; 126 | CGFloat rightInset = self.bounds.size.width * 0.5 - self.rangeViews.lastObject.contentInset.left; 127 | self.scrollView.contentInset = UIEdgeInsetsMake(0, leftInset, 0, rightInset); 128 | [self displayRangeViewsIfNeed]; 129 | } 130 | 131 | #pragma mark - KVO 132 | 133 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 134 | if ([keyPath isEqualToString:@"contentOffset"]) { 135 | [self displayRangeViewsIfNeed]; 136 | } else { 137 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 138 | } 139 | } 140 | 141 | #pragma mark - Action 142 | 143 | - (void)tapContentAction:(UITapGestureRecognizer *)gesture { 144 | CGPoint point = [gesture locationInView:gesture.view]; 145 | BOOL tapOnVideoRangeView = NO; 146 | for (VIRangeView *view in self.rangeViews) { 147 | CGRect rect = [view.superview convertRect:view.frame toView:self]; 148 | if (CGRectContainsPoint(rect, point)) { 149 | tapOnVideoRangeView = YES; 150 | break; 151 | } 152 | } 153 | if (!tapOnVideoRangeView) { 154 | [self resignVideoRangeView]; 155 | } 156 | } 157 | 158 | - (void)tapRangeViewAction:(UITapGestureRecognizer *)gesture { 159 | if (gesture.state == UIGestureRecognizerStateEnded) { 160 | VIRangeView *rangeView = (VIRangeView *)gesture.view; 161 | if ([rangeView isKindOfClass:[VIRangeView class]]) { 162 | if (!rangeView.isActived) { 163 | [rangeView activeEarAnimated:YES]; 164 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 165 | if (obj != rangeView && obj.isActived) { 166 | [obj inactiveEarAnimated:YES]; 167 | } 168 | }]; 169 | } else { 170 | [rangeView inactiveEarAnimated:YES]; 171 | } 172 | [self.delegate timelineView:self didChangeActive:[self isActived]]; 173 | } 174 | } 175 | } 176 | 177 | #pragma mark - Public 178 | 179 | - (void)reloadWithRangeViews:(NSArray *)rangeViews { 180 | [self removeAllRangeViews]; 181 | [rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 182 | [self insertRangeView:obj atIndex:idx]; 183 | }]; 184 | } 185 | 186 | - (void)insertRangeView:(VIRangeView *)view atIndex:(NSInteger)index { 187 | view.widthPerSecond = self.contentWidthPerSecond; 188 | view.delegate = self; 189 | view.translatesAutoresizingMaskIntoConstraints = NO; 190 | [self.scrollRangeContentView addSubview:view]; 191 | UITapGestureRecognizer *tapContentGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapRangeViewAction:)]; 192 | [view addGestureRecognizer:tapContentGesture]; 193 | 194 | if (self.rangeViews.count == 0) { 195 | view.vi_constraints = 196 | @[[view.leftAnchor constraintEqualToAnchor:self.scrollRangeContentView.leftAnchor], 197 | [view.rightAnchor constraintEqualToAnchor:self.scrollRangeContentView.rightAnchor], 198 | [view.topAnchor constraintEqualToAnchor:self.scrollRangeContentView.topAnchor], 199 | [view.bottomAnchor constraintEqualToAnchor:self.scrollRangeContentView.bottomAnchor] 200 | ]; 201 | [NSLayoutConstraint activateConstraints:view.vi_constraints]; 202 | } else { 203 | void(^updateLeft)(VIRangeView *rangeView) = ^(VIRangeView *rangeView) { 204 | [rangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 205 | CGFloat offset = rangeView.contentInset.right + view.contentInset.left - (self.rangeViewLeftInset + self.rangeViewRightInset); 206 | NSLayoutConstraint *rightConstraint = [rangeView.rightAnchor constraintEqualToAnchor:view.leftAnchor constant:offset]; 207 | return rightConstraint; 208 | }]; 209 | }; 210 | 211 | void(^updateRight)(VIRangeView *rangeView) = ^(VIRangeView *rangeView) { 212 | [rangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 213 | return nil; 214 | }]; 215 | }; 216 | 217 | if (index >= self.rangeViews.count) { 218 | VIRangeView *leftRangeView = self.rangeViews.lastObject; 219 | updateLeft(leftRangeView); 220 | 221 | view.vi_constraints = 222 | @[[view.rightAnchor constraintEqualToAnchor:self.scrollRangeContentView.rightAnchor], 223 | [view.topAnchor constraintEqualToAnchor:self.scrollRangeContentView.topAnchor], 224 | [view.bottomAnchor constraintEqualToAnchor:self.scrollRangeContentView.bottomAnchor] 225 | ]; 226 | [NSLayoutConstraint activateConstraints:view.vi_constraints]; 227 | } else if (index == 0) { 228 | VIRangeView *rightRangeView = self.rangeViews.firstObject; 229 | updateRight(rightRangeView); 230 | 231 | CGFloat offset = (rightRangeView.contentInset.left + view.contentInset.right) - (self.rangeViewLeftInset + self.rangeViewRightInset); 232 | view.vi_constraints = 233 | @[[view.leftAnchor constraintEqualToAnchor:self.scrollRangeContentView.leftAnchor], 234 | [view.rightAnchor constraintEqualToAnchor:rightRangeView.leftAnchor constant:offset], 235 | [view.topAnchor constraintEqualToAnchor:self.scrollRangeContentView.topAnchor], 236 | [view.bottomAnchor constraintEqualToAnchor:self.scrollRangeContentView.bottomAnchor] 237 | ]; 238 | [NSLayoutConstraint activateConstraints:view.vi_constraints]; 239 | } else { 240 | VIRangeView *leftRangeView = self.rangeViews[index - 1]; 241 | VIRangeView *rightRangeView = self.rangeViews[index]; 242 | 243 | updateLeft(leftRangeView); 244 | 245 | updateRight(rightRangeView); 246 | 247 | 248 | CGFloat offset = (rightRangeView.contentInset.left + view.contentInset.right) - (self.rangeViewLeftInset + self.rangeViewRightInset); 249 | view.vi_constraints = 250 | @[[view.topAnchor constraintEqualToAnchor:self.scrollRangeContentView.topAnchor], 251 | [view.bottomAnchor constraintEqualToAnchor:self.scrollRangeContentView.bottomAnchor], 252 | [view.rightAnchor constraintEqualToAnchor:rightRangeView.leftAnchor constant:offset], 253 | ]; 254 | [NSLayoutConstraint activateConstraints:view.vi_constraints]; 255 | } 256 | } 257 | [self.rangeViews insertObject:view atIndex:index]; 258 | } 259 | 260 | - (void)removeCurrentActivedRangeViewCompletion:(void(^)(void))completion { 261 | __block NSInteger index = -1; 262 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 263 | if (obj.isActived) { 264 | index = idx; 265 | *stop = YES; 266 | } 267 | }]; 268 | if (index != -1) { 269 | [self removeRangeViewAtIndex:index animated:YES completion:completion]; 270 | } 271 | } 272 | 273 | - (void)removeRangeViewAtIndex:(NSInteger)index animated:(BOOL)animated completion:(void(^)(void))completion { 274 | if (index < 0 || index >= self.rangeViews.count) { 275 | return; 276 | } 277 | VIRangeView *rangeView = self.rangeViews[index]; 278 | CGFloat contentWidth = rangeView.contentWidth; 279 | 280 | void(^completionHandler)(void) = ^{ 281 | [rangeView removeFromSuperview]; 282 | if (self.rangeViews.count > 1) { 283 | if (index == 0) { 284 | VIRangeView *rightRangeView = self.rangeViews[index + 1]; 285 | [rightRangeView updateLeftConstraint:^NSLayoutConstraint * _Nonnull{ 286 | return [rightRangeView.leftAnchor constraintEqualToAnchor:rightRangeView.superview.leftAnchor]; 287 | }]; 288 | } else if (index == self.rangeViews.count - 1) { 289 | VIRangeView *leftRangeView = self.rangeViews[index - 1]; 290 | [leftRangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 291 | return [leftRangeView.rightAnchor constraintEqualToAnchor:leftRangeView.superview.rightAnchor]; 292 | }]; 293 | } else { 294 | VIRangeView *rightRangeView = self.rangeViews[index + 1]; 295 | VIRangeView *leftRangeView = self.rangeViews[index - 1]; 296 | [leftRangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 297 | CGFloat offset = (rightRangeView.contentInset.left + leftRangeView.contentInset.right) - (self.rangeViewLeftInset + self.rangeViewRightInset); 298 | return [leftRangeView.rightAnchor constraintEqualToAnchor:rightRangeView.leftAnchor constant:offset]; 299 | }]; 300 | } 301 | } 302 | 303 | [self.rangeViews removeObjectAtIndex:index]; 304 | if (completion) { 305 | completion(); 306 | } 307 | }; 308 | 309 | if (animated) { 310 | [self.scrollRangeContentView insertSubview:rangeView atIndex:0]; 311 | [UIView animateWithDuration:0.5 delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 312 | if (self.rangeViews.count > 1) { 313 | if (index == 0) { 314 | VIRangeView *rightRangeView = self.rangeViews[index + 1]; 315 | [rangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 316 | return [rangeView.rightAnchor constraintEqualToAnchor:rightRangeView.leftAnchor constant:(contentWidth + rangeView.contentInset.right + rightRangeView.contentInset.left)]; 317 | }]; 318 | } else if (index == self.rangeViews.count - 1) { 319 | VIRangeView *leftRangeView = self.rangeViews[index - 1]; 320 | [leftRangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 321 | return [leftRangeView.rightAnchor constraintEqualToAnchor:rangeView.leftAnchor constant:(contentWidth + rangeView.contentInset.left + leftRangeView.contentInset.right)]; 322 | }]; 323 | } else { 324 | VIRangeView *rightRangeView = self.rangeViews[index + 1]; 325 | [rangeView updateRightConstraint:^NSLayoutConstraint * _Nonnull{ 326 | return [rangeView.rightAnchor constraintEqualToAnchor:rightRangeView.leftAnchor constant:(contentWidth + rangeView.contentInset.right + rightRangeView.contentInset.left)]; 327 | }]; 328 | } 329 | } 330 | 331 | rangeView.alpha = 0.0; 332 | rangeView.transform = CGAffineTransformMakeScale(0.5, 0.5); 333 | 334 | [self layoutIfNeeded]; 335 | } completion:^(BOOL finished) { 336 | completionHandler(); 337 | }]; 338 | } else { 339 | completionHandler(); 340 | } 341 | 342 | } 343 | 344 | - (CGFloat)timelineWidthPerSeconds { 345 | return self.contentWidthPerSecond; 346 | } 347 | 348 | - (CGFloat)calculateOffsetXAtTime:(CMTime)time { 349 | CGFloat offsetX = -self.scrollView.contentInset.left; 350 | offsetX += CMTimeGetSeconds(time) * [self timelineWidthPerSeconds]; 351 | if (isnan(offsetX)) { 352 | offsetX = -self.scrollView.contentInset.left; 353 | } 354 | return offsetX; 355 | } 356 | 357 | - (NSInteger)getRangeViewIndexAtTime:(CMTime)time { 358 | __block NSInteger index = 0; 359 | __block NSTimeInterval duration = CMTimeGetSeconds(time); 360 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 361 | NSTimeInterval contentDuration = CMTimeGetSeconds(CMTimeSubtract(obj.endTime, obj.startTime)); 362 | if (duration <= contentDuration) { 363 | index = idx; 364 | *stop = YES; 365 | } else { 366 | duration -= contentDuration; 367 | } 368 | }]; 369 | return index; 370 | } 371 | 372 | - (CMTime)calculateTimeAtOffsetX:(CGFloat)offsetX { 373 | CGFloat offset = offsetX + self.scrollView.contentInset.left; 374 | NSTimeInterval duration = offset / [self timelineWidthPerSeconds]; 375 | return CMTimeMakeWithSeconds(duration, 600); 376 | } 377 | 378 | 379 | - (NSInteger)getRangeViewIndexAtOffsetX:(CGFloat)offsetX { 380 | __block CGFloat offset = offsetX + self.scrollView.contentInset.left; 381 | __block NSInteger index = 0; 382 | NSTimeInterval widthPerSeconds = [self timelineWidthPerSeconds]; 383 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 384 | NSTimeInterval contentDuration = CMTimeGetSeconds(CMTimeSubtract(obj.endTime, obj.startTime)); 385 | CGFloat width = contentDuration * widthPerSeconds; 386 | if (offset <= width) { 387 | index = idx; 388 | *stop = YES; 389 | } else { 390 | offset -= width; 391 | } 392 | }]; 393 | 394 | return index; 395 | } 396 | 397 | - (void)adjustScrollViewOffsetAtTime:(CMTime)time { 398 | if (!CMTIME_IS_VALID(time)) { 399 | return; 400 | } 401 | CGFloat offsetX = [self calculateOffsetXAtTime:time]; 402 | self.previousScrollViewDelegate = self.scrollView.delegate; 403 | self.scrollView.delegate = nil; 404 | self.scrollView.contentOffset = CGPointMake(offsetX, 0); 405 | [self displayRangeViewsIfNeed]; 406 | self.scrollView.delegate = self.previousScrollViewDelegate; 407 | } 408 | 409 | - (void)scrollToStartOfRangeView:(VIRangeView *)rangeView animated:(BOOL)animated completion:(void(^)(void))completion { 410 | CGPoint center = [rangeView convertPoint:rangeView.leftEarView.center toView:self]; 411 | CGPoint lineCenter = CGPointMake(center.x + rangeView.leftEarView.bounds.size.width * 0.5, center.y); 412 | CGFloat centerX = self.bounds.size.width * 0.5; 413 | CGPoint contentOffset = self.scrollView.contentOffset; 414 | contentOffset.x -= centerX - lineCenter.x; 415 | [self scrollToContentOffset:contentOffset animated:animated completion:completion]; 416 | } 417 | 418 | - (void)scrollToEndOfRangeView:(VIRangeView *)rangeView animated:(BOOL)animated completion:(void(^)(void))completion { 419 | CGPoint center = [rangeView convertPoint:rangeView.rightEarView.center toView:self]; 420 | CGPoint lineCenter = CGPointMake(center.x - rangeView.rightEarView.bounds.size.width * 0.5, center.y); 421 | CGFloat centerX = self.bounds.size.width * 0.5; 422 | CGPoint contentOffset = self.scrollView.contentOffset; 423 | contentOffset.x -= centerX - lineCenter.x; 424 | [self scrollToContentOffset:contentOffset animated:animated completion:completion]; 425 | } 426 | 427 | - (void)scrollToContentOffset:(CGPoint)contentOffset animated:(BOOL)animated completion:(void(^)(void))completion { 428 | if (animated) { 429 | [UIView animateWithDuration:0.3 animations:^{ 430 | self.scrollView.contentOffset = contentOffset; 431 | } completion:^(BOOL finished) { 432 | if (completion) { 433 | completion(); 434 | } 435 | }]; 436 | } else { 437 | self.scrollView.contentOffset = contentOffset; 438 | if (completion) { 439 | completion(); 440 | } 441 | } 442 | } 443 | 444 | #pragma mark - Helper 445 | 446 | - (BOOL)isActived { 447 | BOOL isActived = NO; 448 | 449 | for (VIRangeView *rangeView in self.rangeViews) { 450 | if (rangeView.isActived) { 451 | isActived = YES; 452 | break; 453 | } 454 | } 455 | 456 | return isActived; 457 | } 458 | 459 | - (void)resignVideoRangeView { 460 | if ([self isActived]) { 461 | [self.delegate timelineView:self didChangeActive:NO]; 462 | } 463 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 464 | if (obj.isActived) { 465 | [obj inactiveEarAnimated:YES]; 466 | } 467 | }]; 468 | } 469 | 470 | - (void)removeAllRangeViews { 471 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 472 | [obj removeFromSuperview]; 473 | }]; 474 | [self.rangeViews removeAllObjects]; 475 | } 476 | 477 | - (void)displayRangeViewsIfNeed { 478 | NSArray *visiableRangeViews = [self fetchVisiableRangeViews]; 479 | [visiableRangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 480 | [obj.contentView updateDataIfNeed]; 481 | }]; 482 | } 483 | 484 | - (NSArray *)fetchVisiableRangeViews { 485 | NSMutableArray *rangeViews = [NSMutableArray array]; 486 | [self.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 487 | CGRect rect = [obj.superview convertRect:obj.frame toView:self.scrollView]; 488 | BOOL intersects = CGRectIntersectsRect(self.scrollView.bounds, rect); 489 | if (intersects) { 490 | [rangeViews addObject:obj]; 491 | } 492 | }]; 493 | return rangeViews; 494 | } 495 | 496 | 497 | 498 | #pragma mark - VIRangeViewDelegate 499 | 500 | - (void)rangeView:(VIRangeView *)rangeView didChangeActive:(BOOL)isActive { 501 | [self.rangeViewDelegate rangeView:rangeView didChangeActive:isActive]; 502 | } 503 | 504 | - (void)rangeViewBeginUpdateLeft:(VIRangeView *)rangeView { 505 | self.previousScrollViewDelegate = self.scrollView.delegate; 506 | self.scrollView.delegate = nil; 507 | [self.rangeViewDelegate rangeViewBeginUpdateLeft:rangeView]; 508 | } 509 | 510 | - (void)rangeView:(VIRangeView *)rangeView updateLeftOffset:(CGFloat)offset isAuto:(BOOL)isAuto { 511 | [self.rangeViewDelegate rangeView:rangeView updateLeftOffset:offset isAuto:isAuto]; 512 | 513 | CGPoint center = [rangeView convertPoint:rangeView.leftEarView.center toView:self]; 514 | self.centerLineView.center = CGPointMake(center.x + rangeView.leftEarView.bounds.size.width * 0.5, center.y); 515 | 516 | if (isAuto) { 517 | return; 518 | } 519 | 520 | UIEdgeInsets inset = self.scrollView.contentInset; 521 | inset.left = self.scrollView.frame.size.width; 522 | self.scrollView.contentInset = inset; 523 | 524 | CGPoint contentOffset = self.scrollView.contentOffset; 525 | contentOffset.x -= offset; 526 | [self.scrollView setContentOffset:contentOffset animated:NO]; 527 | } 528 | 529 | - (void)rangeViewEndUpdateLeftOffset:(VIRangeView *)rangeView { 530 | 531 | UIEdgeInsets inset = self.scrollView.contentInset; 532 | inset.left = inset.right; 533 | 534 | CGFloat centerX = self.bounds.size.width * 0.5; 535 | 536 | CGPoint contentOffset = self.scrollView.contentOffset; 537 | contentOffset.x -= centerX - self.centerLineView.center.x; 538 | [UIView animateWithDuration:0.3 animations:^{ 539 | self.scrollView.contentInset = inset; 540 | self.scrollView.contentOffset = contentOffset; 541 | self.centerLineView.center = CGPointMake(centerX, self.bounds.size.height * 0.5); 542 | } completion:^(BOOL finished) { 543 | self.scrollView.delegate = self.previousScrollViewDelegate; 544 | [self.rangeViewDelegate rangeViewEndUpdateLeftOffset:rangeView]; 545 | }]; 546 | } 547 | 548 | - (void)rangeViewBeginUpdateRight:(VIRangeView *)rangeView { 549 | self.previousScrollViewDelegate = self.scrollView.delegate; 550 | self.scrollView.delegate = nil; 551 | [self.rangeViewDelegate rangeViewBeginUpdateRight:rangeView]; 552 | } 553 | 554 | - (void)rangeView:(VIRangeView *)rangeView updateRightOffset:(CGFloat)offset isAuto:(BOOL)isAuto { 555 | [self.rangeViewDelegate rangeView:rangeView updateRightOffset:offset isAuto:isAuto]; 556 | 557 | CGPoint center = [rangeView convertPoint:rangeView.rightEarView.center toView:self]; 558 | self.centerLineView.center = CGPointMake(center.x - rangeView.rightEarView.frame.size.width * 0.5, center.y); 559 | if (isAuto) { 560 | CGPoint contentOffset = self.scrollView.contentOffset; 561 | contentOffset.x += offset; 562 | [self.scrollView setContentOffset:contentOffset animated: false]; 563 | } else { 564 | UIEdgeInsets inset = self.scrollView.contentInset; 565 | inset.right = self.scrollView.frame.size.width; 566 | self.scrollView.contentInset = inset; 567 | } 568 | } 569 | 570 | - (void)rangeViewEndUpdateRightOffset:(VIRangeView *)rangeView { 571 | 572 | UIEdgeInsets inset = self.scrollView.contentInset; 573 | inset.right = inset.left; 574 | 575 | CGFloat centerX = self.bounds.size.width * 0.5; 576 | 577 | CGPoint contentOffset = self.scrollView.contentOffset; 578 | contentOffset.x -= centerX - self.centerLineView.center.x; 579 | [UIView animateWithDuration:0.3 animations:^{ 580 | self.scrollView.contentInset = inset; 581 | self.scrollView.contentOffset = contentOffset; 582 | self.centerLineView.center = CGPointMake(centerX, self.bounds.size.height * 0.5); 583 | } completion:^(BOOL finished) { 584 | self.scrollView.delegate = self.previousScrollViewDelegate; 585 | [self.rangeViewDelegate rangeViewEndUpdateRightOffset:rangeView]; 586 | }]; 587 | } 588 | 589 | @end 590 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIVideoRangeContentView.h: -------------------------------------------------------------------------------- 1 | // 2 | // VIVideoRangeContentView.h 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | @import CoreMedia; 11 | #import "VIRangeContentView.h" 12 | 13 | @class VIVideoRangeContentView; 14 | 15 | @protocol VIVideoRangeContentViewDataSource 16 | 17 | - (NSInteger)videoRangeContentViewNumberOfImages:(VIVideoRangeContentView *)view; 18 | - (UIImage *)videoRangeContent:(VIVideoRangeContentView *)view imageAtIndex:(NSInteger)index preferredSize:(CGSize)size; 19 | 20 | @optional 21 | 22 | - (BOOL)videoRangeContent:(VIVideoRangeContentView *)view hasCacheAtIndex:(NSInteger)index; 23 | 24 | @end 25 | 26 | @interface VIVideoRangeContentView : VIRangeContentView 27 | 28 | @property (nonatomic, strong) NSOperationQueue *loadImageQueue; 29 | 30 | @property (nonatomic, strong) id dataSource; 31 | @property (nonatomic) CGSize imageSize; 32 | @property (nonatomic) NSInteger preloadCount; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /VITimelineViewDemo/Source/VIVideoRangeContentView.m: -------------------------------------------------------------------------------- 1 | // 2 | // VIVideoRangeContentView.m 3 | // vito 4 | // 5 | // Created by Vito on 2018/8/31. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "VIVideoRangeContentView.h" 10 | #import 11 | #import "UIView+ConstraintHolder.h" 12 | 13 | @interface UIImageView (VIOperation) 14 | 15 | - (void)setVi_operation:(NSOperation *)operation; 16 | - (NSOperation *)vi_operation; 17 | 18 | @end 19 | 20 | @implementation UIImageView (VIOperation) 21 | 22 | static const char VIOperationKey = '\0'; 23 | - (void)setVi_operation:(NSOperation *)operation { 24 | if (operation != self.vi_operation) { 25 | objc_setAssociatedObject(self, &VIOperationKey, operation, OBJC_ASSOCIATION_RETAIN_NONATOMIC); 26 | } 27 | } 28 | 29 | - (NSOperation *)vi_operation { 30 | return objc_getAssociatedObject(self, &VIOperationKey); 31 | } 32 | 33 | 34 | @end 35 | 36 | @interface VIVideoRangeContentView() 37 | 38 | @property (nonatomic, strong) NSMutableSet *reusableImageViews; 39 | @property (nonatomic, strong) NSMutableDictionary *imageViewDic; 40 | 41 | @end 42 | 43 | @implementation VIVideoRangeContentView 44 | 45 | - (instancetype)initWithFrame:(CGRect)frame 46 | { 47 | self = [super initWithFrame:frame]; 48 | if (self) { 49 | [self commonInit]; 50 | } 51 | return self; 52 | } 53 | 54 | - (instancetype)initWithCoder:(NSCoder *)coder 55 | { 56 | self = [super initWithCoder:coder]; 57 | if (self) { 58 | [self commonInit]; 59 | } 60 | return self; 61 | } 62 | 63 | - (void)commonInit { 64 | _reusableImageViews = [NSMutableSet set]; 65 | _imageViewDic = [NSMutableDictionary dictionary]; 66 | _preloadCount = 1; 67 | _loadImageQueue = [[NSOperationQueue alloc] init]; 68 | _loadImageQueue.maxConcurrentOperationCount = 4; 69 | } 70 | 71 | - (void)layoutIfNeeded { 72 | [super layoutIfNeeded]; 73 | if (CGSizeEqualToSize(self.imageSize, CGSizeZero)) { 74 | self.imageSize = CGSizeMake(self.bounds.size.height, self.bounds.size.height); 75 | } 76 | [self updateDataIfNeed]; 77 | } 78 | 79 | - (void)layoutSubviews { 80 | [super layoutSubviews]; 81 | if (CGSizeEqualToSize(self.imageSize, CGSizeZero)) { 82 | self.imageSize = CGSizeMake(self.bounds.size.height, self.bounds.size.height); 83 | } 84 | [self updateDataIfNeed]; 85 | } 86 | 87 | #pragma mark - Override 88 | 89 | - (void)reloadData { 90 | [self removeImageViewsOutOfRange:NSMakeRange(0, 0)]; 91 | [self updateDataIfNeed]; 92 | } 93 | 94 | - (void)updateDataIfNeed { 95 | NSRange visiableRange = [self calculateVisiableRange]; 96 | if (visiableRange.length == 0) { 97 | return; 98 | } 99 | [self removeImageViewsOutOfRange:visiableRange]; 100 | for (NSInteger i = visiableRange.location; i < visiableRange.location + visiableRange.length; i++) { 101 | [self loadImageViewAtIndex:i]; 102 | } 103 | } 104 | 105 | #pragma mark - Logic 106 | 107 | - (NSRange)calculateVisiableRange { 108 | if (self.imageSize.height <= 0) { 109 | return NSMakeRange(0, 0); 110 | } 111 | 112 | if (!self.dataSource) { 113 | return NSMakeRange(0, 0); 114 | } 115 | UIWindow *window = [UIApplication sharedApplication].keyWindow; 116 | if (!window) { 117 | return NSMakeRange(0, 0); 118 | } 119 | 120 | CGRect availableRectInSuperView = CGRectIntersection([self.superview convertRect:self.superview.bounds toView:self], self.bounds); 121 | CGRect rectInWindow = [self convertRect:availableRectInSuperView toView:window]; 122 | CGRect availableRectInWindow = CGRectIntersection(window.bounds, rectInWindow); 123 | if (!(availableRectInWindow.size.width > 0 && availableRectInWindow.size.height > 0)) { 124 | return NSMakeRange(0, 0); 125 | } 126 | 127 | CGRect availableRect = [self convertRect:availableRectInWindow fromView:window]; 128 | CGFloat startOffset = availableRect.origin.x; 129 | NSInteger startIndexOfImage = startOffset / self.imageSize.width; 130 | NSInteger endIndexOfImage = ceil((availableRect.size.width + startOffset) / self.imageSize.width); 131 | 132 | if (self.preloadCount > 0) { 133 | startIndexOfImage = startIndexOfImage - self.preloadCount; 134 | startIndexOfImage = MAX(0, startIndexOfImage); 135 | endIndexOfImage = endIndexOfImage + self.preloadCount; 136 | NSInteger maxIndex = [self.dataSource videoRangeContentViewNumberOfImages:self]; 137 | endIndexOfImage = MIN(maxIndex, endIndexOfImage); 138 | } 139 | 140 | startIndexOfImage = MIN(startIndexOfImage, endIndexOfImage); 141 | return NSMakeRange(startIndexOfImage, endIndexOfImage - startIndexOfImage); 142 | } 143 | 144 | - (void)removeImageViewsOutOfRange:(NSRange)range { 145 | NSMutableArray *outIndexes = [NSMutableArray array]; 146 | [[self.imageViewDic allKeys] enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 147 | if (obj.integerValue < range.location || obj.integerValue > (range.location + range.length)) { 148 | [outIndexes addObject:obj]; 149 | } 150 | }]; 151 | 152 | [outIndexes enumerateObjectsUsingBlock:^(NSNumber * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 153 | UIImageView *imageView = self.imageViewDic[obj]; 154 | imageView.tag = 0; 155 | imageView.image = nil; 156 | [imageView removeFromSuperview]; 157 | [self.imageViewDic removeObjectForKey:obj]; 158 | [self.reusableImageViews addObject:imageView]; 159 | }]; 160 | } 161 | 162 | - (void)loadImageViewAtIndex:(NSInteger)index { 163 | if (!self.dataSource) { 164 | return; 165 | } 166 | 167 | UIImageView *imageView = self.imageViewDic[@(index)]; 168 | 169 | if (imageView.tag == index && imageView.superview == self && imageView.image) { 170 | return; 171 | } 172 | 173 | if (imageView.vi_operation && !imageView.vi_operation.isCancelled) { 174 | return; 175 | } 176 | 177 | if (!imageView) { 178 | imageView = [self createImageView]; 179 | self.imageViewDic[@(index)] = imageView; 180 | } 181 | 182 | NSInteger previousIndex = imageView.tag; 183 | imageView.tag = index; 184 | 185 | if (previousIndex != index || !imageView.image) { 186 | [self layoutImageView:imageView atIndex:index]; 187 | // load image data 188 | if (!imageView.image) { 189 | BOOL hasCache = NO; 190 | if ([self.dataSource respondsToSelector:@selector(videoRangeContent:hasCacheAtIndex:)]) { 191 | hasCache = [self.dataSource videoRangeContent:self hasCacheAtIndex:index]; 192 | } 193 | if (self.loadImageQueue && !hasCache) { 194 | NSBlockOperation *loadImageOperation = [[NSBlockOperation alloc] init]; 195 | __weak typeof(loadImageOperation)weakOperation = loadImageOperation; 196 | __weak typeof(self)weakSelf = self; 197 | __weak typeof(imageView)weakImageView = imageView; 198 | [loadImageOperation addExecutionBlock:^{ 199 | __strong __typeof(weakOperation)strongOperation = weakOperation; 200 | __strong __typeof(weakSelf)strongSelf = weakSelf; 201 | __strong __typeof(weakImageView)imageView = weakImageView; 202 | if (!strongSelf || !strongOperation) { 203 | return; 204 | } 205 | if (strongOperation.isCancelled) { 206 | return; 207 | } 208 | UIImage *image = [strongSelf.dataSource videoRangeContent:strongSelf imageAtIndex:index preferredSize:strongSelf.imageSize]; 209 | 210 | if (strongOperation.isCancelled) { 211 | return; 212 | } 213 | [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 214 | if (imageView.tag == index) { 215 | [UIView transitionWithView:imageView duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{ 216 | imageView.image = image; 217 | } completion:nil]; 218 | } 219 | }]; 220 | }]; 221 | [self.loadImageQueue addOperation:loadImageOperation]; 222 | [imageView.vi_operation cancel]; 223 | [imageView setVi_operation:loadImageOperation]; 224 | } else { 225 | UIImage *image = [self.dataSource videoRangeContent:self imageAtIndex:index preferredSize:self.imageSize]; 226 | imageView.image = image; 227 | } 228 | } 229 | } 230 | } 231 | 232 | - (UIImageView *)createImageView { 233 | UIImageView *imageView = [self.reusableImageViews anyObject]; 234 | if (imageView) { 235 | [self.reusableImageViews removeObject:imageView]; 236 | imageView.tag = -1; 237 | return imageView; 238 | } 239 | 240 | imageView = [[UIImageView alloc] init]; 241 | imageView.translatesAutoresizingMaskIntoConstraints = NO; 242 | imageView.tag = -1; 243 | imageView.clipsToBounds = YES; 244 | imageView.contentMode = UIViewContentModeScaleAspectFill; 245 | 246 | return imageView; 247 | } 248 | 249 | - (void)layoutImageView:(UIImageView *)imageView atIndex:(NSInteger)index { 250 | if (imageView.superview != self) { 251 | [self insertSubview:imageView atIndex:0]; 252 | } 253 | 254 | [NSLayoutConstraint deactivateConstraints:imageView.vi_constraints]; 255 | 256 | NSMutableArray *constraints = [NSMutableArray array]; 257 | [constraints addObject:[imageView.centerYAnchor constraintEqualToAnchor:self.centerYAnchor]]; 258 | [constraints addObject:[imageView.widthAnchor constraintEqualToConstant:self.imageSize.width]]; 259 | [constraints addObject:[imageView.heightAnchor constraintEqualToConstant:self.imageSize.height]]; 260 | [constraints addObject:[imageView.leftAnchor constraintEqualToAnchor:self.leftAnchor constant:round((index * self.imageSize.width))]]; 261 | imageView.vi_constraints = constraints; 262 | 263 | [NSLayoutConstraint activateConstraints:constraints]; 264 | } 265 | 266 | @end 267 | 268 | -------------------------------------------------------------------------------- /VITimelineViewDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/15. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /VITimelineViewDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/15. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import 11 | #import "VITimelineView+Creator.h" 12 | 13 | @interface ViewController () 14 | 15 | 16 | @end 17 | 18 | @implementation ViewController 19 | 20 | - (void)viewDidLoad { 21 | [super viewDidLoad]; 22 | 23 | NSURL *url1 = [[NSBundle mainBundle] URLForResource:@"bamboo" withExtension:@"mp4"]; 24 | AVAsset *asset1 = [AVAsset assetWithURL:url1]; 25 | 26 | NSURL *url2 = [[NSBundle mainBundle] URLForResource:@"water" withExtension:@"mp4"]; 27 | AVAsset *asset2 = [AVAsset assetWithURL:url2]; 28 | 29 | CGFloat widthPerSecond = 40; 30 | CGSize imageSize = CGSizeMake(30, 45); 31 | 32 | VITimelineView *timelineView = 33 | [VITimelineView timelineViewWithAssets:@[asset1, asset2] 34 | imageSize:imageSize 35 | widthPerSecond:widthPerSecond]; 36 | timelineView.delegate = self; 37 | timelineView.rangeViewDelegate = self; 38 | timelineView.backgroundColor = [UIColor colorWithRed:0.11 green:0.15 blue:0.34 alpha:1.00]; 39 | timelineView.translatesAutoresizingMaskIntoConstraints = NO; 40 | [self.view addSubview:timelineView]; 41 | [timelineView.leftAnchor constraintEqualToAnchor:self.view.leftAnchor].active = YES; 42 | [timelineView.rightAnchor constraintEqualToAnchor:self.view.rightAnchor].active = YES; 43 | [timelineView.heightAnchor constraintEqualToConstant:100].active = YES; 44 | [timelineView.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor].active = YES; 45 | 46 | CIImage *ciimage = [CIImage imageWithColor:[CIColor colorWithRed:0.30 green:0.59 blue:0.70 alpha:1]]; 47 | CGImageRef cgimage = [[CIContext context] createCGImage:ciimage fromRect:CGRectMake(0, 0, 1, 60)]; 48 | UIImage *image = [UIImage imageWithCGImage:cgimage]; 49 | timelineView.centerLineView.image = image; 50 | [timelineView.rangeViews enumerateObjectsUsingBlock:^(VIRangeView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 51 | obj.clipsToBounds = YES; 52 | obj.layer.cornerRadius = 4; 53 | obj.leftEarView.backgroundColor = [UIColor colorWithRed:0.72 green:0.73 blue:0.77 alpha:1.00]; 54 | obj.rightEarView.backgroundColor = [UIColor colorWithRed:0.72 green:0.73 blue:0.77 alpha:1.00]; 55 | obj.backgroundView.backgroundColor = [UIColor colorWithRed:0.72 green:0.73 blue:0.77 alpha:1.00]; 56 | }]; 57 | } 58 | 59 | - (UIStatusBarStyle)preferredStatusBarStyle { 60 | return UIStatusBarStyleLightContent; 61 | } 62 | 63 | - (void)rangeView:(VIRangeView *)rangeView didChangeActive:(BOOL)isActive { 64 | NSLog(@"rangeView:%@ didchangeActive: %@", rangeView, @(isActive)); 65 | } 66 | 67 | - (void)rangeView:(VIRangeView *)rangeView updateLeftOffset:(CGFloat)offset isAuto:(BOOL)isAuto { 68 | NSLog(@"2.updateLeftOffset rangeView offset: %@ width: %@", @(offset), @(rangeView.contentWidth)); 69 | } 70 | 71 | - (void)rangeView:(VIRangeView *)rangeView updateRightOffset:(CGFloat)offset isAuto:(BOOL)isAuto { 72 | NSLog(@"2.updateRightOffset rangeView offset: %@ width: %@", @(offset), @(rangeView.contentWidth)); 73 | } 74 | 75 | - (void)rangeViewBeginUpdateLeft:(VIRangeView *)rangeView { 76 | NSLog(@"1.rangeViewBeginUpdateLeft rangeView width: %@", @(rangeView.contentWidth)); 77 | } 78 | 79 | - (void)rangeViewBeginUpdateRight:(VIRangeView *)rangeView { 80 | NSLog(@"1.rangeViewBeginUpdateRight rangeView width: %@", @(rangeView.contentWidth)); 81 | } 82 | 83 | - (void)rangeViewEndUpdateLeftOffset:(VIRangeView *)rangeView { 84 | NSLog(@"3.rangeViewEndUpdateLeftOffset rangeView width: %@", @(rangeView.contentWidth)); 85 | } 86 | 87 | - (void)rangeViewEndUpdateRightOffset:(VIRangeView *)rangeView { 88 | NSLog(@"3.rangeViewEndUpdateRightOffset rangeView width: %@", @(rangeView.contentWidth)); 89 | } 90 | 91 | - (void)timelineView:(nonnull VITimelineView *)view didChangeActive:(BOOL)isActive { 92 | NSLog(@"timelineview didchangeActive: %@", @(isActive)); 93 | } 94 | 95 | 96 | @end 97 | -------------------------------------------------------------------------------- /VITimelineViewDemo/bamboo.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VideoFlint/VITimelineView/c04d8e8e11b65ed43446dd193669d9957771c448/VITimelineViewDemo/bamboo.mp4 -------------------------------------------------------------------------------- /VITimelineViewDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // VITimelineViewDemo 4 | // 5 | // Created by Vito on 2018/11/15. 6 | // Copyright © 2018 vito. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | @autoreleasepool { 14 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /VITimelineViewDemo/water.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/VideoFlint/VITimelineView/c04d8e8e11b65ed43446dd193669d9957771c448/VITimelineViewDemo/water.mp4 --------------------------------------------------------------------------------