├── .gitignore ├── LICENSE ├── MPPlayerDemo ├── MPPlayerDemo.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ └── contents.xcworkspacedata ├── MPPlayerDemo.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── MPPlayerDemo │ ├── AppDelegate.h │ ├── AppDelegate.m │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── comment.imageset │ │ │ ├── Contents.json │ │ │ └── comment.png │ │ ├── defaultUserIcon.imageset │ │ │ ├── Contents.json │ │ │ ├── defaultUserIcon.png │ │ │ ├── defaultUserIcon@2x.png │ │ │ └── defaultUserIcon@3x.png │ │ ├── icon_play_pause.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_play_pause@2x.png │ │ │ └── icon_play_pause@3x.png │ │ ├── icon_titlebar_whiteback.imageset │ │ │ ├── Contents.json │ │ │ ├── icon_titlebar_whiteback@2x.png │ │ │ └── icon_titlebar_whiteback@3x.png │ │ ├── img_video_loading.imageset │ │ │ ├── Contents.json │ │ │ ├── img_video_loading@2x.png │ │ │ └── img_video_loading@3x.png │ │ ├── like.imageset │ │ │ ├── Contents.json │ │ │ └── like.png │ │ ├── loading_bgView.imageset │ │ │ ├── Contents.json │ │ │ └── loading_bgView.png │ │ ├── logo.imageset │ │ │ ├── Contents.json │ │ │ └── logo.png │ │ ├── new_allPause_44x44_.imageset │ │ │ ├── Contents.json │ │ │ ├── new_allPause_44x44_@2x.png │ │ │ └── new_allPause_44x44_@3x.png │ │ ├── new_allPlay_44x44_.imageset │ │ │ ├── Contents.json │ │ │ ├── new_allPlay_44x44_@2x.png │ │ │ └── new_allPlay_44x44_@3x.png │ │ ├── share.imageset │ │ │ ├── Contents.json │ │ │ └── share.png │ │ └── zfplayer_back.imageset │ │ │ ├── Contents.json │ │ │ ├── zfplayer_back@2x.png │ │ │ └── zfplayer_back@3x.png │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── CardLayout │ │ ├── MPCardStackLayout.h │ │ └── MPCardStackLayout.m │ ├── Controller │ │ ├── MPCardLayoutViewController.h │ │ ├── MPCardLayoutViewController.m │ │ ├── MPDetailViewController.h │ │ ├── MPDetailViewController.m │ │ ├── MPListViewController.h │ │ ├── MPListViewController.m │ │ ├── MPUserDynamicDetailViewController.h │ │ ├── MPUserDynamicDetailViewController.m │ │ ├── MPUserDynamicViewController.h │ │ ├── MPUserDynamicViewController.m │ │ ├── MPWaterFallViewController.h │ │ └── MPWaterFallViewController.m │ ├── Info.plist │ ├── Model │ │ ├── ZFTableData.h │ │ ├── ZFTableData.m │ │ └── data.json │ ├── Other │ │ ├── NSString+Size.h │ │ ├── NSString+Size.m │ │ ├── ZFTableViewCellLayout.h │ │ └── ZFTableViewCellLayout.m │ ├── Player │ │ ├── MPPlayableProtocol.h │ │ ├── MPPlayerController.h │ │ ├── MPPlayerController.m │ │ ├── MPPreLoaderModel.h │ │ ├── MPPreLoaderModel.m │ │ ├── MPlayerAttributeManager.h │ │ ├── MPlayerAttributeManager.m │ │ ├── ZFDouYinControlView.h │ │ └── ZFDouYinControlView.m │ ├── Transition │ │ ├── MPTransition.h │ │ ├── MPTransition.m │ │ ├── MPUserDynamicTransition.h │ │ └── MPUserDynamicTransition.m │ ├── View │ │ ├── MPCardLayoutCell.h │ │ ├── MPCardLayoutCell.m │ │ ├── MPUserDynamicCollectionViewCell.h │ │ ├── MPUserDynamicCollectionViewCell.m │ │ ├── MPWaterFallCollectionViewCell.h │ │ ├── MPWaterFallCollectionViewCell.m │ │ ├── ZFDouYinCell.h │ │ ├── ZFDouYinCell.m │ │ ├── ZFTableViewCell.h │ │ └── ZFTableViewCell.m │ ├── ViewController.h │ ├── ViewController.m │ ├── WaterFall │ │ ├── MPWaterFallLayout.h │ │ └── MPWaterFallLayout.m │ └── main.m ├── MPPlayerDemoTests │ ├── Info.plist │ └── MPPlayerDemoTests.m ├── MPPlayerDemoUITests │ ├── Info.plist │ └── MPPlayerDemoUITests.m ├── Podfile └── Podfile.lock ├── README.md └── playerDemo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | # CocoaPods 34 | # 35 | # We recommend against adding the Pods directory to your .gitignore. However 36 | # you should judge for yourself, the pros and cons are mentioned at: 37 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 38 | # 39 | Pods/ 40 | # 41 | # Add this line if you want to avoid checking in source code from the Xcode workspace 42 | # *.xcworkspace 43 | 44 | # Carthage 45 | # 46 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 47 | # Carthage/Checkouts 48 | 49 | Carthage/Build/ 50 | 51 | # fastlane 52 | # 53 | # It is recommended to not store the screenshots in the git repo. 54 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 55 | # For more information about the recommended setup visit: 56 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 57 | 58 | fastlane/report.xml 59 | fastlane/Preview.html 60 | fastlane/screenshots/**/*.png 61 | fastlane/test_output 62 | 63 | # Code Injection 64 | # 65 | # After new code Injection tools there's a generated folder /iOSInjectionProject 66 | # https://github.com/johnno1962/injectionforxcode 67 | 68 | iOSInjectionProject/ 69 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maple 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface AppDelegate : UIResponder 12 | 13 | @property (nonatomic, strong) UIWindow *window; 14 | 15 | @end 16 | 17 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. 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 | @end 24 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/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 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/comment.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "comment.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/comment.imageset/comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/comment.imageset/comment.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "defaultUserIcon.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "defaultUserIcon@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "defaultUserIcon@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/defaultUserIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/defaultUserIcon.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/defaultUserIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/defaultUserIcon@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/defaultUserIcon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/defaultUserIcon.imageset/defaultUserIcon@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_play_pause.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_play_pause@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon_play_pause@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_play_pause.imageset/icon_play_pause@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_play_pause.imageset/icon_play_pause@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_play_pause.imageset/icon_play_pause@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_play_pause.imageset/icon_play_pause@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_titlebar_whiteback.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "icon_titlebar_whiteback@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "icon_titlebar_whiteback@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_titlebar_whiteback.imageset/icon_titlebar_whiteback@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_titlebar_whiteback.imageset/icon_titlebar_whiteback@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_titlebar_whiteback.imageset/icon_titlebar_whiteback@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/icon_titlebar_whiteback.imageset/icon_titlebar_whiteback@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/img_video_loading.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "img_video_loading@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "img_video_loading@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/img_video_loading.imageset/img_video_loading@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/img_video_loading.imageset/img_video_loading@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/img_video_loading.imageset/img_video_loading@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/img_video_loading.imageset/img_video_loading@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/like.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "like.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/like.imageset/like.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/like.imageset/like.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/loading_bgView.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "loading_bgView.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/loading_bgView.imageset/loading_bgView.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/loading_bgView.imageset/loading_bgView.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/logo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "logo.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/logo.imageset/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/logo.imageset/logo.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPause_44x44_.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "new_allPause_44x44_@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "new_allPause_44x44_@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPause_44x44_.imageset/new_allPause_44x44_@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPause_44x44_.imageset/new_allPause_44x44_@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPause_44x44_.imageset/new_allPause_44x44_@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPause_44x44_.imageset/new_allPause_44x44_@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPlay_44x44_.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "new_allPlay_44x44_@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "new_allPlay_44x44_@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPlay_44x44_.imageset/new_allPlay_44x44_@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPlay_44x44_.imageset/new_allPlay_44x44_@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPlay_44x44_.imageset/new_allPlay_44x44_@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/new_allPlay_44x44_.imageset/new_allPlay_44x44_@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/share.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "share.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "version" : 1, 19 | "author" : "xcode" 20 | } 21 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/share.imageset/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/share.imageset/share.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/zfplayer_back.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "scale" : "1x" 6 | }, 7 | { 8 | "idiom" : "universal", 9 | "filename" : "zfplayer_back@2x.png", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "filename" : "zfplayer_back@3x.png", 15 | "scale" : "3x" 16 | } 17 | ], 18 | "info" : { 19 | "version" : 1, 20 | "author" : "xcode" 21 | } 22 | } -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/zfplayer_back.imageset/zfplayer_back@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/zfplayer_back.imageset/zfplayer_back@2x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Assets.xcassets/zfplayer_back.imageset/zfplayer_back@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/MPPlayerDemo/MPPlayerDemo/Assets.xcassets/zfplayer_back.imageset/zfplayer_back@3x.png -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/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 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/CardLayout/MPCardStackLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPCardStackLayout.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPCardStackLayout : UICollectionViewLayout 14 | 15 | @property (nonatomic) CGSize itemSize; 16 | @property (nonatomic) CGFloat spacing; 17 | @property (nonatomic) NSInteger maximumVisibleItems; 18 | 19 | @end 20 | 21 | NS_ASSUME_NONNULL_END 22 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/CardLayout/MPCardStackLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPCardStackLayout.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPCardStackLayout.h" 10 | 11 | @implementation MPCardStackLayout 12 | 13 | - (instancetype)init 14 | { 15 | self = [super init]; 16 | self.itemSize = CGSizeMake(250, 400); 17 | self.spacing = 4; 18 | self.maximumVisibleItems = 4; 19 | return self; 20 | } 21 | 22 | - (void)prepareLayout 23 | { 24 | [super prepareLayout]; 25 | NSAssert(self.collectionView.numberOfSections == 1, @"不支持多个Section"); 26 | } 27 | 28 | - (CGSize)collectionViewContentSize 29 | { 30 | if (self.collectionView == nil) { 31 | return CGSizeZero; 32 | } 33 | NSInteger itemsCount = [self.collectionView numberOfItemsInSection:0]; 34 | return CGSizeMake(self.collectionView.bounds.size.width * itemsCount, self.collectionView.bounds.size.height); 35 | } 36 | 37 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 38 | { 39 | if (self.collectionView == nil) 40 | return nil; 41 | NSInteger totalItemsCount = [self.collectionView numberOfItemsInSection:0]; 42 | NSInteger tmp = (int)self.collectionView.contentOffset.x / (int)self.collectionView.bounds.size.width; 43 | NSInteger minVisibleIndex = MAX(0, tmp); 44 | NSInteger maxVisibleIndex = MIN(totalItemsCount, minVisibleIndex + self.maximumVisibleItems); 45 | 46 | CGFloat contentCenterX = self.collectionView.contentOffset.x + self.collectionView.bounds.size.width / 2; 47 | NSInteger deltaOffset = (int)self.collectionView.contentOffset.x % (int)self.collectionView.bounds.size.width; 48 | CGFloat percentageDeltaOffset = deltaOffset / self.collectionView.bounds.size.width; 49 | 50 | NSMutableArray *attributes = [[NSMutableArray alloc] init]; 51 | for (NSInteger i = minVisibleIndex; i < maxVisibleIndex; i++) { 52 | UICollectionViewLayoutAttributes *attribute = [self conputeLayoutAttributesForItem:[NSIndexPath indexPathForRow:i inSection:0] minVisibleIndex:minVisibleIndex contentCenterX:contentCenterX deltaOffset:deltaOffset percentageDeltaOffset:percentageDeltaOffset]; 53 | [attributes addObject:attribute]; 54 | } 55 | return attributes; 56 | } 57 | 58 | - (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds 59 | { 60 | return YES; 61 | } 62 | 63 | - (UICollectionViewLayoutAttributes *)conputeLayoutAttributesForItem: (NSIndexPath *)indexPath minVisibleIndex: (NSInteger)minVisibleIndex contentCenterX: (CGFloat)contentCenterX deltaOffset: (CGFloat)deltaOffset percentageDeltaOffset: (CGFloat)percentageDeltaOffset { 64 | if (self.collectionView == nil) { 65 | return [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 66 | } 67 | UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 68 | NSInteger cardIndex = indexPath.row - minVisibleIndex; 69 | attributes.size = self.itemSize; 70 | CGFloat scale = 1.0 - cardIndex * 0.1; 71 | contentCenterX -= 20; 72 | // 宽度减少的值 73 | CGFloat deltaX = self.itemSize.width * cardIndex * 0.1; 74 | CGFloat centerX = contentCenterX + self.spacing * cardIndex + deltaX; 75 | CGFloat centerY = CGRectGetMidY(self.collectionView.bounds); 76 | CGPoint center = CGPointMake(centerX, centerY); 77 | attributes.zIndex = self.maximumVisibleItems - cardIndex; 78 | attributes.size = self.itemSize; 79 | if (cardIndex == 0) { 80 | center.x -= deltaOffset; 81 | attributes.transform = CGAffineTransformIdentity; 82 | }else if (cardIndex >= 1 && cardIndex < self.maximumVisibleItems) { 83 | scale = scale + percentageDeltaOffset * 0.1; 84 | attributes.transform = CGAffineTransformMakeScale(scale, scale); 85 | center.x -= (self.spacing + 0.1 * self.itemSize.width) * percentageDeltaOffset; 86 | if (cardIndex == self.maximumVisibleItems - 1) { 87 | attributes.alpha = percentageDeltaOffset; 88 | } 89 | } 90 | attributes.center = center; 91 | return attributes; 92 | } 93 | 94 | 95 | // MARK: - Setter 96 | - (void)setItemSize:(CGSize)itemSize 97 | { 98 | _itemSize = itemSize; 99 | if (self.collectionView != nil) { 100 | [self invalidateLayout]; 101 | } 102 | } 103 | 104 | - (void)setSpacing:(CGFloat)spacing 105 | { 106 | _spacing = spacing; 107 | if (self.collectionView != nil) { 108 | [self invalidateLayout]; 109 | } 110 | } 111 | 112 | - (void)setMaximumVisibleItems:(NSInteger)maximumVisibleItems 113 | { 114 | _maximumVisibleItems = maximumVisibleItems; 115 | if (self.collectionView != nil) { 116 | [self invalidateLayout]; 117 | } 118 | } 119 | 120 | @end 121 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPCardLayoutViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPCardLayoutViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPCardLayoutViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPCardLayoutViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPCardLayoutViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPCardLayoutViewController.h" 10 | #import "ZFUtilities.h" 11 | #import "ZFTableData.h" 12 | #import "MPCardStackLayout.h" 13 | #import "MPCardLayoutCell.h" 14 | #import 15 | 16 | @interface MPCardLayoutViewController () 17 | 18 | 19 | @property (nonatomic, strong) UICollectionView *collectionView; 20 | @property (nonatomic, strong) NSMutableArray *dataSource; 21 | 22 | @end 23 | 24 | @implementation MPCardLayoutViewController 25 | 26 | - (void)viewDidLoad 27 | { 28 | [super viewDidLoad]; 29 | [self requestData]; 30 | [self setup]; 31 | } 32 | 33 | - (void)requestData 34 | { 35 | NSString *path = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"]; 36 | NSData *data = [NSData dataWithContentsOfFile:path]; 37 | NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 38 | 39 | self.dataSource = @[].mutableCopy; 40 | NSArray *videoList = [rootDict objectForKey:@"list"]; 41 | for (NSDictionary *dataDic in videoList) { 42 | ZFTableData *data = [[ZFTableData alloc] init]; 43 | [data setValuesForKeysWithDictionary:dataDic]; 44 | [self.dataSource addObject:data]; 45 | } 46 | } 47 | 48 | - (void)setup 49 | { 50 | self.view.backgroundColor = [UIColor whiteColor]; 51 | MPCardStackLayout *layout = [[MPCardStackLayout alloc] init]; 52 | CGFloat itemW = self.view.bounds.size.width - 16 - 20 * 2 - 15; 53 | CGFloat itemH = itemW + 97; 54 | layout.itemSize = CGSizeMake(itemW, itemH); 55 | self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; 56 | 57 | [self.collectionView registerClass:[MPCardLayoutCell class] forCellWithReuseIdentifier:@"MPCardLayoutCell"]; 58 | self.collectionView.pagingEnabled = YES; 59 | self.collectionView.showsVerticalScrollIndicator = NO; 60 | self.collectionView.showsHorizontalScrollIndicator = NO; 61 | self.collectionView.backgroundColor = [UIColor whiteColor]; 62 | self.collectionView.delegate = self; 63 | self.collectionView.dataSource = self; 64 | [self.view addSubview:self.collectionView]; 65 | 66 | self.collectionView.frame = CGRectMake(0, 100, self.view.bounds.size.width, itemH); 67 | } 68 | 69 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 70 | { 71 | return self.dataSource.count; 72 | } 73 | 74 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 75 | { 76 | MPCardLayoutCell *cell = (MPCardLayoutCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"MPCardLayoutCell" forIndexPath:indexPath]; 77 | ZFTableData *data = self.dataSource[indexPath.row]; 78 | [cell.coverImageView setImageWithURLString:data.thumbnail_url placeholderImageName:nil]; 79 | return cell; 80 | } 81 | 82 | @end 83 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPDetailViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPDetailViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MPPlayerController.h" 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPDetailViewController : UIViewController 14 | 15 | @property (nonatomic, strong) MPPlayerController *player; 16 | @property (nonatomic) NSInteger index; 17 | @property (nonatomic, strong) NSMutableArray *dataSource; 18 | @property (nonatomic, strong) UIView *startView; 19 | @property (nonatomic, strong) UIImage *startImage; 20 | @property (nonatomic, copy) void(^popbackBlock)(void); 21 | 22 | @end 23 | 24 | NS_ASSUME_NONNULL_END 25 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPDetailViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPDetailViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPDetailViewController.h" 10 | #import "ZFDouYinCell.h" 11 | #import "ZFDouYinControlView.h" 12 | #import 13 | #import 14 | #import "MPTransition.h" 15 | 16 | static NSString *kIdentifier = @"kIdentifier"; 17 | 18 | @interface MPDetailViewController () 19 | 20 | @property (nonatomic, strong) UITableView *tableView; 21 | @property (nonatomic, strong) UIButton *backBtn; 22 | @property (nonatomic, strong) ZFDouYinControlView *controlView; 23 | @property (nonatomic, strong) NSIndexPath *playingIndexPath; 24 | @property (nonatomic, strong) ZFPlayerControlView *preControlView; 25 | @property (nonatomic) BOOL isInited; 26 | @property (nonatomic) BOOL isPassPlayer; 27 | @end 28 | 29 | @implementation MPDetailViewController 30 | 31 | - (void)viewDidLoad 32 | { 33 | [super viewDidLoad]; 34 | self.view.backgroundColor = [UIColor whiteColor]; 35 | [self.view addSubview:self.tableView]; 36 | [self.view addSubview:self.backBtn]; 37 | 38 | @weakify(self) 39 | if (!self.player) { 40 | self.player = [MPPlayerController playrWithContainerView:[UIView new]]; 41 | [self requestData]; 42 | self.player.playableArray = self.dataSource; 43 | }else { 44 | for(UIView *subView in self.player.currentPlayerManager.view.subviews) { 45 | [subView removeFromSuperview]; 46 | } 47 | self.isPassPlayer = YES; 48 | } 49 | 50 | self.player.presentationSizeChanged = ^(id _Nonnull asset, CGSize size) { 51 | @strongify(self) 52 | if (size.width >= size.height) { 53 | self.player.currentPlayerManager.scalingMode = ZFPlayerScalingModeAspectFit; 54 | } else { 55 | self.player.currentPlayerManager.scalingMode = ZFPlayerScalingModeAspectFill; 56 | } 57 | }; 58 | [self.tableView reloadData]; 59 | self.playingIndexPath = [NSIndexPath indexPathForRow:self.index inSection:0]; 60 | [self.tableView scrollToRowAtIndexPath:self.playingIndexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO]; 61 | } 62 | 63 | - (void)viewWillLayoutSubviews { 64 | [super viewWillLayoutSubviews]; 65 | self.backBtn.frame = CGRectMake(15, CGRectGetMaxY([UIApplication sharedApplication].statusBarFrame), 36, 36); 66 | } 67 | 68 | - (void)viewWillAppear:(BOOL)animated { 69 | [super viewWillAppear:animated]; 70 | 71 | [self.navigationController setNavigationBarHidden:YES animated:animated]; 72 | } 73 | 74 | - (void)viewWillDisappear:(BOOL)animated { 75 | [super viewWillDisappear:animated]; 76 | 77 | [self.navigationController setNavigationBarHidden:NO animated:animated]; 78 | } 79 | 80 | - (void)requestData { 81 | NSString *path = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"]; 82 | NSData *data = [NSData dataWithContentsOfFile:path]; 83 | NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 84 | 85 | self.dataSource = @[].mutableCopy; 86 | NSArray *videoList = [rootDict objectForKey:@"list"]; 87 | for (NSDictionary *dataDic in videoList) { 88 | ZFTableData *data = [[ZFTableData alloc] init]; 89 | [data setValuesForKeysWithDictionary:dataDic]; 90 | [self.dataSource addObject:data]; 91 | } 92 | self.player.playableArray = self.dataSource; 93 | } 94 | 95 | - (void)viewDidAppear:(BOOL)animated 96 | { 97 | [super viewDidAppear:animated]; 98 | if (!self.isInited) { 99 | self.isInited = YES; 100 | if (self.isPassPlayer) { 101 | UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:self.playingIndexPath]; 102 | UIView *playerView = [cell viewWithTag:10086]; 103 | self.player.controlView = self.controlView; 104 | if (playerView) { 105 | [self.player updateNoramlPlayerWithContainerView:playerView]; 106 | [self.controlView resetControlView]; 107 | ZFTableData *data = self.dataSource[self.playingIndexPath.row]; 108 | UIViewContentMode imageMode; 109 | if (data.thumbnail_width >= data.thumbnail_height) { 110 | imageMode = UIViewContentModeScaleAspectFit; 111 | } else { 112 | imageMode = UIViewContentModeScaleAspectFill; 113 | } 114 | [self.controlView showCoverViewWithUrl:data.thumbnail_url withImageMode:imageMode]; 115 | } 116 | }else { 117 | self.player.controlView = self.controlView; 118 | [self playTheVideoAtIndexPath:self.playingIndexPath scrollToTop:NO]; 119 | } 120 | } 121 | self.navigationController.delegate = self; 122 | } 123 | 124 | - (void)didMoveToParentViewController:(UIViewController *)parent { 125 | if (!parent) { 126 | if (self.isPassPlayer) { 127 | [self.controlView removeFromSuperview]; 128 | for (UIView *subView in self.player.currentPlayerManager.view.subviews) { 129 | [subView removeFromSuperview]; 130 | } 131 | self.player.controlView = self.preControlView; 132 | if (self.popbackBlock) { 133 | self.popbackBlock(); 134 | } 135 | } 136 | } 137 | } 138 | 139 | #pragma mark - UIScrollViewDelegate 列表播放必须实现 140 | - (void)cellPlayVideo 141 | { 142 | if (self.tableView.visibleCells.count && self.tableView.visibleCells.count == 1) { 143 | UITableViewCell *cell = self.tableView.visibleCells.firstObject; 144 | NSIndexPath *ip = [self.tableView indexPathForCell:cell]; 145 | NSComparisonResult result = [ip compare:self.playingIndexPath]; 146 | // 判断indexPath是否发生变化 147 | if (ip && result != NSOrderedSame) { 148 | [self.player stop]; 149 | [self playTheVideoAtIndexPath:ip scrollToTop:NO]; 150 | } 151 | if (ip) { 152 | self.playingIndexPath = ip; 153 | } 154 | } 155 | } 156 | 157 | 158 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView 159 | { 160 | [self cellPlayVideo]; 161 | } 162 | 163 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView 164 | { 165 | [self handlePlayerOutofScreen: scrollView]; 166 | } 167 | 168 | - (void)handlePlayerOutofScreen: (UIScrollView *)scrollView 169 | { 170 | // 为了处理快速滑动时,player复用的bug 171 | UIView *cell = [self.tableView cellForRowAtIndexPath:self.playingIndexPath]; 172 | if (!cell && self.playingIndexPath) { 173 | [self.player stop]; 174 | return; 175 | } 176 | } 177 | 178 | #pragma mark - UITableViewDataSource 179 | 180 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 181 | return self.dataSource.count; 182 | } 183 | 184 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 185 | ZFDouYinCell *cell = [tableView dequeueReusableCellWithIdentifier:kIdentifier]; 186 | cell.data = self.dataSource[indexPath.row]; 187 | return cell; 188 | } 189 | 190 | #pragma mark - private method 191 | 192 | - (void)backClick:(UIButton *)sender { 193 | [self.navigationController popViewControllerAnimated:YES]; 194 | self.navigationController.delegate = self; 195 | } 196 | 197 | /// play the video 198 | - (void)playTheVideoAtIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop { 199 | UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath]; 200 | UIView *contanier = [cell viewWithTag:10086]; 201 | [self.player updateNoramlPlayerWithContainerView:contanier]; 202 | [self.player playWithPlayable:self.dataSource[indexPath.row]]; 203 | [self.controlView resetControlView]; 204 | ZFTableData *data = self.dataSource[indexPath.row]; 205 | UIViewContentMode imageMode; 206 | if (data.thumbnail_width >= data.thumbnail_height) { 207 | imageMode = UIViewContentModeScaleAspectFit; 208 | } else { 209 | imageMode = UIViewContentModeScaleAspectFill; 210 | } 211 | [self.controlView showCoverViewWithUrl:data.thumbnail_url withImageMode:imageMode]; 212 | self.playingIndexPath = indexPath; 213 | } 214 | 215 | - (UITableView *)tableView { 216 | if (!_tableView) { 217 | _tableView = [[UITableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; 218 | _tableView.pagingEnabled = YES; 219 | [_tableView registerClass:[ZFDouYinCell class] forCellReuseIdentifier:kIdentifier]; 220 | _tableView.backgroundColor = [UIColor lightGrayColor]; 221 | _tableView.delegate = self; 222 | _tableView.dataSource = self; 223 | _tableView.separatorStyle = UITableViewCellSeparatorStyleNone; 224 | _tableView.showsVerticalScrollIndicator = NO; 225 | _tableView.scrollsToTop = NO; 226 | if (@available(iOS 11.0, *)) { 227 | _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 228 | } else { 229 | self.automaticallyAdjustsScrollViewInsets = NO; 230 | } 231 | _tableView.estimatedRowHeight = 0; 232 | _tableView.estimatedSectionFooterHeight = 0; 233 | _tableView.estimatedSectionHeaderHeight = 0; 234 | _tableView.frame = self.view.bounds; 235 | _tableView.rowHeight = _tableView.frame.size.height; 236 | _tableView.scrollsToTop = NO; 237 | } 238 | return _tableView; 239 | } 240 | 241 | - (ZFDouYinControlView *)controlView { 242 | if (!_controlView) { 243 | _controlView = [ZFDouYinControlView new]; 244 | } 245 | return _controlView; 246 | } 247 | 248 | - (UIButton *)backBtn { 249 | if (!_backBtn) { 250 | _backBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 251 | [_backBtn setImage:[UIImage imageNamed:@"icon_titlebar_whiteback"] forState:UIControlStateNormal]; 252 | [_backBtn addTarget:self action:@selector(backClick:) forControlEvents:UIControlEventTouchUpInside]; 253 | } 254 | return _backBtn; 255 | } 256 | 257 | - (void)setPlayer:(MPPlayerController *)player 258 | { 259 | _player = player; 260 | self.preControlView = (ZFPlayerControlView *)player.controlView; 261 | [self.preControlView removeFromSuperview]; 262 | } 263 | 264 | - (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{ 265 | if (self.startView == nil) 266 | return nil; 267 | if (self.playingIndexPath && 268 | self.playingIndexPath.row != self.index) 269 | return nil; 270 | return [MPTransition animationWithDuration:0.3 startView:self.startView startImage:self.startImage player:self.player operation:operation completion:^{ 271 | }]; 272 | } 273 | 274 | 275 | @end 276 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPListViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPListViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPListViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPListViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPListViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPListViewController.h" 10 | #import "ZFTableViewCell.h" 11 | #import "ZFTableData.h" 12 | #import "MPPlayerController.h" 13 | #import 14 | #import "MPDetailViewController.h" 15 | #import "ZFUtilities.h" 16 | 17 | static NSString *kIdentifier = @"kIdentifier"; 18 | 19 | @interface MPListViewController () 20 | 21 | @property (nonatomic, strong) UITableView *tableView; 22 | @property (nonatomic, strong) NSMutableArray *dataSource; 23 | @property (nonatomic, strong) MPPlayerController *player; 24 | @property (nonatomic, strong) ZFPlayerControlView *controlView; 25 | @property (nonatomic, strong) NSMutableArray *playableArray; 26 | @property (nonatomic) BOOL isInited; 27 | 28 | @end 29 | 30 | @implementation MPListViewController 31 | 32 | - (void)viewDidLoad { 33 | [super viewDidLoad]; 34 | self.tableView = [[UITableView alloc] init]; 35 | self.tableView.delegate = self; 36 | self.tableView.dataSource = self; 37 | CGFloat y = iPhoneX ? 88 : 64; 38 | self.tableView.frame = CGRectMake(0, y, self.view.bounds.size.width, self.view.bounds.size.height - y); 39 | [self.tableView registerClass:[ZFTableViewCell class] forCellReuseIdentifier:kIdentifier]; 40 | if (@available(iOS 11.0, *)) { 41 | _tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 42 | } else { 43 | self.automaticallyAdjustsScrollViewInsets = NO; 44 | } 45 | _tableView.estimatedRowHeight = 0; 46 | _tableView.estimatedSectionFooterHeight = 0; 47 | _tableView.estimatedSectionHeaderHeight = 0; 48 | [self.view addSubview:self.tableView]; 49 | 50 | self.player = [MPPlayerController playerWithScrollView:self.tableView containerViewTag:100]; 51 | self.controlView = [[ZFPlayerControlView alloc] init]; 52 | self.player.controlView = self.controlView; 53 | [self setupPlayerDisappearBlock]; 54 | 55 | /// 停止的时候找出最合适的播放(只能找到设置了tag值cell) 56 | @weakify(self) 57 | _tableView.zf_scrollViewDidEndScrollingCallback = ^(NSIndexPath * _Nonnull indexPath) { 58 | @strongify(self) 59 | if (!self.player.playingIndexPath) { 60 | [self playTheVideoAtIndexPath:indexPath scrollToTop:NO]; 61 | } 62 | }; 63 | 64 | [self requestData]; 65 | } 66 | 67 | - (void)viewDidAppear:(BOOL)animated { 68 | [super viewDidAppear:animated]; 69 | if (!self.isInited) { 70 | self.isInited = YES; 71 | @weakify(self) 72 | [self.tableView zf_filterShouldPlayCellWhileScrolled:^(NSIndexPath *indexPath) { 73 | @strongify(self) 74 | [self playTheVideoAtIndexPath:indexPath scrollToTop:NO]; 75 | }]; 76 | } 77 | 78 | } 79 | 80 | - (void)setupPlayerDisappearBlock 81 | { 82 | @weakify(self) 83 | self.player.zf_playerDisappearingInScrollView = ^(NSIndexPath * _Nonnull indexPath, CGFloat playerDisapperaPercent) { 84 | @strongify(self) 85 | // 超出需要播放的百分比时,马上记录播放时间,方便下次seek 86 | if (playerDisapperaPercent >= self.player.playerDisapperaPercent) { 87 | ZFTableData *data = self.playableArray[indexPath.row]; 88 | data.current_time = self.player.currentPlayerManager.currentTime; 89 | } 90 | }; 91 | } 92 | 93 | - (void)requestData { 94 | NSString *path = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"]; 95 | NSData *data = [NSData dataWithContentsOfFile:path]; 96 | NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 97 | 98 | self.dataSource = @[].mutableCopy; 99 | self.playableArray = @[].mutableCopy; 100 | NSArray *videoList = [rootDict objectForKey:@"list"]; 101 | for (NSDictionary *dataDic in videoList) { 102 | ZFTableData *data = [[ZFTableData alloc] init]; 103 | [data setValuesForKeysWithDictionary:dataDic]; 104 | ZFTableViewCellLayout *layout = [[ZFTableViewCellLayout alloc] initWithData:data]; 105 | [self.playableArray addObject:data]; 106 | [self.dataSource addObject:layout]; 107 | } 108 | self.player.playableArray = self.playableArray; 109 | } 110 | 111 | /// play the video 112 | - (void)playTheVideoAtIndexPath:(NSIndexPath *)indexPath scrollToTop:(BOOL)scrollToTop { 113 | NSInteger index = indexPath.row; 114 | ZFTableViewCellLayout *layout = self.dataSource[index]; 115 | [self.player playTheIndexPath:indexPath playable:self.playableArray[index]]; 116 | // seek播放记录 117 | self.player.currentPlayerManager.seekTime = layout.data.current_time; 118 | [self.controlView showTitle:layout.data.title 119 | coverURLString:layout.data.thumbnail_url 120 | fullScreenMode:layout.isVerticalVideo?ZFFullScreenModePortrait:ZFFullScreenModeLandscape]; 121 | } 122 | 123 | #pragma mark - UIScrollViewDelegate 列表播放必须实现 124 | - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { 125 | [scrollView zf_scrollViewDidEndDecelerating]; 126 | } 127 | 128 | - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { 129 | [scrollView zf_scrollViewDidEndDraggingWillDecelerate:decelerate]; 130 | } 131 | 132 | - (void)scrollViewDidScrollToTop:(UIScrollView *)scrollView { 133 | [scrollView zf_scrollViewDidScrollToTop]; 134 | } 135 | 136 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { 137 | [scrollView zf_scrollViewDidScroll]; 138 | } 139 | 140 | - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { 141 | [scrollView zf_scrollViewWillBeginDragging]; 142 | } 143 | 144 | #pragma mark - UITableViewDataSource 145 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { 146 | return self.dataSource.count; 147 | } 148 | 149 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { 150 | ZFTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kIdentifier]; 151 | [cell setDelegate:self withIndexPath:indexPath]; 152 | cell.layout = self.dataSource[indexPath.row]; 153 | [cell setNormalMode]; 154 | return cell; 155 | } 156 | 157 | - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { 158 | ZFTableViewCellLayout *layout = self.dataSource[indexPath.row]; 159 | return layout.height; 160 | } 161 | 162 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 163 | { 164 | ZFTableViewCell *cell = (ZFTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]; 165 | NSIndexPath *currentIndexPath = [self.tableView indexPathForCell:cell]; 166 | // 点击的不是正在播放的cell,就先播放再跳转 167 | if ([currentIndexPath compare:self.tableView.zf_playingIndexPath] != NSOrderedSame) { 168 | [self.player stopCurrentPlayingCell]; 169 | self.tableView.zf_playingIndexPath = currentIndexPath; 170 | [self playTheVideoAtIndexPath:currentIndexPath scrollToTop:NO]; 171 | [self.player.currentPlayerManager.view layoutIfNeeded]; 172 | } 173 | self.tableView.zf_playingIndexPath = currentIndexPath; 174 | 175 | MPDetailViewController *vc = [[MPDetailViewController alloc] init]; 176 | vc.player = self.player; 177 | vc.index = indexPath.row; 178 | vc.startImage = cell.coverImageView.image; 179 | vc.startView = cell.coverImageView; 180 | vc.dataSource = [self.playableArray mutableCopy]; 181 | @weakify(self) 182 | vc.popbackBlock = ^{ 183 | @strongify(self) 184 | [self.player updateScrollViewPlayerToCell]; 185 | [self.player.currentPlayerManager play]; 186 | }; 187 | self.navigationController.delegate = vc; 188 | [self.navigationController pushViewController:vc animated:YES]; 189 | } 190 | 191 | - (void)zf_playTheVideoAtIndexPath:(NSIndexPath *)indexPath { 192 | [self playTheVideoAtIndexPath:indexPath scrollToTop:NO]; 193 | } 194 | 195 | 196 | 197 | @end 198 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPUserDynamicDetailViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicDetailViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/18. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPUserDynamicDetailViewController : UIViewController 14 | 15 | @property (nonatomic) NSInteger index; 16 | @property (nonatomic) NSInteger totalCount; 17 | @property (nonatomic, strong) UIImage *iconImage; 18 | @property (nonatomic, strong) UIImageView *startImageView; 19 | @property (nonatomic, strong) UICollectionViewCell *startCell; 20 | 21 | @end 22 | 23 | NS_ASSUME_NONNULL_END 24 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPUserDynamicDetailViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicDetailViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/18. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPUserDynamicDetailViewController.h" 10 | #import "MPTransition.h" 11 | #import "MPUserDynamicViewController.h" 12 | #import "MPUserDynamicTransition.h" 13 | #import 14 | 15 | @interface MPUserDynamicDetailViewController () 16 | 17 | @property (nonatomic, strong) UIPercentDrivenInteractiveTransition *interactiveGes; 18 | @property (nonatomic, assign) BOOL isInteracting; 19 | @property (nonatomic, strong) MPUserDynamicTransition *transition; 20 | @property (nonatomic, assign) CGFloat startOffsetY; 21 | @property (nonatomic, assign) CGFloat lastOffsetY; 22 | 23 | @end 24 | 25 | @implementation MPUserDynamicDetailViewController 26 | 27 | - (void)viewDidLoad { 28 | [super viewDidLoad]; 29 | self.view.backgroundColor = [UIColor redColor]; 30 | self.interactiveGes = [[UIPercentDrivenInteractiveTransition alloc] init]; 31 | self.isInteracting = YES; 32 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panAction:)]; 33 | [self.view addGestureRecognizer:pan]; 34 | self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:UIBarButtonItemStylePlain target:self action:@selector(backAction)]; 35 | } 36 | 37 | - (void)viewDidAppear:(BOOL)animated 38 | { 39 | [super viewDidAppear:animated]; 40 | self.navigationController.delegate = self; 41 | } 42 | 43 | - (void)backAction 44 | { 45 | self.isInteracting = NO; 46 | [self.navigationController popViewControllerAnimated:YES]; 47 | } 48 | 49 | - (CGFloat)percentForGesture:(UIPanGestureRecognizer *)gesture{ 50 | // 最多只能移动SL_SCREEN_HEIGHT * 0.5 51 | CGFloat maxOffset = ZFPlayer_ScreenHeight * 0.5; 52 | CGFloat y = [gesture locationInView:[UIApplication sharedApplication].keyWindow].y; 53 | // 移动的距离 54 | CGFloat distance = y - self.startOffsetY; 55 | distance = MIN(maxOffset, distance); 56 | double degree = (distance / maxOffset) * M_PI_2; 57 | // 为增量实现一个曲线变化的效果 58 | double x = 1 - (sin(degree)); 59 | // 计算增量 60 | CGFloat delta = distance - self.lastOffsetY; 61 | self.lastOffsetY = self.lastOffsetY + x * delta; 62 | self.lastOffsetY = MAX(self.lastOffsetY, 0); 63 | CGFloat percent = self.lastOffsetY / maxOffset; 64 | return percent; 65 | } 66 | 67 | 68 | - (void)panAction: (UIPanGestureRecognizer *)gestureRecognizer 69 | { 70 | switch (gestureRecognizer.state){ 71 | case UIGestureRecognizerStateBegan: 72 | { 73 | self.startOffsetY = [gestureRecognizer locationInView:[UIApplication sharedApplication].keyWindow].y; 74 | [self.navigationController popViewControllerAnimated:YES]; 75 | break; 76 | } 77 | case UIGestureRecognizerStateChanged: 78 | // 调用updateInteractiveTransition来更新动画进度 79 | // 里面嵌套定义 percentForGesture 方法计算动画进度 80 | [self.interactiveGes updateInteractiveTransition:[self percentForGesture:gestureRecognizer]]; 81 | break; 82 | case UIGestureRecognizerStateEnded: 83 | //判断手势位置,要大于一般,就完成这个转场,要小于一半就取消 84 | if ([self percentForGesture:gestureRecognizer] >= 0.4) { 85 | self.transition.isComplete = YES; 86 | // 完成交互转场 87 | [self.interactiveGes finishInteractiveTransition]; 88 | }else { 89 | // 取消交互转场 90 | [self.interactiveGes cancelInteractiveTransition]; 91 | } 92 | break; 93 | default: 94 | [self.interactiveGes cancelInteractiveTransition]; 95 | break; 96 | } 97 | } 98 | 99 | // MARK: - Transition 100 | - (id)navigationController:(UINavigationController *)navigationController animationControllerForOperation:(UINavigationControllerOperation)operation fromViewController:(UIViewController *)fromVC toViewController:(UIViewController *)toVC{ 101 | UIView *startView = self.startCell; 102 | UIImage *startImage = self.iconImage; 103 | CGFloat endX = 0; 104 | if (operation == UINavigationControllerOperationPop) { 105 | endX = [self getPopTransitionEndX]; 106 | } 107 | if (startView == nil) 108 | return nil; 109 | NSTimeInterval duration = 0.2; 110 | MPUserDynamicTransition *transition = [MPUserDynamicTransition animationWithDuration:duration startView:startView startImage:startImage endX:endX operation:operation]; 111 | if (operation == UINavigationControllerOperationPop) { 112 | transition.isInteracting = self.isInteracting; 113 | } 114 | self.transition = transition; 115 | return transition; 116 | } 117 | 118 | - (id )navigationController:(UINavigationController *)navigationController interactionControllerForAnimationController:(id)animationController 119 | { 120 | return self.isInteracting ? self.interactiveGes : nil; 121 | } 122 | 123 | - (CGFloat)getPopTransitionEndX 124 | { 125 | CGRect winFrame = [self.startCell convertRect:self.startCell.frame toView:nil]; 126 | return winFrame.origin.x; 127 | } 128 | 129 | @end 130 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPUserDynamicViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/18. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPUserDynamicViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPUserDynamicViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/18. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPUserDynamicViewController.h" 10 | #import "MPUserDynamicCollectionViewCell.h" 11 | #import "ZFUtilities.h" 12 | #import "ZFTableData.h" 13 | #import "MPUserDynamicDetailViewController.h" 14 | #import 15 | 16 | @interface MPUserDynamicViewController ()< 17 | UICollectionViewDelegate, 18 | UICollectionViewDataSource 19 | > 20 | 21 | @property (nonatomic, strong) UICollectionView *collectionView; 22 | @property (nonatomic, strong) NSMutableArray *dataSource; 23 | @end 24 | 25 | @implementation MPUserDynamicViewController 26 | 27 | - (void)viewDidLoad { 28 | [super viewDidLoad]; 29 | [self requestData]; 30 | [self setup]; 31 | } 32 | 33 | - (void)requestData 34 | { 35 | NSString *path = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"]; 36 | NSData *data = [NSData dataWithContentsOfFile:path]; 37 | NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 38 | 39 | self.dataSource = @[].mutableCopy; 40 | NSArray *videoList = [rootDict objectForKey:@"list"]; 41 | for (NSDictionary *dataDic in videoList) { 42 | ZFTableData *data = [[ZFTableData alloc] init]; 43 | [data setValuesForKeysWithDictionary:dataDic]; 44 | [self.dataSource addObject:data]; 45 | } 46 | } 47 | 48 | - (void)setup 49 | { 50 | self.view.backgroundColor = [UIColor whiteColor]; 51 | UICollectionViewFlowLayout *layout = [[UICollectionViewFlowLayout alloc] init]; 52 | layout.scrollDirection = UICollectionViewScrollDirectionHorizontal; 53 | layout.itemSize = CGSizeMake(80, 80); 54 | self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectZero collectionViewLayout:layout]; 55 | 56 | [self.collectionView registerClass:[MPUserDynamicCollectionViewCell class] forCellWithReuseIdentifier:@"MPUserDynamicCollectionViewCell"]; 57 | self.collectionView.showsVerticalScrollIndicator = NO; 58 | self.collectionView.showsHorizontalScrollIndicator = NO; 59 | self.collectionView.backgroundColor = [UIColor whiteColor]; 60 | self.collectionView.delegate = self; 61 | self.collectionView.dataSource = self; 62 | [self.view addSubview:self.collectionView]; 63 | 64 | self.collectionView.frame = CGRectMake(0, 100, self.view.frame.size.width, 80); 65 | } 66 | 67 | #pragma mark - UICollectionViewDelegate, UICollectionViewDataSource 68 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 69 | { 70 | return self.dataSource.count; 71 | } 72 | 73 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 74 | { 75 | MPUserDynamicCollectionViewCell *cell = (MPUserDynamicCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"MPUserDynamicCollectionViewCell" forIndexPath:indexPath]; 76 | ZFTableData *data = self.dataSource[indexPath.row]; 77 | [cell.iconImageView setImageWithURLString:data.head placeholder:nil]; 78 | return cell; 79 | } 80 | 81 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath 82 | { 83 | MPUserDynamicDetailViewController *vc = [[MPUserDynamicDetailViewController alloc] init]; 84 | vc.index = indexPath.row; 85 | vc.totalCount = self.dataSource.count; 86 | MPUserDynamicCollectionViewCell *cell = (MPUserDynamicCollectionViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath]; 87 | vc.iconImage = cell.iconImageView.image; 88 | vc.startImageView = cell.iconImageView; 89 | vc.startCell = cell; 90 | self.navigationController.delegate = vc; 91 | [self.navigationController pushViewController:vc animated:YES]; 92 | } 93 | 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPWaterFallViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPWaterFallViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/1/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPWaterFallViewController : UIViewController 14 | 15 | @end 16 | 17 | NS_ASSUME_NONNULL_END 18 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Controller/MPWaterFallViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPWaterFallViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/1/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPWaterFallViewController.h" 10 | #import "MPWaterFallCollectionViewCell.h" 11 | #import "MPWaterFallLayout.h" 12 | #import "ZFUtilities.h" 13 | #import "ZFTableData.h" 14 | #import "MPDetailViewController.h" 15 | 16 | @interface MPWaterFallViewController () 17 | 18 | @property (nonatomic, strong) UICollectionView *collectionView; 19 | @property (nonatomic, strong) NSMutableArray *dataSource; 20 | @property (nonatomic, strong) MPWaterFallLayout *layout; 21 | 22 | @end 23 | 24 | @implementation MPWaterFallViewController 25 | 26 | - (void)viewDidLoad { 27 | [super viewDidLoad]; 28 | [self setupUI]; 29 | } 30 | 31 | - (void)setupUI 32 | { 33 | self.view.backgroundColor = [UIColor whiteColor]; 34 | 35 | MPWaterFallLayout *layout = [MPWaterFallLayout waterFallLayoutWithColumn:2]; 36 | layout.sectionInset = UIEdgeInsetsMake(10, 10, 10, 0); 37 | layout.columnSpacing = 10; 38 | layout.rowSpacing = 10; 39 | layout.dataSource = self; 40 | self.layout = layout; 41 | 42 | CGFloat y = iPhoneX ? 88 : 64; 43 | self.collectionView = [[UICollectionView alloc] initWithFrame:CGRectMake(0, y, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight - y) collectionViewLayout:layout]; 44 | self.collectionView.backgroundColor = [UIColor whiteColor]; 45 | self.collectionView.delegate = self; 46 | self.collectionView.dataSource = self; 47 | [self.collectionView registerClass:[MPWaterFallCollectionViewCell class] forCellWithReuseIdentifier:@"MPWaterFallCollectionViewCellID"]; 48 | if (@available(iOS 11.0, *)) { 49 | self.collectionView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; 50 | } else { 51 | self.automaticallyAdjustsScrollViewInsets = NO; 52 | } 53 | [self.view addSubview:self.collectionView]; 54 | [self requestData]; 55 | [self.collectionView reloadData]; 56 | } 57 | 58 | - (void)requestData { 59 | NSString *path = [[NSBundle mainBundle] pathForResource:@"data" ofType:@"json"]; 60 | NSData *data = [NSData dataWithContentsOfFile:path]; 61 | NSDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:nil]; 62 | 63 | self.dataSource = @[].mutableCopy; 64 | NSArray *videoList = [rootDict objectForKey:@"list"]; 65 | for (NSDictionary *dataDic in videoList) { 66 | ZFTableData *data = [[ZFTableData alloc] init]; 67 | [data setValuesForKeysWithDictionary:dataDic]; 68 | data.thumbnail_height = (self.layout.itemWidth / data.thumbnail_width) * data.thumbnail_height; 69 | [self.dataSource addObject:data]; 70 | } 71 | } 72 | 73 | 74 | // MARK: - MPWaterFallLayoutDataSource 75 | - (CGFloat)waterFallLayout:(MPWaterFallLayout *)layout itemHeightForItemWidth:(CGFloat)itemWidth atIndexPath:(NSIndexPath *)indexPath 76 | { 77 | ZFTableData *data = self.dataSource[indexPath.row]; 78 | return data.thumbnail_height; 79 | } 80 | 81 | // MARK: - UICollectionViewDelegate, UICollectionViewDataSource 82 | - (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section 83 | { 84 | return self.dataSource.count; 85 | } 86 | 87 | - (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 88 | { 89 | MPWaterFallCollectionViewCell *cell = (MPWaterFallCollectionViewCell *)[collectionView dequeueReusableCellWithReuseIdentifier:@"MPWaterFallCollectionViewCellID" forIndexPath:indexPath]; 90 | ZFTableData *data = self.dataSource[indexPath.row]; 91 | cell.data = data; 92 | return cell; 93 | } 94 | 95 | - (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath 96 | { 97 | MPWaterFallCollectionViewCell *cell = (MPWaterFallCollectionViewCell *)[collectionView cellForItemAtIndexPath:indexPath]; 98 | MPDetailViewController *vc = [[MPDetailViewController alloc] init]; 99 | vc.index = indexPath.row; 100 | vc.startImage = cell.imageView.image; 101 | vc.startView = cell.imageView; 102 | vc.dataSource = [self.dataSource mutableCopy]; 103 | self.navigationController.delegate = vc; 104 | [self.navigationController pushViewController:vc animated:YES]; 105 | } 106 | 107 | @end 108 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSAppTransportSecurity 6 | 7 | NSAllowsArbitraryLoads 8 | 9 | 10 | CFBundleDevelopmentRegion 11 | $(DEVELOPMENT_LANGUAGE) 12 | CFBundleExecutable 13 | $(EXECUTABLE_NAME) 14 | CFBundleIdentifier 15 | $(PRODUCT_BUNDLE_IDENTIFIER) 16 | CFBundleInfoDictionaryVersion 17 | 6.0 18 | CFBundleName 19 | $(PRODUCT_NAME) 20 | CFBundlePackageType 21 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 22 | CFBundleShortVersionString 23 | 1.0 24 | CFBundleVersion 25 | 1 26 | LSRequiresIPhoneOS 27 | 28 | UILaunchStoryboardName 29 | LaunchScreen 30 | UIMainStoryboardFile 31 | Main 32 | UIRequiredDeviceCapabilities 33 | 34 | armv7 35 | 36 | UISupportedInterfaceOrientations 37 | 38 | UIInterfaceOrientationPortrait 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UISupportedInterfaceOrientations~ipad 43 | 44 | UIInterfaceOrientationPortrait 45 | UIInterfaceOrientationPortraitUpsideDown 46 | UIInterfaceOrientationLandscapeLeft 47 | UIInterfaceOrientationLandscapeRight 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Model/ZFTableData.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZFTableData.h 3 | // ZFPlayer 4 | // 5 | // Created by 紫枫 on 2018/4/24. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "MPPlayableProtocol.h" 11 | #import 12 | 13 | @interface ZFTableData : NSObject 14 | /// 记录播放进度 15 | @property (nonatomic, assign) NSTimeInterval current_time; 16 | @property (nonatomic, copy) NSString *nick_name; 17 | @property (nonatomic, copy) NSString *head; 18 | @property (nonatomic, assign) NSInteger agree_num; 19 | @property (nonatomic, assign) NSInteger share_num; 20 | @property (nonatomic, assign) NSInteger post_num; 21 | @property (nonatomic, copy) NSString *title; 22 | @property (nonatomic, assign) CGFloat thumbnail_width; 23 | @property (nonatomic, assign) CGFloat thumbnail_height; 24 | @property (nonatomic, assign) CGFloat video_duration; 25 | @property (nonatomic, assign) CGFloat video_width; 26 | @property (nonatomic, assign) CGFloat video_height; 27 | @property (nonatomic, copy) NSString *thumbnail_url; 28 | @property (nonatomic, copy) NSString *video_url; 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Model/ZFTableData.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZFTableData.m 3 | // ZFPlayer 4 | // 5 | // Created by 紫枫 on 2018/4/24. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import "ZFTableData.h" 10 | 11 | @implementation ZFTableData 12 | 13 | - (void)setValue:(id)value forUndefinedKey:(NSString *)key {} 14 | 15 | @end 16 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Other/NSString+Size.h: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Size.h 3 | // createImg 4 | // 5 | // Created by 紫枫 on 2018/4/3. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface NSString (Size) 12 | /** 13 | 根据字体、行数、行间距和指定的宽度constrainedWidth计算文本占据的size 14 | @param font 字体 15 | @param numberOfLines 显示文本行数,值为0不限制行数 16 | @param lineSpacing 行间距 17 | @param constrainedWidth 文本指定的宽度 18 | @return 返回文本占据的size 19 | */ 20 | - (CGSize)textSizeWithFont:(UIFont*)font 21 | numberOfLines:(NSInteger)numberOfLines 22 | lineSpacing:(CGFloat)lineSpacing 23 | constrainedWidth:(CGFloat)constrainedWidth; 24 | 25 | /** 26 | 根据字体、行数、行间距和指定的宽度constrainedWidth计算文本占据的size 27 | @param font 字体 28 | @param numberOfLines 显示文本行数,值为0不限制行数 29 | @param constrainedWidth 文本指定的宽度 30 | @return 返回文本占据的size 31 | */ 32 | - (CGSize)textSizeWithFont:(UIFont*)font 33 | numberOfLines:(NSInteger)numberOfLines 34 | constrainedWidth:(CGFloat)constrainedWidth; 35 | 36 | /// 计算字符串长度(一行时候) 37 | - (CGSize)textSizeWithFont:(UIFont*)font 38 | limitWidth:(CGFloat)maxWidth; 39 | 40 | - (CGSize)textSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(NSLineBreakMode)lineBreakMode; 41 | 42 | @end 43 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Other/NSString+Size.m: -------------------------------------------------------------------------------- 1 | // 2 | // NSString+Size.m 3 | // createImg 4 | // 5 | // Created by 紫枫 on 2018/4/3. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import "NSString+Size.h" 10 | 11 | @implementation NSString (Size) 12 | 13 | /** 14 | 根据字体、行数、行间距和constrainedWidth计算文本占据的size 15 | **/ 16 | - (CGSize)textSizeWithFont:(UIFont*)font 17 | numberOfLines:(NSInteger)numberOfLines 18 | lineSpacing:(CGFloat)lineSpacing 19 | constrainedWidth:(CGFloat)constrainedWidth{ 20 | 21 | if (self.length == 0) { 22 | return CGSizeZero; 23 | } 24 | CGFloat oneLineHeight = font.lineHeight; 25 | CGSize textSize = [self boundingRectWithSize:CGSizeMake(constrainedWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size; 26 | // 行数 27 | CGFloat rows = textSize.height / oneLineHeight; 28 | CGFloat realHeight = oneLineHeight; 29 | // 0 不限制行数,真实高度加上行间距 30 | if (numberOfLines == 0) { 31 | if (rows >= 1) { 32 | realHeight = (rows * oneLineHeight) + (rows - 1) * lineSpacing; 33 | } 34 | } else { 35 | // 行数超过指定行数的时候,限制行数 36 | if (rows > numberOfLines) { 37 | rows = numberOfLines; 38 | } 39 | realHeight = (rows * oneLineHeight) + (rows - 1) * lineSpacing; 40 | } 41 | // 返回真实的宽高 42 | return CGSizeMake(constrainedWidth, realHeight); 43 | } 44 | 45 | - (CGSize)textSizeWithFont:(UIFont*)font 46 | numberOfLines:(NSInteger)numberOfLines 47 | constrainedWidth:(CGFloat)constrainedWidth{ 48 | 49 | if (self.length == 0) { 50 | return CGSizeZero; 51 | } 52 | CGFloat oneLineHeight = font.lineHeight; 53 | CGSize textSize = [self boundingRectWithSize:CGSizeMake(constrainedWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:font} context:nil].size; 54 | // 行数 55 | CGFloat rows = textSize.height / oneLineHeight; 56 | CGFloat realHeight = oneLineHeight; 57 | // 0 不限制行数,真实高度加上行间距 58 | if (numberOfLines == 0) { 59 | if (rows >= 1) { 60 | realHeight = (rows * oneLineHeight) + (rows - 1) ; 61 | } 62 | } else { 63 | // 行数超过指定行数的时候,限制行数 64 | if (rows > numberOfLines) { 65 | rows = numberOfLines; 66 | } 67 | realHeight = (rows * oneLineHeight) + (rows - 1) ; 68 | } 69 | // 返回真实的宽高 70 | return CGSizeMake(constrainedWidth, realHeight); 71 | } 72 | 73 | /// 计算字符串长度(一行时候) 74 | - (CGSize)textSizeWithFont:(UIFont*)font 75 | limitWidth:(CGFloat)maxWidth { 76 | CGSize size = [self boundingRectWithSize:CGSizeMake(MAXFLOAT, 36)options:(NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading) attributes:@{ NSFontAttributeName : font} context:nil].size; 77 | size.width = size.width > maxWidth ? maxWidth : size.width; 78 | size.width = ceil(size.width); 79 | size.height = ceil(size.height); 80 | return size; 81 | } 82 | 83 | - (CGSize)textSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(NSLineBreakMode)lineBreakMode { 84 | CGSize textSize; 85 | if (CGSizeEqualToSize(size, CGSizeZero)) { 86 | NSDictionary *attributes = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName]; 87 | textSize = [self sizeWithAttributes:attributes]; 88 | } else { 89 | textSize = [self sizeWithFont:font constrainedToSize:size lineBreakMode:NSLineBreakByCharWrapping]; 90 | } 91 | return textSize; 92 | } 93 | 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Other/ZFTableViewCellLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZFTableViewCellLayout.h 3 | // ZFPlayer 4 | // 5 | // Created by 紫枫 on 2018/5/22. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ZFTableData.h" 11 | 12 | @interface ZFTableViewCellLayout : NSObject 13 | @property (nonatomic, strong) ZFTableData *data; 14 | @property (nonatomic, readonly) CGRect headerRect; 15 | @property (nonatomic, readonly) CGRect nickNameRect; 16 | @property (nonatomic, readonly) CGRect videoRect; 17 | @property (nonatomic, readonly) CGRect playBtnRect; 18 | @property (nonatomic, readonly) CGRect titleLabelRect; 19 | @property (nonatomic, readonly) CGRect maskViewRect; 20 | @property (nonatomic, readonly) CGFloat height; 21 | @property (nonatomic, readonly) BOOL isVerticalVideo; 22 | 23 | - (instancetype)initWithData:(ZFTableData *)data; 24 | 25 | @end 26 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Other/ZFTableViewCellLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZFTableViewCellLayout.m 3 | // ZFPlayer 4 | // 5 | // Created by 紫枫 on 2018/5/22. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import "ZFTableViewCellLayout.h" 10 | #import "NSString+Size.h" 11 | 12 | @interface ZFTableViewCellLayout () 13 | 14 | @property (nonatomic, assign) CGRect headerRect; 15 | @property (nonatomic, assign) CGRect nickNameRect; 16 | @property (nonatomic, assign) CGRect videoRect; 17 | @property (nonatomic, assign) CGRect playBtnRect; 18 | @property (nonatomic, assign) CGRect titleLabelRect; 19 | @property (nonatomic, assign) CGRect maskViewRect; 20 | @property (nonatomic, assign) BOOL isVerticalVideo; 21 | @property (nonatomic, assign) CGFloat height; 22 | 23 | @end 24 | 25 | @implementation ZFTableViewCellLayout 26 | 27 | - (instancetype)initWithData:(ZFTableData *)data { 28 | self = [super init]; 29 | if (self) { 30 | _data = data; 31 | 32 | CGFloat min_x = 0; 33 | CGFloat min_y = 0; 34 | CGFloat min_w = 0; 35 | CGFloat min_h = 0; 36 | CGFloat min_view_w = [UIScreen mainScreen].bounds.size.width; 37 | CGFloat margin = 10; 38 | 39 | min_x = 10; 40 | min_y = 10; 41 | min_w = 30; 42 | min_h = min_w; 43 | self.headerRect = CGRectMake(min_x, min_y, min_w, min_h); 44 | 45 | min_x = CGRectGetMaxX(self.headerRect) + 10; 46 | min_y = 18; 47 | min_w = [data.nick_name textSizeWithFont:[UIFont systemFontOfSize:15] limitWidth:min_view_w-2*margin-min_x].width; 48 | min_h = 15; 49 | self.nickNameRect = CGRectMake(min_x, min_y, min_w, min_h); 50 | 51 | min_x = 0; 52 | min_y = CGRectGetMaxY(self.headerRect)+margin; 53 | min_w = min_view_w; 54 | min_h = self.videoHeight; 55 | self.videoRect = CGRectMake(min_x, min_y, min_w, min_h); 56 | 57 | min_w = 44; 58 | min_h = min_w; 59 | min_x = (CGRectGetWidth(self.videoRect)-min_w)/2; 60 | min_y = (CGRectGetHeight(self.videoRect)-min_h)/2; 61 | self.playBtnRect = CGRectMake(min_x, min_y, min_w, min_h); 62 | 63 | min_x = margin; 64 | min_y = CGRectGetMaxY(self.videoRect) + margin; 65 | min_w = CGRectGetWidth(self.videoRect) - 2*margin; 66 | min_h = [data.title textSizeWithFont:[UIFont systemFontOfSize:15] numberOfLines:0 constrainedWidth:min_w].height; 67 | self.titleLabelRect = CGRectMake(min_x, min_y, min_w, min_h); 68 | 69 | self.height = CGRectGetMaxY(self.titleLabelRect)+margin; 70 | 71 | min_x = 0; 72 | min_y = 0; 73 | min_w = min_view_w; 74 | min_h = self.height; 75 | self.maskViewRect = CGRectMake(min_x, min_y, min_w, min_h); 76 | 77 | } 78 | return self; 79 | } 80 | 81 | - (BOOL)isVerticalVideo { 82 | return _data.video_width < _data.video_height; 83 | } 84 | 85 | - (CGFloat)videoHeight { 86 | CGFloat videoHeight; 87 | if (self.isVerticalVideo) { 88 | videoHeight = [UIScreen mainScreen].bounds.size.width * 0.6 * self.data.video_height/self.data.video_width; 89 | } else { 90 | videoHeight = [UIScreen mainScreen].bounds.size.width * self.data.video_height/self.data.video_width; 91 | } 92 | return videoHeight; 93 | } 94 | 95 | @end 96 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPPlayableProtocol.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef XSTViedoFlowProtocol_h 3 | #define XSTViedoFlowProtocol_h 4 | 5 | /// XSTPlayerController播放的模型,必须实现这个协议 6 | @protocol XSTPlayable 7 | /// string 视频链接 8 | @property (nonatomic, copy) NSString *video_url; 9 | @end 10 | 11 | 12 | #endif /* XSTViedoFlowProtocol_h */ 13 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPPlayerController.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #import 4 | #import 5 | #import "MPPlayableProtocol.h" 6 | 7 | NS_ASSUME_NONNULL_BEGIN 8 | 9 | @interface MPPlayerController : NSObject 10 | 11 | // 预加载上几条 12 | @property (nonatomic, assign) NSUInteger preLoadNum; 13 | /// 预加载下几条 14 | @property (nonatomic, assign) NSUInteger nextLoadNum; 15 | /// 预加载的的百分比,默认10% 16 | @property (nonatomic, assign) double preloadPrecent; 17 | /// 设置playableAssets后,马上预加载的条数 18 | @property (nonatomic, assign) NSUInteger initPreloadNum; 19 | /// The indexPath is playing. 20 | @property (nonatomic, readonly, nullable) NSIndexPath *playingIndexPath; 21 | /// The current player controller is disappear, not dealloc 22 | @property (nonatomic, getter=isViewControllerDisappear) BOOL viewControllerDisappear; 23 | @property (nonatomic, readonly) UIView *containerView; 24 | /// 可播放的视频的模型数组,若是混合区域,模型需要实现XSTPlayable 25 | /// set之后,先预加载几个 26 | @property (nonatomic, copy) NSArray> *playableArray; 27 | /// 当前正在播放的 MPPlayable 资源 28 | @property (nonatomic, strong, readonly) id currentPlayable; 29 | /// The currentPlayerManager must conform `ZFPlayerMediaPlayback` protocol. 30 | @property (nonatomic, strong) id currentPlayerManager; 31 | /// The custom controlView must conform `ZFPlayerMediaControl` protocol. 32 | @property (nonatomic, strong) UIView *controlView; 33 | /// 保存player在信息流时,应该显示的scalingMode 34 | @property (nonatomic, assign) ZFPlayerScalingMode videoFlowScalingMode; 35 | @property (nonatomic) CGFloat playerDisapperaPercent; 36 | @property (nonatomic) CGFloat playerApperaPercent; 37 | @property (nonatomic, getter=isWWANAutoPlay) BOOL WWANAutoPlay; 38 | @property (nonatomic, assign) BOOL isPlaying; 39 | 40 | // MARK: - Block 41 | /// 准备播放的block 42 | @property (nonatomic, copy, nullable) void(^playerReadyToPlay)(id asset, NSURL *assetURL); 43 | /// 播放进度的block 44 | @property (nonatomic, copy, nullable) void(^playerPlayTimeChanged)(id asset, NSTimeInterval currentTime, NSTimeInterval duration); 45 | /// 播放缓存时间的block 46 | @property (nonatomic, copy, nullable) void(^playerBufferTimeChanged)(id asset, NSTimeInterval bufferTime); 47 | /// 播放失败回调 48 | @property (nonatomic, copy, nullable) void(^playerPlayFailed)(id asset, id error); 49 | /// 播放到结尾的回调 50 | @property (nonatomic, copy, nullable) void(^playerDidToEnd)(id asset); 51 | // 播放器size变化的回调 52 | @property (nonatomic, copy, nullable) void(^presentationSizeChanged)(id asset, CGSize size); 53 | /// The block invoked when the player playback state changed. 54 | @property (nonatomic, copy, nullable) void(^playerPlayStateChanged)(id asset, ZFPlayerPlaybackState playState); 55 | /// The block invoked when the player load state changed. 56 | @property (nonatomic, copy, nullable) void(^playerLoadStateChanged)(id asset, ZFPlayerLoadState loadState); 57 | @property (nonatomic, copy, nullable) void(^zf_playerDisappearingInScrollView)(NSIndexPath *indexPath, CGFloat playerDisapperaPercent); 58 | @property (nonatomic, copy, nullable) void(^zf_playerDidDisappearInScrollView)(NSIndexPath *indexPath); 59 | 60 | 61 | // MARK: - Init 62 | /** 63 | 创建播放单个视频的PlayerController 64 | 65 | @param containerView 指定显示视频的容器 66 | @return MPPlayerController实例 67 | */ 68 | + (instancetype)playrWithContainerView:(UIView *)containerView; 69 | 70 | /** 71 | 创建播放单个视频的PlayerController 72 | 73 | @param containerView 指定显示视频的容器 74 | @return MPPlayerController实例 75 | */ 76 | - (instancetype)initWithContainerView:(UIView *)containerView; 77 | 78 | /** 79 | 在UITableView或UICollectionView中使用的PlayerController 80 | 81 | @param scrollView UITableView或UICollectionView 82 | @param containerViewTag 指定显示视频的容器tag,cell的子视图的tag 83 | @return PlayerController实例 84 | */ 85 | + (instancetype)playerWithScrollView:(UIScrollView *)scrollView containerViewTag:(NSInteger)containerViewTag; 86 | 87 | /** 88 | 在UITableView或UICollectionView中使用的PlayerController 89 | 90 | @param scrollView UITableView或UICollectionView 91 | @param containerViewTag 指定显示视频的容器tag,cell的子视图的tag 92 | @return PlayerController实例 93 | */ 94 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView containerViewTag:(NSInteger)containerViewTag; 95 | 96 | // MARK: - Method 97 | /// 移除player,移除其他通知 98 | - (void)stop; 99 | 100 | /// 停止播放播放的Cell 101 | - (void)stopCurrentPlayingCell; 102 | 103 | /// 播放对应的indexPath,传入resouce 104 | - (void)playTheIndexPath:(NSIndexPath *)indexPath playable: (id)playable; 105 | 106 | /// 播放指定的url 107 | - (void)playWithPlayable: (id)playable; 108 | 109 | /// 设置player显示,消失的百分比,用于判断自动播放和暂停 110 | - (void)setDisapperaPercent: (CGFloat)disappearPercent appearPercent: (CGFloat)appearPercent; 111 | 112 | /// 横屏显示 113 | - (void)enterLandscapeFullScreen:(UIInterfaceOrientation)orientation animated:(BOOL)animated; 114 | 115 | /// 退出全屏 116 | - (void)exitFullScreen: (BOOL)isAnimated; 117 | 118 | /// Add Player到Cell上 119 | - (void)updateScrollViewPlayerToCell; 120 | 121 | /// 更新Playerc的容器 122 | - (void)updateNoramlPlayerWithContainerView:(UIView *)containerView; 123 | 124 | 125 | 126 | @end 127 | 128 | NS_ASSUME_NONNULL_END 129 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPPlayerController.m: -------------------------------------------------------------------------------- 1 | // 2 | // XSTPlayerController.m 3 | // XStarSDK 4 | // 5 | // Created by Beauty-ruanjian on 2019/7/4. 6 | // 7 | 8 | #import "MPPlayerController.h" 9 | #import "MPlayerAttributeManager.h" 10 | #import 11 | #import "MPPreLoaderModel.h" 12 | #import 13 | 14 | @interface MPPlayerController() 15 | 16 | @property (nonatomic, strong) ZFPlayerController *player; 17 | /// 预加载的模型数组 18 | @property (nonatomic, strong) NSMutableArray *preloadArr; 19 | @property (nonatomic, assign) BOOL isAnimating; 20 | @property (nonatomic, assign) UIInterfaceOrientation orientation; 21 | 22 | @end 23 | 24 | @implementation MPPlayerController 25 | 26 | // MARK: - Init 27 | + (instancetype)playrWithContainerView:(UIView *)containerView 28 | { 29 | return [[self alloc] initWithContainerView: containerView]; 30 | } 31 | 32 | - (instancetype)initWithContainerView:(UIView *)containerView 33 | { 34 | if (self = [super init]) 35 | { 36 | MPlayerAttributeManager *mgr = [[MPlayerAttributeManager alloc] init]; 37 | _player = [[ZFPlayerController alloc] initWithPlayerManager:mgr containerView:containerView]; 38 | [_player setCustomAudioSession:YES]; 39 | [self setup]; 40 | } 41 | return self; 42 | } 43 | 44 | + (instancetype)playerWithScrollView:(UIScrollView *)scrollView containerViewTag:(NSInteger)containerViewTag 45 | { 46 | return [[self alloc] initWithScrollView:scrollView containerViewTag:containerViewTag]; 47 | } 48 | 49 | - (instancetype)initWithScrollView:(UIScrollView *)scrollView containerViewTag:(NSInteger)containerViewTag 50 | { 51 | if (self = [super init]) 52 | { 53 | MPlayerAttributeManager *mgr = [[MPlayerAttributeManager alloc] init]; 54 | _player = [[ZFPlayerController alloc] initWithScrollView:scrollView playerManager:mgr containerViewTag:containerViewTag]; 55 | _player.disableGestureTypes = ZFPlayerDisableGestureTypesPan; 56 | [_player setCustomAudioSession:YES]; 57 | [self setup]; 58 | } 59 | return self; 60 | } 61 | 62 | - (instancetype)init 63 | { 64 | if (self = [super init]) 65 | { 66 | [self setup]; 67 | } 68 | return self; 69 | } 70 | 71 | /// 初始化 72 | - (void)setup 73 | { 74 | _preLoadNum = 2; 75 | _nextLoadNum = 2; 76 | _preloadPrecent = 0.1; 77 | _initPreloadNum = 3; 78 | _player.allowOrentitaionRotation = NO; 79 | _player.playerDisapperaPercent = 0.5; 80 | _player.playerApperaPercent = 0.5; 81 | @weakify(self) 82 | _player.playerDidToEnd = ^(id _Nonnull asset) { 83 | @strongify(self) 84 | [self.player.currentPlayerManager replay]; 85 | }; 86 | } 87 | 88 | // MARK: - Method 89 | - (void)stop 90 | { 91 | self.player.scrollView.zf_playingIndexPath = nil; 92 | [self.player stop]; 93 | } 94 | 95 | - (void)stopCurrentPlayingCell 96 | { 97 | [self.player stopCurrentPlayingCell]; 98 | } 99 | 100 | - (void)playTheIndexPath:(NSIndexPath *)indexPath playable: (id)playable 101 | { 102 | // 播放前,先停止所有的预加载任务 103 | [self cancelAllPreload]; 104 | _currentPlayable = playable; 105 | [self.player playTheIndexPath:indexPath assetURL:[NSURL URLWithString:playable.video_url] scrollToTop:NO]; 106 | __weak typeof(self) weakSelf = self; 107 | self.playerReadyToPlay = ^(id _Nonnull asset, NSURL * _Nonnull assetURL) { 108 | [weakSelf preload: playable]; 109 | }; 110 | } 111 | 112 | - (void)playWithPlayable: (id)playable 113 | { 114 | _currentPlayable = playable; 115 | self.player.assetURL = [NSURL URLWithString:playable.video_url]; 116 | } 117 | 118 | - (void)setDisapperaPercent: (CGFloat)disappearPercent appearPercent: (CGFloat)appearPercent 119 | { 120 | self.player.playerDisapperaPercent = disappearPercent; 121 | self.player.playerApperaPercent = appearPercent; 122 | } 123 | 124 | - (void)enterLandscapeFullScreen:(UIInterfaceOrientation)orientation animated:(BOOL)animated 125 | { 126 | CGFloat cellHeight = iPhoneX ? ZFPlayer_ScreenHeight - 83 : ZFPlayer_ScreenHeight; 127 | if (self.isAnimating) { 128 | return; 129 | } 130 | if (self.orientation == orientation) { 131 | return; 132 | } 133 | if (self.player.currentPlayerManager.playState == ZFPlayerPlayStatePlaying ||self.player.currentPlayerManager.playState == ZFPlayerPlayStatePaused 134 | ) { 135 | self.isAnimating = YES; 136 | } 137 | self.orientation = orientation; 138 | CGFloat rotation = 0; 139 | if (orientation == UIInterfaceOrientationLandscapeLeft) { 140 | rotation = M_PI_2; 141 | }else if (orientation == UIInterfaceOrientationLandscapeRight) { 142 | rotation = M_PI_2 * 3; 143 | } 144 | UIView *presentView = self.player.currentPlayerManager.view; 145 | CGRect landRect = CGRectMake(0, 0, cellHeight, ZFPlayer_ScreenWidth); 146 | [UIView animateWithDuration:0.35 animations:^{ 147 | presentView.layer.affineTransform = CGAffineTransformMakeRotation(rotation); 148 | } completion:^(BOOL finished) { 149 | self.isAnimating = NO; 150 | }]; 151 | presentView.layer.bounds = landRect; 152 | } 153 | 154 | - (void)exitFullScreen: (BOOL)isAnimated 155 | { 156 | CGFloat cellHeight = iPhoneX ? ZFPlayer_ScreenHeight - 49 : ZFPlayer_ScreenHeight; 157 | if (self.isAnimating) { 158 | return; 159 | } 160 | if (self.orientation == UIInterfaceOrientationPortrait) { 161 | return; 162 | } 163 | CGRect frame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, cellHeight); 164 | UIView *presentView = self.player.currentPlayerManager.view; 165 | if (!isAnimated) { 166 | presentView.layer.affineTransform = CGAffineTransformIdentity; 167 | self.orientation = UIInterfaceOrientationPortrait; 168 | }else { 169 | self.isAnimating = YES; 170 | self.orientation = UIInterfaceOrientationPortrait; 171 | [UIView animateWithDuration:0.35 animations:^{ 172 | presentView.layer.affineTransform = CGAffineTransformIdentity; 173 | }completion:^(BOOL finish){ 174 | self.isAnimating = NO; 175 | }]; 176 | } 177 | self.player.currentPlayerManager.view.layer.bounds = frame; 178 | } 179 | 180 | /// Add Player到Cell上 181 | - (void)updateScrollViewPlayerToCell 182 | { 183 | if (self.player.currentPlayerManager.view && 184 | self.player.scrollView.zf_playingIndexPath && 185 | self.player.containerViewTag) { 186 | UIView *cell = [self.player.scrollView zf_getCellForIndexPath:self.player.scrollView.zf_playingIndexPath]; 187 | UIView *containerView = [cell viewWithTag:self.player.containerViewTag]; 188 | [self updateNoramlPlayerWithContainerView:containerView]; 189 | } 190 | } 191 | 192 | /// 更新Playerc的容器 193 | - (void)updateNoramlPlayerWithContainerView:(UIView *)containerView 194 | { 195 | [self.player addPlayerViewToContainerView:containerView]; 196 | } 197 | 198 | // MARK: - Preload 199 | /// 根据传入的模型,预加载上几个,下几个的视频 200 | - (void)preload: (id)resource 201 | { 202 | if (self.playableArray.count <= 1) 203 | return; 204 | if (_nextLoadNum == 0 && _preLoadNum == 0) 205 | return; 206 | NSInteger start = [self.playableArray indexOfObject:resource]; 207 | if (start == NSNotFound) 208 | return; 209 | [self cancelAllPreload]; 210 | NSInteger index = 0; 211 | for (NSInteger i = start + 1; i < self.playableArray.count && index < _nextLoadNum; i++) 212 | { 213 | index += 1; 214 | id model = self.playableArray[i]; 215 | MPPreLoaderModel *preModel = [self getPreloadModel: model.video_url]; 216 | if (preModel) { 217 | @synchronized (self.preloadArr) { 218 | [self.preloadArr addObject: preModel]; 219 | } 220 | } 221 | } 222 | index = 0; 223 | for (NSInteger i = start - 1; i >= 0 && index < _preLoadNum; i--) 224 | { 225 | index += 1; 226 | id model = self.playableArray[i]; 227 | MPPreLoaderModel *preModel = [self getPreloadModel: model.video_url]; 228 | if (preModel) { 229 | @synchronized (self.preloadArr) { 230 | [self.preloadArr addObject:preModel]; 231 | } 232 | } 233 | } 234 | [self processLoader]; 235 | } 236 | 237 | /// 取消所有的预加载 238 | - (void)cancelAllPreload 239 | { 240 | @synchronized (self.preloadArr) { 241 | if (self.preloadArr.count == 0) 242 | { 243 | return; 244 | } 245 | [self.preloadArr enumerateObjectsUsingBlock:^(MPPreLoaderModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 246 | [obj.loader close]; 247 | }]; 248 | [self.preloadArr removeAllObjects]; 249 | } 250 | } 251 | 252 | - (MPPreLoaderModel *)getPreloadModel: (NSString *)urlStr 253 | { 254 | if (!urlStr) 255 | return nil; 256 | // 判断是否已在队列中 257 | __block Boolean res = NO; 258 | @synchronized (self.preloadArr) { 259 | [self.preloadArr enumerateObjectsUsingBlock:^(MPPreLoaderModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 260 | if ([obj.url isEqualToString:urlStr]) 261 | { 262 | res = YES; 263 | *stop = YES; 264 | } 265 | }]; 266 | } 267 | if (res) 268 | return nil; 269 | NSURL *proxyUrl = [KTVHTTPCache proxyURLWithOriginalURL: [NSURL URLWithString:urlStr]]; 270 | KTVHCDataCacheItem *item = [KTVHTTPCache cacheCacheItemWithURL:proxyUrl]; 271 | double cachePrecent = 1.0 * item.cacheLength / item.totalLength; 272 | // 判断缓存已经超过10%了 273 | if (cachePrecent >= self.preloadPrecent) 274 | return nil; 275 | KTVHCDataRequest *req = [[KTVHCDataRequest alloc] initWithURL:proxyUrl headers:[NSDictionary dictionary]]; 276 | KTVHCDataLoader *loader = [KTVHTTPCache cacheLoaderWithRequest:req]; 277 | MPPreLoaderModel *preModel = [[MPPreLoaderModel alloc] initWithURL:urlStr loader:loader]; 278 | return preModel; 279 | } 280 | 281 | - (void)processLoader 282 | { 283 | @synchronized (self.preloadArr) { 284 | if (self.preloadArr.count == 0) 285 | return; 286 | MPPreLoaderModel *model = self.preloadArr.firstObject; 287 | model.loader.delegate = self; 288 | [model.loader prepare]; 289 | } 290 | } 291 | 292 | /// 根据loader,移除预加载任务 293 | - (void)removePreloadTask: (KTVHCDataLoader *)loader 294 | { 295 | @synchronized (self.preloadArr) { 296 | MPPreLoaderModel *target = nil; 297 | for (MPPreLoaderModel *model in self.preloadArr) { 298 | if ([model.loader isEqual:loader]) 299 | { 300 | target = model; 301 | break; 302 | } 303 | } 304 | if (target) 305 | [self.preloadArr removeObject:target]; 306 | } 307 | } 308 | 309 | // MARK: - KTVHCDataLoaderDelegate 310 | - (void)ktv_loaderDidFinish:(KTVHCDataLoader *)loader 311 | { 312 | } 313 | - (void)ktv_loader:(KTVHCDataLoader *)loader didFailWithError:(NSError *)error 314 | { 315 | // 若预加载失败的话,就直接移除任务,开始下一个预加载任务 316 | [self removePreloadTask:loader]; 317 | [self processLoader]; 318 | } 319 | - (void)ktv_loader:(KTVHCDataLoader *)loader didChangeProgress:(double)progress 320 | { 321 | if (progress >= self.preloadPrecent) 322 | { 323 | [loader close]; 324 | [self removePreloadTask:loader]; 325 | [self processLoader]; 326 | } 327 | } 328 | 329 | // MARK: - Getter 330 | - (BOOL)isViewControllerDisappear 331 | { 332 | return self.player.isViewControllerDisappear; 333 | } 334 | 335 | - (NSIndexPath *)playingIndexPath 336 | { 337 | return self.player.playingIndexPath; 338 | } 339 | 340 | - (NSMutableArray *)preloadArr 341 | { 342 | if (_preloadArr == nil) 343 | { 344 | _preloadArr = [NSMutableArray array]; 345 | } 346 | return _preloadArr; 347 | } 348 | 349 | - (id)currentPlayerManager 350 | { 351 | return self.player.currentPlayerManager; 352 | } 353 | 354 | - (BOOL)isPlaying 355 | { 356 | return self.player.currentPlayerManager.isPlaying; 357 | } 358 | 359 | - (UIView *)containerView 360 | { 361 | return self.player.containerView; 362 | } 363 | 364 | - (BOOL)isWWANAutoPlay 365 | { 366 | return self.player.isWWANAutoPlay; 367 | } 368 | 369 | - (void (^)(id _Nonnull, NSTimeInterval, NSTimeInterval))playerPlayTimeChanged 370 | { 371 | return _player.playerPlayTimeChanged; 372 | } 373 | 374 | - (CGFloat)playerApperaPercent 375 | { 376 | return _player.playerApperaPercent; 377 | } 378 | 379 | - (CGFloat)playerDisapperaPercent 380 | { 381 | return _player.playerDisapperaPercent; 382 | } 383 | 384 | 385 | // MARK: - Setter 386 | - (void)setPlayableArray:(NSArray> *)playableArray 387 | { 388 | _playableArray = playableArray; 389 | [self cancelAllPreload]; 390 | // 默认预加载前几条数据 391 | NSRange range = NSMakeRange(0, _initPreloadNum); 392 | if (range.length > playableArray.count) { 393 | range.length = playableArray.count; 394 | } 395 | NSArray *subArr = [playableArray subarrayWithRange: range]; 396 | for (id model in subArr) 397 | { 398 | MPPreLoaderModel *preload = [self getPreloadModel:model.video_url]; 399 | if (preload) { 400 | @synchronized (self.preloadArr) { 401 | [self.preloadArr addObject: preload]; 402 | } 403 | } 404 | } 405 | [self processLoader]; 406 | } 407 | 408 | - (void)setWWANAutoPlay:(BOOL)WWANAutoPlay 409 | { 410 | self.player.WWANAutoPlay = WWANAutoPlay; 411 | } 412 | 413 | - (void)setControlView:(UIView *)controlView 414 | { 415 | _controlView = controlView; 416 | self.player.controlView = controlView; 417 | } 418 | 419 | - (void)setViewControllerDisappear:(BOOL)viewControllerDisappear 420 | { 421 | self.player.viewControllerDisappear = viewControllerDisappear; 422 | } 423 | 424 | - (void)setPlayingIndexPath:(NSIndexPath * _Nullable)playingIndexPath 425 | { 426 | self.playingIndexPath = playingIndexPath; 427 | } 428 | 429 | - (void)setPlayerDidToEnd:(void (^)(id _Nonnull))playerDidToEnd { 430 | _player.playerDidToEnd = ^(id _Nonnull asset) { 431 | playerDidToEnd(asset); 432 | }; 433 | } 434 | 435 | - (void)setPlayerPlayFailed:(void (^)(id _Nonnull, id _Nonnull))playerPlayFailed { 436 | _player.playerPlayFailed = playerPlayFailed; 437 | } 438 | 439 | - (void)setPlayerReadyToPlay:(void (^)(id _Nonnull, NSURL * _Nonnull))playerReadyToPlay { 440 | _player.playerReadyToPlay = ^(id _Nonnull asset, NSURL * _Nonnull assetURL) { 441 | playerReadyToPlay(asset, assetURL); 442 | }; 443 | } 444 | 445 | - (void)setPlayerPlayTimeChanged:(void (^)(id _Nonnull, NSTimeInterval, NSTimeInterval))playerPlayTimeChanged { 446 | _player.playerPlayTimeChanged = playerPlayTimeChanged; 447 | } 448 | 449 | - (void)setPlayerBufferTimeChanged:(void (^)(id _Nonnull, NSTimeInterval))playerBufferTimeChanged { 450 | _player.playerBufferTimeChanged = playerBufferTimeChanged; 451 | } 452 | 453 | - (void)setPresentationSizeChanged:(void (^)(id _Nonnull, CGSize))presentationSizeChanged { 454 | _player.presentationSizeChanged = presentationSizeChanged; 455 | } 456 | 457 | - (void)setPlayerPlayStateChanged:(void (^)(id _Nonnull, ZFPlayerPlaybackState))playerPlayStateChanged 458 | { 459 | _player.playerPlayStateChanged = playerPlayStateChanged; 460 | } 461 | 462 | - (void)setPlayerLoadStateChanged:(void (^)(id _Nonnull, ZFPlayerLoadState))playerLoadStateChanged 463 | { 464 | _player.playerLoadStateChanged = playerLoadStateChanged; 465 | } 466 | 467 | - (void)setZf_playerDisappearingInScrollView:(void (^)(NSIndexPath * _Nonnull, CGFloat))zf_playerDisappearingInScrollView 468 | { 469 | _player.zf_playerDisappearingInScrollView = zf_playerDisappearingInScrollView; 470 | } 471 | 472 | - (void)setZf_playerDidDisappearInScrollView:(void (^)(NSIndexPath * _Nonnull))zf_playerDidDisappearInScrollView 473 | { 474 | _player.zf_playerDidDisappearInScrollView = zf_playerDidDisappearInScrollView; 475 | } 476 | 477 | - (void)setPlayerApperaPercent:(CGFloat)playerApperaPercent 478 | { 479 | _player.playerApperaPercent = playerApperaPercent; 480 | } 481 | 482 | - (void)setPlayerDisapperaPercent:(CGFloat)playerDisapperaPercent 483 | { 484 | _player.playerDisapperaPercent = playerDisapperaPercent; 485 | } 486 | 487 | @end 488 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPPreLoaderModel.h: -------------------------------------------------------------------------------- 1 | // 2 | // PreLoaderModel.h 3 | // ListDemo 4 | // 5 | // Created by Beauty-ruanjian on 2019/4/17. 6 | // Copyright © 2019 Beauty-ruanjian. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | /// 预加载模型 14 | @interface MPPreLoaderModel : NSObject 15 | 16 | /// 加载的URL 17 | @property (nonatomic, copy, readonly) NSString *url; 18 | /// 请求URL的Loader 19 | @property (nonatomic, strong, readonly) KTVHCDataLoader *loader; 20 | 21 | - (instancetype)initWithURL: (NSString *)url loader: (KTVHCDataLoader *)loader; 22 | 23 | @end 24 | 25 | NS_ASSUME_NONNULL_END 26 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPPreLoaderModel.m: -------------------------------------------------------------------------------- 1 | // 2 | // PreLoaderModel.m 3 | // ListDemo 4 | // 5 | // Created by Beauty-ruanjian on 2019/4/17. 6 | // Copyright © 2019 Beauty-ruanjian. All rights reserved. 7 | // 8 | 9 | #import "MPPreLoaderModel.h" 10 | 11 | @implementation MPPreLoaderModel 12 | 13 | - (instancetype)initWithURL: (NSString *)url loader: (KTVHCDataLoader *)loader 14 | { 15 | if (self = [super init]) 16 | { 17 | _url = url; 18 | _loader = loader; 19 | } 20 | return self; 21 | } 22 | 23 | 24 | @end 25 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPlayerAttributeManager.h: -------------------------------------------------------------------------------- 1 | 2 | 3 | #import 4 | #import 5 | 6 | NS_ASSUME_NONNULL_BEGIN 7 | 8 | /// 视频属性,视频资源等管理 9 | @interface MPlayerAttributeManager : NSObject 10 | 11 | @property (nonatomic) BOOL shouldAutoPlay; 12 | 13 | @end 14 | 15 | NS_ASSUME_NONNULL_END 16 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/MPlayerAttributeManager.m: -------------------------------------------------------------------------------- 1 | 2 | #import "MPlayerAttributeManager.h" 3 | #import 4 | #import 5 | #import 6 | #import 7 | 8 | /*! 9 | * Refresh interval for timed observations of AVPlayer 10 | */ 11 | static float const kTimeRefreshInterval = 0.1; 12 | static NSString *const kStatus = @"status"; 13 | static NSString *const kLoadedTimeRanges = @"loadedTimeRanges"; 14 | static NSString *const kPlaybackBufferEmpty = @"playbackBufferEmpty"; 15 | static NSString *const kPlaybackLikelyToKeepUp = @"playbackLikelyToKeepUp"; 16 | static NSString *const kPresentationSize = @"presentationSize"; 17 | 18 | @interface ZFPlayerPresentView : ZFPlayerView 19 | 20 | @property (nonatomic, strong) AVPlayer *player; 21 | /// default is AVLayerVideoGravityResizeAspect. 22 | @property (nonatomic, strong) AVLayerVideoGravity videoGravity; 23 | 24 | @end 25 | 26 | @implementation ZFPlayerPresentView 27 | 28 | + (Class)layerClass { 29 | return [AVPlayerLayer class]; 30 | } 31 | 32 | - (AVPlayerLayer *)avLayer { 33 | return (AVPlayerLayer *)self.layer; 34 | } 35 | 36 | - (instancetype)initWithFrame:(CGRect)frame { 37 | self = [super initWithFrame:frame]; 38 | if (self) { 39 | self.backgroundColor = [UIColor blackColor]; 40 | } 41 | return self; 42 | } 43 | 44 | - (void)setPlayer:(AVPlayer *)player { 45 | if (player == _player) return; 46 | self.avLayer.player = player; 47 | } 48 | 49 | - (void)setVideoGravity:(AVLayerVideoGravity)videoGravity { 50 | if (videoGravity == self.videoGravity) return; 51 | [self avLayer].videoGravity = videoGravity; 52 | } 53 | 54 | - (AVLayerVideoGravity)videoGravity { 55 | return [self avLayer].videoGravity; 56 | } 57 | 58 | @end 59 | 60 | @interface MPlayerAttributeManager () { 61 | id _timeObserver; 62 | id _itemEndObserver; 63 | ZFKVOController *_playerItemKVO; 64 | } 65 | 66 | @property (nonatomic, strong, readonly) AVURLAsset *asset; 67 | @property (nonatomic, strong, readonly) AVPlayerItem *playerItem; 68 | @property (nonatomic, strong, readonly) AVPlayer *player; 69 | @property (nonatomic, strong) AVPlayerLayer *playerLayer; 70 | @property (nonatomic, assign) BOOL isBuffering; 71 | @property (nonatomic, assign) BOOL isReadyToPlay; 72 | @property (nonatomic, copy) void(^muteStateBlock)(void); 73 | /// 记录每个url的加载失败次数的字典,最多重试3次 74 | @property (nonatomic, strong) NSMutableDictionary *retryDic; 75 | /// 记录可以播放的视频后缀 76 | @property (nonatomic, copy) NSArray *playableSuffixArray; 77 | 78 | @end 79 | 80 | @implementation MPlayerAttributeManager 81 | 82 | @synthesize view = _view; 83 | @synthesize currentTime = _currentTime; 84 | @synthesize totalTime = _totalTime; 85 | @synthesize playerPlayTimeChanged = _playerPlayTimeChanged; 86 | @synthesize playerBufferTimeChanged = _playerBufferTimeChanged; 87 | @synthesize playerDidToEnd = _playerDidToEnd; 88 | @synthesize bufferTime = _bufferTime; 89 | @synthesize playState = _playState; 90 | @synthesize loadState = _loadState; 91 | @synthesize assetURL = _assetURL; 92 | @synthesize playerPrepareToPlay = _playerPrepareToPlay; 93 | @synthesize playerReadyToPlay = _playerReadyToPlay; 94 | @synthesize playerPlayStateChanged = _playerPlayStateChanged; 95 | @synthesize playerLoadStateChanged = _playerLoadStateChanged; 96 | @synthesize seekTime = _seekTime; 97 | @synthesize muted = _muted; 98 | @synthesize volume = _volume; 99 | @synthesize presentationSize = _presentationSize; 100 | @synthesize isPlaying = _isPlaying; 101 | @synthesize rate = _rate; 102 | @synthesize isPreparedToPlay = _isPreparedToPlay; 103 | @synthesize scalingMode = _scalingMode; 104 | @synthesize playerPlayFailed = _playerPlayFailed; 105 | @synthesize presentationSizeChanged = _presentationSizeChanged; 106 | 107 | + (void)initialize 108 | { 109 | [KTVHTTPCache logSetConsoleLogEnable:NO]; 110 | NSError *error = nil; 111 | [KTVHTTPCache proxyStart:&error]; 112 | if (error) { 113 | NSLog(@"Proxy Start Failure, %@", error); 114 | } 115 | [KTVHTTPCache encodeSetURLConverter:^NSURL *(NSURL *URL) { 116 | // NSLog(@"URL Filter reviced URL : %@", URL); 117 | return URL; 118 | }]; 119 | [KTVHTTPCache downloadSetUnacceptableContentTypeDisposer:^BOOL(NSURL *URL, NSString *contentType) { 120 | return NO; 121 | }]; 122 | // 设置缓存最大容量 123 | [KTVHTTPCache cacheSetMaxCacheLength:1024 * 1024 * 1024]; 124 | } 125 | 126 | - (instancetype)init { 127 | self = [super init]; 128 | if (self) { 129 | _scalingMode = ZFPlayerScalingModeAspectFit; 130 | self.retryDic = [NSMutableDictionary dictionary]; 131 | } 132 | return self; 133 | } 134 | 135 | - (void)setupMuteStateBlock:(void (^)(void))muteStateBlock 136 | { 137 | self.muteStateBlock = muteStateBlock; 138 | } 139 | 140 | - (void)removeMuteStateBlock 141 | { 142 | self.muteStateBlock = nil; 143 | } 144 | 145 | - (void)prepareToPlay { 146 | if (!_assetURL) return; 147 | _isPreparedToPlay = YES; 148 | [self initializePlayer]; 149 | [self play]; 150 | self.loadState = ZFPlayerLoadStatePrepare; 151 | if (_playerPrepareToPlay) _playerPrepareToPlay(self, self.assetURL); 152 | } 153 | 154 | - (void)reloadPlayer { 155 | self.seekTime = self.currentTime; 156 | [self prepareToPlay]; 157 | } 158 | 159 | - (void)play { 160 | if (!_isPreparedToPlay) { 161 | [self prepareToPlay]; 162 | } else { 163 | if (self.muteStateBlock) { 164 | self.muteStateBlock(); 165 | } 166 | [self.player play]; 167 | self.player.rate = self.rate; 168 | self->_isPlaying = YES; 169 | self.playState = ZFPlayerPlayStatePlaying; 170 | } 171 | } 172 | 173 | - (void)pause { 174 | [self.player pause]; 175 | self->_isPlaying = NO; 176 | self.playState = ZFPlayerPlayStatePaused; 177 | [_playerItem cancelPendingSeeks]; 178 | [_asset cancelLoading]; 179 | } 180 | 181 | - (void)stop { 182 | [_playerItemKVO safelyRemoveAllObservers]; 183 | self.loadState = ZFPlayerLoadStateUnknown; 184 | if (self.player.rate != 0) [self.player pause]; 185 | [self.player removeTimeObserver:_timeObserver]; 186 | [self.player replaceCurrentItemWithPlayerItem:nil]; 187 | _timeObserver = nil; 188 | [[NSNotificationCenter defaultCenter] removeObserver:_itemEndObserver name:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem]; 189 | _itemEndObserver = nil; 190 | _isPlaying = NO; 191 | _player = nil; 192 | _assetURL = nil; 193 | _playerItem = nil; 194 | _isPreparedToPlay = NO; 195 | self->_currentTime = 0; 196 | self->_totalTime = 0; 197 | self->_bufferTime = 0; 198 | self.isReadyToPlay = NO; 199 | self.playState = ZFPlayerPlayStatePlayStopped; 200 | } 201 | 202 | - (void)replay { 203 | @weakify(self) 204 | [self seekToTime:0 completionHandler:^(BOOL finished) { 205 | @strongify(self) 206 | [self play]; 207 | }]; 208 | } 209 | 210 | /// Replace the current playback address 211 | - (void)replaceCurrentAssetURL:(NSURL *)assetURL { 212 | self.assetURL = assetURL; 213 | } 214 | 215 | - (void)seekToTime:(NSTimeInterval)time completionHandler:(void (^ __nullable)(BOOL finished))completionHandler { 216 | CMTime seekTime = CMTimeMake(time, 1); 217 | [_player seekToTime:seekTime toleranceBefore:kCMTimeZero toleranceAfter:kCMTimeZero completionHandler:completionHandler]; 218 | } 219 | 220 | - (UIImage *)thumbnailImageAtCurrentTime { 221 | AVAssetImageGenerator *imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:_asset]; 222 | CMTime expectedTime = _playerItem.currentTime; 223 | CGImageRef cgImage = NULL; 224 | 225 | imageGenerator.requestedTimeToleranceBefore = kCMTimeZero; 226 | imageGenerator.requestedTimeToleranceAfter = kCMTimeZero; 227 | cgImage = [imageGenerator copyCGImageAtTime:expectedTime actualTime:NULL error:NULL]; 228 | 229 | if (!cgImage) { 230 | imageGenerator.requestedTimeToleranceBefore = kCMTimePositiveInfinity; 231 | imageGenerator.requestedTimeToleranceAfter = kCMTimePositiveInfinity; 232 | cgImage = [imageGenerator copyCGImageAtTime:expectedTime actualTime:NULL error:NULL]; 233 | } 234 | 235 | UIImage *image = [UIImage imageWithCGImage:cgImage]; 236 | return image; 237 | } 238 | 239 | - (BOOL)isPlayableSuffix: (NSString *)suffix 240 | { 241 | if (!suffix || suffix.length == 0) 242 | return NO; 243 | suffix = [suffix uppercaseString]; 244 | return [self.playableSuffixArray containsObject:suffix]; 245 | } 246 | 247 | #pragma mark - private method 248 | 249 | /// Calculate buffer progress 250 | - (NSTimeInterval)availableDuration { 251 | NSArray *timeRangeArray = _playerItem.loadedTimeRanges; 252 | CMTime currentTime = [_player currentTime]; 253 | BOOL foundRange = NO; 254 | CMTimeRange aTimeRange = {0}; 255 | if (timeRangeArray.count) { 256 | aTimeRange = [[timeRangeArray objectAtIndex:0] CMTimeRangeValue]; 257 | if (CMTimeRangeContainsTime(aTimeRange, currentTime)) { 258 | foundRange = YES; 259 | } 260 | } 261 | 262 | if (foundRange) { 263 | CMTime maxTime = CMTimeRangeGetEnd(aTimeRange); 264 | NSTimeInterval playableDuration = CMTimeGetSeconds(maxTime); 265 | if (playableDuration > 0) { 266 | return playableDuration; 267 | } 268 | } 269 | return 0; 270 | } 271 | 272 | - (void)initializePlayer { 273 | _asset = [AVURLAsset assetWithURL:self.assetURL]; 274 | _playerItem = [AVPlayerItem playerItemWithAsset:self.asset]; 275 | _player = [AVPlayer playerWithPlayerItem:_playerItem]; 276 | [self enableAudioTracks:YES inPlayerItem:_playerItem]; 277 | 278 | ZFPlayerPresentView *presentView = (ZFPlayerPresentView *)self.view; 279 | presentView.player = _player; 280 | self.scalingMode = _scalingMode; 281 | if (@available(iOS 9.0, *)) { 282 | _playerItem.canUseNetworkResourcesForLiveStreamingWhilePaused = NO; 283 | } 284 | if (@available(iOS 10.0, *)) { 285 | _playerItem.preferredForwardBufferDuration = 5; 286 | _player.automaticallyWaitsToMinimizeStalling = NO; 287 | } 288 | [self itemObserving]; 289 | } 290 | 291 | /// Playback speed switching method 292 | - (void)enableAudioTracks:(BOOL)enable inPlayerItem:(AVPlayerItem*)playerItem { 293 | for (AVPlayerItemTrack *track in playerItem.tracks){ 294 | if ([track.assetTrack.mediaType isEqual:AVMediaTypeVideo]) { 295 | track.enabled = enable; 296 | } 297 | } 298 | } 299 | 300 | /** 301 | * 缓冲较差时候回调这里 302 | */ 303 | - (void)bufferingSomeSecond { 304 | // playbackBufferEmpty会反复进入,因此在bufferingOneSecond延时播放执行完之前再调用bufferingSomeSecond都忽略 305 | if (self.isBuffering || self.playState == ZFPlayerPlayStatePlayStopped) return; 306 | self.isBuffering = YES; 307 | 308 | // 需要先暂停一小会之后再播放,否则网络状况不好的时候时间在走,声音播放不出来 309 | [self.player pause]; 310 | dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ 311 | // 如果此时用户已经暂停了,则不再需要开启播放了 312 | if (!self.isPlaying) { 313 | self.isBuffering = NO; 314 | return; 315 | } 316 | [self play]; 317 | // 如果执行了play还是没有播放则说明还没有缓存好,则再次缓存一段时间 318 | self.isBuffering = NO; 319 | if (!self.playerItem.isPlaybackLikelyToKeepUp) [self bufferingSomeSecond]; 320 | }); 321 | } 322 | 323 | - (void)itemObserving { 324 | [_playerItemKVO safelyRemoveAllObservers]; 325 | _playerItemKVO = [[ZFKVOController alloc] initWithTarget:_playerItem]; 326 | [_playerItemKVO safelyAddObserver:self 327 | forKeyPath:kStatus 328 | options:NSKeyValueObservingOptionNew 329 | context:nil]; 330 | [_playerItemKVO safelyAddObserver:self 331 | forKeyPath:kPlaybackBufferEmpty 332 | options:NSKeyValueObservingOptionNew 333 | context:nil]; 334 | [_playerItemKVO safelyAddObserver:self 335 | forKeyPath:kPlaybackLikelyToKeepUp 336 | options:NSKeyValueObservingOptionNew 337 | context:nil]; 338 | [_playerItemKVO safelyAddObserver:self 339 | forKeyPath:kLoadedTimeRanges 340 | options:NSKeyValueObservingOptionNew 341 | context:nil]; 342 | [_playerItemKVO safelyAddObserver:self 343 | forKeyPath:kPresentationSize 344 | options:NSKeyValueObservingOptionNew 345 | context:nil]; 346 | 347 | CMTime interval = CMTimeMakeWithSeconds(kTimeRefreshInterval, NSEC_PER_SEC); 348 | @weakify(self) 349 | _timeObserver = [self.player addPeriodicTimeObserverForInterval:interval queue:dispatch_get_main_queue() usingBlock:^(CMTime time) { 350 | @strongify(self) 351 | if (!self) return; 352 | NSArray *loadedRanges = self.playerItem.seekableTimeRanges; 353 | /// 大于0才把状态改为可以播放,解决黑屏问题 354 | if (CMTimeGetSeconds(time) > 0 && !self.isReadyToPlay) { 355 | self.isReadyToPlay = YES; 356 | self.loadState = ZFPlayerLoadStatePlaythroughOK; 357 | } 358 | if (loadedRanges.count > 0) { 359 | if (self.playerPlayTimeChanged) self.playerPlayTimeChanged(self, self.currentTime, self.totalTime); 360 | } 361 | }]; 362 | 363 | _itemEndObserver = [[NSNotificationCenter defaultCenter] addObserverForName:AVPlayerItemDidPlayToEndTimeNotification object:self.playerItem queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) { 364 | @strongify(self) 365 | if (!self) return; 366 | self.playState = ZFPlayerPlayStatePlayStopped; 367 | if (self.playerDidToEnd) self.playerDidToEnd(self); 368 | }]; 369 | } 370 | 371 | - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { 372 | dispatch_async(dispatch_get_main_queue(), ^{ 373 | if ([keyPath isEqualToString:kStatus]) { 374 | if (self.player.currentItem.status == AVPlayerItemStatusReadyToPlay) { 375 | /// 第一次初始化 376 | if (self.loadState == ZFPlayerLoadStatePrepare) { 377 | if (self.playerPrepareToPlay) self.playerReadyToPlay(self, self.assetURL); 378 | } 379 | if (self.seekTime) { 380 | [self seekToTime:self.seekTime completionHandler:nil]; 381 | self.seekTime = 0; 382 | } 383 | if (self.isPlaying) [self play]; 384 | self.player.muted = self.muted; 385 | NSArray *loadedRanges = self.playerItem.seekableTimeRanges; 386 | if (loadedRanges.count > 0) { 387 | /// Fix https://github.com/renzifeng/ZFPlayer/issues/475 388 | if (self.playerPlayTimeChanged) self.playerPlayTimeChanged(self, self.currentTime, self.totalTime); 389 | } 390 | } else if (self.player.currentItem.status == AVPlayerItemStatusFailed) { 391 | NSUInteger retryCount = 1; 392 | if (!self.retryDic[self.assetURL.absoluteString]) { 393 | self.retryDic[self.assetURL.absoluteString] = @(retryCount); 394 | }else { 395 | retryCount = [self.retryDic[self.assetURL.absoluteString] intValue]; 396 | if (retryCount <= 3) { 397 | retryCount += 1; 398 | self.retryDic[self.assetURL.absoluteString] = @(retryCount); 399 | } 400 | } 401 | if (retryCount <= 3) { 402 | [self reloadPlayer]; 403 | } 404 | self.playState = ZFPlayerPlayStatePlayFailed; 405 | NSError *error = self.player.currentItem.error; 406 | if (self.playerPlayFailed) self.playerPlayFailed(self, error); 407 | } 408 | } else if ([keyPath isEqualToString:kPlaybackBufferEmpty]) { 409 | // When the buffer is empty 410 | if (self.playerItem.playbackBufferEmpty) { 411 | self.loadState = ZFPlayerLoadStateStalled; 412 | [self bufferingSomeSecond]; 413 | } 414 | } else if ([keyPath isEqualToString:kPlaybackLikelyToKeepUp]) { 415 | // When the buffer is good 416 | if (self.playerItem.playbackLikelyToKeepUp) { 417 | self.loadState = ZFPlayerLoadStatePlayable; 418 | if (self.isPlaying) [self play]; 419 | } 420 | } else if ([keyPath isEqualToString:kLoadedTimeRanges]) { 421 | NSTimeInterval bufferTime = [self availableDuration]; 422 | self->_bufferTime = bufferTime; 423 | if (self.playerBufferTimeChanged) self.playerBufferTimeChanged(self, bufferTime); 424 | } else if ([keyPath isEqualToString:kPresentationSize]) { 425 | self->_presentationSize = self.playerItem.presentationSize; 426 | if (self.presentationSizeChanged) { 427 | self.presentationSizeChanged(self, self->_presentationSize); 428 | } 429 | } else { 430 | [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; 431 | } 432 | }); 433 | } 434 | 435 | #pragma mark - getter 436 | 437 | - (UIView *)view { 438 | if (!_view) { 439 | _view = [[ZFPlayerPresentView alloc] init]; 440 | } 441 | return _view; 442 | } 443 | 444 | - (float)rate { 445 | return _rate == 0 ?1:_rate; 446 | } 447 | 448 | - (NSTimeInterval)totalTime { 449 | NSTimeInterval sec = CMTimeGetSeconds(self.player.currentItem.duration); 450 | if (isnan(sec)) { 451 | return 0; 452 | } 453 | return sec; 454 | } 455 | 456 | - (NSTimeInterval)currentTime { 457 | NSTimeInterval sec = CMTimeGetSeconds(self.playerItem.currentTime); 458 | if (isnan(sec) || sec < 0) { 459 | return 0; 460 | } 461 | return sec; 462 | } 463 | 464 | - (NSArray *)playableSuffixArray 465 | { 466 | if (!_playableSuffixArray) { 467 | _playableSuffixArray = @[@"WMV",@"AVI",@"MKV",@"RMVB",@"RM",@"XVID",@"MP4",@"3GP",@"MPG"]; 468 | } 469 | return _playableSuffixArray; 470 | } 471 | 472 | #pragma mark - setter 473 | 474 | - (void)setPlayState:(ZFPlayerPlaybackState)playState { 475 | _playState = playState; 476 | if (self.playerPlayStateChanged) self.playerPlayStateChanged(self, playState); 477 | } 478 | 479 | - (void)setLoadState:(ZFPlayerLoadState)loadState { 480 | _loadState = loadState; 481 | if (self.playerLoadStateChanged) self.playerLoadStateChanged(self, loadState); 482 | } 483 | 484 | - (void)setAssetURL:(NSURL *)assetURL { 485 | if (self.player) [self stop]; 486 | // 如果有缓存,直接取本地缓存 487 | NSURL *url = [KTVHTTPCache cacheCompleteFileURLWithURL:assetURL]; 488 | NSString *suffix = [url pathExtension]; 489 | if (url && [self isPlayableSuffix:suffix]) { 490 | _assetURL = url; 491 | }else { 492 | // 设置代理 493 | _assetURL = [KTVHTTPCache proxyURLWithOriginalURL:assetURL]; 494 | } 495 | [self prepareToPlay]; 496 | } 497 | 498 | - (void)setRate:(float)rate { 499 | _rate = rate; 500 | if (self.player && fabsf(_player.rate) > 0.00001f) { 501 | self.player.rate = rate; 502 | } 503 | } 504 | 505 | - (void)setMuted:(BOOL)muted { 506 | _muted = muted; 507 | self.player.muted = muted; 508 | } 509 | 510 | - (void)setScalingMode:(ZFPlayerScalingMode)scalingMode { 511 | _scalingMode = scalingMode; 512 | ZFPlayerPresentView *presentView = (ZFPlayerPresentView *)self.view; 513 | [CATransaction begin]; 514 | [CATransaction setDisableActions:YES]; 515 | switch (scalingMode) { 516 | case ZFPlayerScalingModeNone: 517 | presentView.videoGravity = AVLayerVideoGravityResizeAspect; 518 | break; 519 | case ZFPlayerScalingModeAspectFit: 520 | presentView.videoGravity = AVLayerVideoGravityResizeAspect; 521 | break; 522 | case ZFPlayerScalingModeAspectFill: 523 | presentView.videoGravity = AVLayerVideoGravityResizeAspectFill; 524 | break; 525 | case ZFPlayerScalingModeFill: 526 | presentView.videoGravity = AVLayerVideoGravityResize; 527 | break; 528 | default: 529 | break; 530 | } 531 | [CATransaction commit]; 532 | } 533 | 534 | - (void)setVolume:(float)volume { 535 | _volume = MIN(MAX(0, volume), 1); 536 | self.player.volume = volume; 537 | } 538 | 539 | 540 | @end 541 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/ZFDouYinControlView.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZFDouYinControlView.h 3 | // ZFPlayer_Example 4 | // 5 | // Created by 任子丰 on 2018/6/4. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import 10 | #import 11 | 12 | @interface ZFDouYinControlView : UIView 13 | 14 | - (void)resetControlView; 15 | 16 | - (void)showCoverViewWithUrl:(NSString *)coverUrl withImageMode:(UIViewContentMode)contentMode; 17 | 18 | @end 19 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Player/ZFDouYinControlView.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZFDouYinControlView.m 3 | // ZFPlayer_Example 4 | // 5 | // Created by 任子丰 on 2018/6/4. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import "ZFDouYinControlView.h" 10 | #import 11 | #import 12 | #import 13 | #import "ZFLoadingView.h" 14 | #import 15 | 16 | @interface ZFDouYinControlView () 17 | /// 封面图 18 | @property (nonatomic, strong) UIImageView *coverImageView; 19 | @property (nonatomic, strong) UIButton *playBtn; 20 | @property (nonatomic, strong) ZFSliderView *sliderView; 21 | @property (nonatomic, strong) UIImageView *bgImgView; 22 | @property (nonatomic, strong) UIView *effectView; 23 | 24 | @end 25 | 26 | @implementation ZFDouYinControlView 27 | @synthesize player = _player; 28 | 29 | - (instancetype)init { 30 | self = [super init]; 31 | if (self) { 32 | [self addSubview:self.playBtn]; 33 | [self addSubview:self.sliderView]; 34 | [self resetControlView]; 35 | } 36 | return self; 37 | } 38 | 39 | - (void)layoutSubviews { 40 | [super layoutSubviews]; 41 | self.coverImageView.frame = self.player.currentPlayerManager.view.bounds; 42 | 43 | CGFloat min_x = 0; 44 | CGFloat min_y = 0; 45 | CGFloat min_w = 0; 46 | CGFloat min_h = 0; 47 | CGFloat min_view_w = self.zf_width; 48 | CGFloat min_view_h = self.zf_height; 49 | 50 | min_w = 100; 51 | min_h = 100; 52 | self.playBtn.frame = CGRectMake(min_x, min_y, min_w, min_h); 53 | self.playBtn.center = self.center; 54 | 55 | min_x = 0; 56 | min_y = min_view_h - 80; 57 | min_w = min_view_w; 58 | min_h = 1; 59 | self.sliderView.frame = CGRectMake(min_x, min_y, min_w, min_h); 60 | 61 | self.bgImgView.frame = self.bounds; 62 | self.effectView.frame = self.bgImgView.bounds; 63 | } 64 | 65 | - (void)resetControlView { 66 | self.playBtn.hidden = YES; 67 | self.sliderView.value = 0; 68 | self.sliderView.bufferValue = 0; 69 | self.coverImageView.contentMode = UIViewContentModeScaleAspectFit; 70 | } 71 | 72 | /// 加载状态改变 73 | - (void)videoPlayer:(ZFPlayerController *)videoPlayer loadStateChanged:(ZFPlayerLoadState)state { 74 | if (state == ZFPlayerLoadStatePrepare) { 75 | self.coverImageView.hidden = NO; 76 | } else if (state == ZFPlayerLoadStatePlaythroughOK || state == ZFPlayerLoadStatePlayable) { 77 | self.coverImageView.hidden = YES; 78 | self.effectView.hidden = NO; 79 | } 80 | if ((state == ZFPlayerLoadStateStalled || state == ZFPlayerLoadStatePrepare) && videoPlayer.currentPlayerManager.isPlaying) { 81 | [self.sliderView startAnimating]; 82 | } else { 83 | [self.sliderView stopAnimating]; 84 | } 85 | } 86 | 87 | /// 播放进度改变回调 88 | - (void)videoPlayer:(ZFPlayerController *)videoPlayer currentTime:(NSTimeInterval)currentTime totalTime:(NSTimeInterval)totalTime { 89 | self.sliderView.value = videoPlayer.progress; 90 | } 91 | 92 | - (void)gestureSingleTapped:(ZFPlayerGestureControl *)gestureControl { 93 | if (self.player.currentPlayerManager.isPlaying) { 94 | [self.player.currentPlayerManager pause]; 95 | self.playBtn.hidden = NO; 96 | self.playBtn.transform = CGAffineTransformMakeScale(1.5f, 1.5f); 97 | [UIView animateWithDuration:0.2f delay:0 98 | options:UIViewAnimationOptionCurveEaseIn animations:^{ 99 | self.playBtn.transform = CGAffineTransformIdentity; 100 | } completion:^(BOOL finished) { 101 | }]; 102 | } else { 103 | [self.player.currentPlayerManager play]; 104 | self.playBtn.hidden = YES; 105 | } 106 | } 107 | 108 | - (void)setPlayer:(ZFPlayerController *)player { 109 | _player = player; 110 | [player.currentPlayerManager.view insertSubview:self.bgImgView atIndex:0]; 111 | [self.bgImgView addSubview:self.effectView]; 112 | [player.currentPlayerManager.view insertSubview:self.coverImageView atIndex:1]; 113 | } 114 | 115 | - (void)showCoverViewWithUrl:(NSString *)coverUrl withImageMode:(UIViewContentMode)contentMode { 116 | self.coverImageView.contentMode = contentMode; 117 | [self.coverImageView setImageWithURLString:coverUrl placeholder:[UIImage imageNamed:@"img_video_loading"]]; 118 | [self.bgImgView setImageWithURLString:coverUrl placeholder:[UIImage imageNamed:@"img_video_loading"]]; 119 | } 120 | 121 | - (BOOL)gestureTriggerCondition:(ZFPlayerGestureControl *)gestureControl 122 | gestureType:(ZFPlayerGestureType)gestureType 123 | gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer 124 | touch:(UITouch *)touch 125 | { 126 | if (gestureType == ZFPlayerGestureTypePan) { 127 | return NO; 128 | } 129 | return YES; 130 | } 131 | 132 | #pragma mark - getter 133 | 134 | - (UIImageView *)bgImgView { 135 | if (!_bgImgView) { 136 | _bgImgView = [[UIImageView alloc] init]; 137 | _bgImgView.userInteractionEnabled = YES; 138 | } 139 | return _bgImgView; 140 | } 141 | 142 | - (UIView *)effectView { 143 | if (!_effectView) { 144 | if (@available(iOS 8.0, *)) { 145 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 146 | _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; 147 | } else { 148 | UIToolbar *effectView = [[UIToolbar alloc] init]; 149 | effectView.barStyle = UIBarStyleBlackTranslucent; 150 | _effectView = effectView; 151 | } 152 | } 153 | return _effectView; 154 | } 155 | 156 | - (UIButton *)playBtn { 157 | if (!_playBtn) { 158 | _playBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 159 | _playBtn.userInteractionEnabled = NO; 160 | [_playBtn setImage:[UIImage imageNamed:@"icon_play_pause"] forState:UIControlStateNormal]; 161 | } 162 | return _playBtn; 163 | } 164 | 165 | - (ZFSliderView *)sliderView { 166 | if (!_sliderView) { 167 | _sliderView = [[ZFSliderView alloc] init]; 168 | _sliderView.maximumTrackTintColor = [UIColor colorWithRed:1 green:1 blue:1 alpha:0.2]; 169 | _sliderView.minimumTrackTintColor = [UIColor whiteColor]; 170 | _sliderView.bufferTrackTintColor = [UIColor clearColor]; 171 | _sliderView.sliderHeight = 1; 172 | _sliderView.isHideSliderBlock = NO; 173 | } 174 | return _sliderView; 175 | } 176 | 177 | - (UIImageView *)coverImageView { 178 | if (!_coverImageView) { 179 | _coverImageView = [[UIImageView alloc] init]; 180 | _coverImageView.userInteractionEnabled = YES; 181 | _coverImageView.clipsToBounds = YES; 182 | } 183 | return _coverImageView; 184 | } 185 | 186 | @end 187 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Transition/MPTransition.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPTransition.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/31. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @class MPPlayerController; 14 | 15 | /// 用于视频信息流的转场动画 16 | @interface MPTransition : NSObject 17 | 18 | /** 19 | 初始化动画 20 | 21 | @param duration 动画时长 22 | @param startView 开始视图 23 | @param startImage 开始图片 24 | @param player 播放器 25 | @param operation 动画形式 26 | @param completion 动画完成block 27 | @return 动画实例 28 | */ 29 | + (instancetype)animationWithDuration:(NSTimeInterval)duration 30 | startView:(UIView *)startView 31 | startImage:(UIImage *)startImage 32 | player: (MPPlayerController *)player 33 | operation:(UINavigationControllerOperation)operation 34 | completion:(void(^)(void))completion; 35 | 36 | @end 37 | 38 | NS_ASSUME_NONNULL_END 39 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Transition/MPTransition.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPTransition.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/31. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPTransition.h" 10 | #import "MPPlayerController.h" 11 | #import 12 | 13 | @interface MPTransition() 14 | 15 | @property (nonatomic, strong) UIView *startView; 16 | @property (nonatomic, strong) UIImage *startImage; 17 | @property (nonatomic, assign) NSTimeInterval duration; 18 | @property (nonatomic, strong) MPPlayerController *player; 19 | @property (nonatomic, assign) UINavigationControllerOperation operation; 20 | @property (nonatomic, assign) void(^completion)(void); 21 | @property (nonatomic, strong) UIView *effectView; 22 | 23 | @end 24 | 25 | @implementation MPTransition 26 | 27 | + (instancetype)animationWithDuration:(NSTimeInterval)duration 28 | startView:(UIView *)startView 29 | startImage:(UIImage *)startImage 30 | player: (MPPlayerController *)player 31 | operation:(UINavigationControllerOperation)operation 32 | completion:(void(^)(void))completion 33 | { 34 | MPTransition *animation = [MPTransition new]; 35 | animation.player = player; 36 | animation.duration = duration; 37 | animation.startView = startView; 38 | animation.startImage = startImage; 39 | animation.operation = operation; 40 | animation.completion = completion; 41 | 42 | return animation; 43 | } 44 | 45 | - (void)animateTransition:(id)transitionContext { 46 | if (self.operation == UINavigationControllerOperationPush) { 47 | [self startPushAnimation: transitionContext]; 48 | }else { 49 | [self startPopAnimation: transitionContext]; 50 | } 51 | } 52 | 53 | - (void)startPushAnimation:(id)transitionContext 54 | { 55 | // 获取 fromView 和 toView 56 | UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 57 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 58 | 59 | // 添加到动画容器视图中 60 | [[transitionContext containerView] addSubview:fromView]; 61 | [[transitionContext containerView] addSubview:toView]; 62 | 63 | UIImageView *bgImgView = [[UIImageView alloc] initWithFrame:fromView.bounds]; 64 | bgImgView.contentMode = UIViewContentModeScaleAspectFill; 65 | bgImgView.image = self.startImage; 66 | UIView *colorCover = [[UIView alloc] init]; 67 | colorCover.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4]; 68 | colorCover.frame = fromView.bounds; 69 | [bgImgView addSubview:colorCover]; 70 | [bgImgView addSubview:self.effectView]; 71 | [[transitionContext containerView] addSubview:bgImgView]; 72 | 73 | // 创建player容器 74 | CGRect winFrame = CGRectZero; 75 | if (self.startView) { 76 | winFrame = [self.startView convertRect:self.startView.bounds toView:nil]; 77 | } 78 | 79 | UIImageView *playerContainer = [[UIImageView alloc] initWithFrame:winFrame]; 80 | playerContainer.image = self.startImage; 81 | playerContainer.contentMode = UIViewContentModeScaleAspectFit; 82 | [[transitionContext containerView] addSubview:playerContainer]; 83 | if (self.player) { 84 | self.player.currentPlayerManager.scalingMode = self.player.videoFlowScalingMode; 85 | self.player.currentPlayerManager.view.backgroundColor = [UIColor clearColor]; 86 | [self.player updateNoramlPlayerWithContainerView:playerContainer]; 87 | } 88 | CGFloat bottomOffset = iPhoneX ? 83 : 0; 89 | NSTimeInterval duration = [self transitionDuration:transitionContext]; 90 | CGRect targetFrame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight - bottomOffset); 91 | 92 | toView.alpha = 0.0f; 93 | bgImgView.alpha = 0; 94 | [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 95 | // mask 渐变效果 96 | bgImgView.alpha = 1; 97 | playerContainer.frame = targetFrame; 98 | } completion:^(BOOL finished) { 99 | toView.alpha = 1.0f; 100 | // 移除临时视图 101 | [bgImgView removeFromSuperview]; 102 | [playerContainer removeFromSuperview]; 103 | // 结束动画 104 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 105 | if (self.completion) { 106 | self.completion(); 107 | } 108 | }]; 109 | } 110 | 111 | - (void)startPopAnimation: (id)transitionContext 112 | { 113 | // 获取 fromView 和 toView 114 | UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 115 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 116 | self.player.currentPlayerManager.view.backgroundColor = [UIColor blackColor]; 117 | 118 | // 添加到动画容器视图中 119 | UIView *container = [transitionContext containerView]; 120 | [container addSubview:toView]; 121 | [container addSubview:fromView]; 122 | container.backgroundColor = [UIColor clearColor]; 123 | 124 | // 添加动画临时视图到 fromView 125 | CGFloat bottomOffset = iPhoneX ? 83 : 0; 126 | CGRect normalFrame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight - bottomOffset); 127 | CGRect winFrame = CGRectZero; 128 | if (self.startView) { 129 | winFrame = [self.startView convertRect:self.startView.bounds toView:nil]; 130 | } 131 | 132 | // 显示图片 133 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:normalFrame]; 134 | imageView.contentMode = UIViewContentModeScaleAspectFit; 135 | imageView.clipsToBounds = YES; 136 | imageView.image = self.startImage; 137 | if (self.player) { 138 | // pop回去的时候,设置回原来的scalingMode 139 | self.player.currentPlayerManager.scalingMode = ZFPlayerScalingModeAspectFill; 140 | [self.player updateNoramlPlayerWithContainerView:imageView]; 141 | } 142 | 143 | [container addSubview:imageView]; 144 | 145 | toView.alpha = 1; 146 | fromView.alpha = 1; 147 | NSTimeInterval duration = [self transitionDuration:transitionContext]; 148 | 149 | [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 150 | imageView.frame = winFrame; 151 | fromView.alpha = 0; 152 | } completion:^(BOOL finished) { 153 | // 移除临时视图 154 | [imageView removeFromSuperview]; 155 | 156 | // 结束动画 157 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 158 | 159 | if (self.completion) { 160 | self.completion(); 161 | } 162 | }]; 163 | } 164 | 165 | - (NSTimeInterval)transitionDuration:(id)transitionContext { 166 | return self.duration; 167 | } 168 | 169 | - (UIView *)effectView { 170 | if (!_effectView) { 171 | if (@available(iOS 8.0, *)) { 172 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 173 | _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; 174 | } else { 175 | UIToolbar *effectView = [[UIToolbar alloc] init]; 176 | effectView.barStyle = UIBarStyleBlackTranslucent; 177 | _effectView = effectView; 178 | } 179 | } 180 | return _effectView; 181 | } 182 | 183 | @end 184 | 185 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Transition/MPUserDynamicTransition.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicTransition.h 3 | // 4 | // Created by Maple on 2019/12/5. 5 | // 6 | 7 | #import 8 | 9 | NS_ASSUME_NONNULL_BEGIN 10 | 11 | /// 关注的用户动态转场 12 | @interface MPUserDynamicTransition : NSObject 13 | /// 是否手势退出 14 | @property (nonatomic, assign) BOOL isInteracting; 15 | /// 是否手势完成 16 | @property (nonatomic, assign) BOOL isComplete; 17 | /** 18 | 初始化动画 19 | 20 | @param duration 动画时长 21 | @param startView 开始视图 22 | @param startImage 开始图片 23 | @param endX pop动画时的endX 24 | @param operation 动画形式 25 | @return 动画实例 26 | */ 27 | + (instancetype)animationWithDuration:(NSTimeInterval)duration 28 | startView:(UIView *)startView 29 | startImage:(UIImage *)startImage 30 | endX: (CGFloat)endX 31 | operation:(UINavigationControllerOperation)operation; 32 | 33 | 34 | @end 35 | 36 | NS_ASSUME_NONNULL_END 37 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/Transition/MPUserDynamicTransition.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicTransition.m 3 | // 4 | // Created by Maple on 2019/12/5. 5 | // 6 | 7 | #import "MPUserDynamicTransition.h" 8 | #import 9 | 10 | @interface MPUserDynamicTransition () 11 | 12 | @property (nonatomic, assign) NSTimeInterval duration; 13 | @property (nonatomic, strong) UIView *startView; 14 | @property (nonatomic, strong) UIImage *startImage; 15 | @property (nonatomic, strong) UICollectionView *collectionView; 16 | @property (nonatomic, assign) NSInteger index; 17 | @property (nonatomic, assign) CGFloat endX; 18 | @property (nonatomic, assign) UINavigationControllerOperation operation; 19 | 20 | @end 21 | 22 | @implementation MPUserDynamicTransition 23 | 24 | + (instancetype)animationWithDuration:(NSTimeInterval)duration 25 | startView:(UIView *)startView 26 | startImage:(UIImage *)startImage 27 | endX: (CGFloat)endX 28 | operation:(UINavigationControllerOperation)operation 29 | { 30 | MPUserDynamicTransition *transition = [[self alloc] init]; 31 | transition.duration = duration; 32 | transition.startView = startView; 33 | transition.startImage = startImage; 34 | transition.endX = endX; 35 | transition.operation = operation; 36 | return transition; 37 | } 38 | 39 | - (NSTimeInterval)transitionDuration:(nullable id)transitionContext { 40 | return self.duration; 41 | } 42 | 43 | - (void)animateTransition:(nonnull id)transitionContext { 44 | if (self.operation == UINavigationControllerOperationPush) { 45 | [self startPushAnimation: transitionContext]; 46 | }else { 47 | [self startPopAnimation: transitionContext]; 48 | } 49 | } 50 | 51 | - (void)startPushAnimation: (nonnull id)transitionContext 52 | { 53 | UIView *contentView = [transitionContext containerView]; 54 | // 获取 fromView 和 toView 55 | UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 56 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 57 | [contentView addSubview:fromView]; 58 | [contentView addSubview:toView]; 59 | 60 | UIImageView *imageView = [[UIImageView alloc] init]; 61 | imageView.contentMode = UIViewContentModeScaleAspectFill; 62 | imageView.clipsToBounds = YES; 63 | imageView.layer.cornerRadius = self.startView.bounds.size.width * 0.5; 64 | imageView.layer.masksToBounds = YES; 65 | imageView.image = self.startImage; 66 | CGRect winFrame = CGRectZero; 67 | if (self.startView) { 68 | winFrame = [self.startView convertRect:self.startView.bounds toView:nil]; 69 | } 70 | imageView.frame = winFrame; 71 | toView.center = imageView.center; 72 | CGFloat scale = 56.0 / ZFPlayer_ScreenWidth; 73 | toView.transform = CGAffineTransformMakeScale(scale, scale); 74 | [UIView animateWithDuration:self.duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 75 | toView.transform = CGAffineTransformIdentity; 76 | toView.center = CGPointMake(ZFPlayer_ScreenWidth * 0.5, ZFPlayer_ScreenHeight * 0.5); 77 | } completion:^(BOOL finished) { 78 | // 结束动画 79 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 80 | }]; 81 | } 82 | 83 | - (void)startPopAnimation: (nonnull id)transitionContext 84 | { 85 | UIView *contentView = [transitionContext containerView]; 86 | // 获取 fromView 和 toView 87 | UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 88 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 89 | UIView *whiteCoverView = [[UIView alloc] init]; 90 | whiteCoverView.backgroundColor = [UIColor blackColor]; 91 | whiteCoverView.frame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight); 92 | whiteCoverView.alpha = 0; 93 | [contentView addSubview:toView]; 94 | [contentView addSubview:whiteCoverView]; 95 | [contentView addSubview:fromView]; 96 | 97 | UIImageView *imageView = [[UIImageView alloc] init]; 98 | imageView.contentMode = UIViewContentModeScaleAspectFill; 99 | imageView.clipsToBounds = YES; 100 | imageView.image = self.startImage; 101 | imageView.layer.cornerRadius = ZFPlayer_ScreenWidth * 0.5; 102 | CGFloat top = (ZFPlayer_ScreenHeight - ZFPlayer_ScreenWidth) * 0.5; 103 | CGRect winFrame = CGRectMake(0, top, ZFPlayer_ScreenWidth, ZFPlayer_ScreenWidth); 104 | imageView.frame = winFrame; 105 | imageView.hidden = YES; 106 | [contentView addSubview:imageView]; 107 | 108 | CGFloat targetCorner = 0; 109 | CGRect targetFrame = CGRectZero; 110 | if (self.startView) { 111 | targetFrame = [self.startView convertRect:self.startView.bounds toView:nil]; 112 | targetFrame = CGRectMake(self.endX, targetFrame.origin.y, targetFrame.size.width, targetFrame.size.height); 113 | targetCorner = self.startView.bounds.size.width * 0.5; 114 | } 115 | dispatch_block_t block = dispatch_block_create(0, ^{ 116 | imageView.hidden = NO; 117 | toView.alpha = 1.0f; 118 | fromView.transform = CGAffineTransformIdentity; 119 | fromView.alpha = 0.0f; 120 | whiteCoverView.alpha = 0.4; 121 | [UIView animateWithDuration:self.duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 122 | whiteCoverView.alpha = 0; 123 | imageView.frame = targetFrame; 124 | imageView.layer.cornerRadius = targetCorner; 125 | } completion:^(BOOL finished) { 126 | [imageView removeFromSuperview]; 127 | [whiteCoverView removeFromSuperview]; 128 | // 结束动画 129 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 130 | }]; 131 | }); 132 | if (self.isInteracting) { 133 | whiteCoverView.alpha = 1; 134 | [UIView animateWithDuration:self.duration animations:^{ 135 | whiteCoverView.alpha = 0.4; 136 | fromView.transform = CGAffineTransformScale(fromView.transform, 0.9, 0.9); 137 | fromView.transform = CGAffineTransformTranslate(fromView.transform, 0, ZFPlayer_ScreenHeight * 0.5); 138 | } completion:^(BOOL finished) { 139 | if (self.isComplete) { 140 | block(); 141 | }else { 142 | [imageView removeFromSuperview]; 143 | [whiteCoverView removeFromSuperview]; 144 | // 结束动画 145 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 146 | } 147 | }]; 148 | }else { 149 | block(); 150 | } 151 | } 152 | 153 | - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{ 154 | // 标记转场结束 155 | id transitionContext = [anim valueForKey:@"transitionContext"]; 156 | // 结束动画 157 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 158 | } 159 | 160 | @end 161 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/MPCardLayoutCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPCardLayoutCell.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPCardLayoutCell : UICollectionViewCell 14 | 15 | @property (nonatomic, strong) UIImageView *coverImageView; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/MPCardLayoutCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPCardLayoutCell.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPCardLayoutCell.h" 10 | 11 | @implementation MPCardLayoutCell 12 | 13 | - (instancetype)initWithFrame:(CGRect)frame 14 | { 15 | self = [super initWithFrame:frame]; 16 | self.coverImageView = [[UIImageView alloc] init]; 17 | self.coverImageView.contentMode = UIViewContentModeScaleAspectFill; 18 | self.coverImageView.layer.masksToBounds = YES; 19 | [self.contentView addSubview:self.coverImageView]; 20 | return self; 21 | } 22 | 23 | - (void)layoutSubviews 24 | { 25 | [super layoutSubviews]; 26 | self.coverImageView.frame = self.bounds; 27 | } 28 | 29 | 30 | @end 31 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/MPUserDynamicCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicCollectionViewCell.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/18. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | NS_ASSUME_NONNULL_BEGIN 12 | 13 | @interface MPUserDynamicCollectionViewCell : UICollectionViewCell 14 | 15 | @property (nonatomic, strong) UIImageView *iconImageView; 16 | 17 | @end 18 | 19 | NS_ASSUME_NONNULL_END 20 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/MPUserDynamicCollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPUserDynamicCollectionViewCell.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/3/18. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPUserDynamicCollectionViewCell.h" 10 | 11 | @implementation MPUserDynamicCollectionViewCell 12 | 13 | - (instancetype)initWithFrame:(CGRect)frame 14 | { 15 | self = [super initWithFrame:frame]; 16 | [self setup]; 17 | return self; 18 | } 19 | 20 | - (void)setup 21 | { 22 | self.iconImageView = [[UIImageView alloc] init]; 23 | self.iconImageView.contentMode = UIViewContentModeScaleAspectFill; 24 | self.iconImageView.layer.cornerRadius = 40; 25 | self.iconImageView.layer.masksToBounds = YES; 26 | self.iconImageView.layer.borderWidth = 1; 27 | self.iconImageView.layer.borderColor = [UIColor orangeColor].CGColor; 28 | self.iconImageView.frame = CGRectMake(0, 0, 80, 80); 29 | [self.contentView addSubview:self.iconImageView]; 30 | } 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/MPWaterFallCollectionViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPWaterFallCollectionViewCell.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/1/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ZFTableData.h" 11 | 12 | @interface MPWaterFallCollectionViewCell : UICollectionViewCell 13 | 14 | @property (nonatomic, strong) UIImageView *imageView; 15 | @property (nonatomic, strong) UILabel *titleLabel; 16 | @property (nonatomic, strong) ZFTableData *data; 17 | 18 | @end 19 | 20 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/MPWaterFallCollectionViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPWaterFallCollectionViewCell.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/1/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPWaterFallCollectionViewCell.h" 10 | #import 11 | 12 | @implementation MPWaterFallCollectionViewCell 13 | 14 | - (instancetype)initWithFrame:(CGRect)frame 15 | { 16 | self = [super initWithFrame:frame]; 17 | [self setup]; 18 | return self; 19 | } 20 | 21 | - (void)setup 22 | { 23 | self.imageView = [[UIImageView alloc] init]; 24 | [self.contentView addSubview:self.imageView]; 25 | } 26 | 27 | - (void)layoutSubviews 28 | { 29 | [super layoutSubviews]; 30 | self.imageView.frame = self.contentView.bounds; 31 | } 32 | 33 | - (void)setData:(ZFTableData *)data 34 | { 35 | _data = data; 36 | [self.imageView setImageWithURLString:data.thumbnail_url placeholder:nil]; 37 | } 38 | 39 | @end 40 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/ZFDouYinCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZFDouYinCell.h 3 | // ZFPlayer_Example 4 | // 5 | // Created by 紫枫 on 2018/6/4. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ZFTableData.h" 11 | 12 | @interface ZFDouYinCell : UITableViewCell 13 | 14 | @property (nonatomic, strong) ZFTableData *data; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/ZFDouYinCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZFDouYinCell.m 3 | // ZFPlayer_Example 4 | // 5 | // Created by 紫枫 on 2018/6/4. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import "ZFDouYinCell.h" 10 | #import 11 | #import 12 | #import 13 | #import 14 | #import "ZFLoadingView.h" 15 | 16 | @interface ZFDouYinCell () 17 | 18 | @property (nonatomic, strong) UIImageView *coverImageView; 19 | 20 | @property (nonatomic, strong) UIButton *likeBtn; 21 | 22 | @property (nonatomic, strong) UIButton *commentBtn; 23 | 24 | @property (nonatomic, strong) UIButton *shareBtn; 25 | 26 | @property (nonatomic, strong) UILabel *titleLabel; 27 | 28 | @property (nonatomic, strong) UIImage *placeholderImage; 29 | 30 | @property (nonatomic, strong) UIImageView *bgImgView; 31 | 32 | @property (nonatomic, strong) UIView *effectView; 33 | 34 | @end 35 | 36 | @implementation ZFDouYinCell 37 | 38 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 39 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 40 | if (self) { 41 | self.selectionStyle = UITableViewCellSelectionStyleNone; 42 | [self.contentView addSubview:self.bgImgView]; 43 | [self.bgImgView addSubview:self.effectView]; 44 | [self.contentView addSubview:self.coverImageView]; 45 | [self.contentView addSubview:self.titleLabel]; 46 | [self.contentView addSubview:self.likeBtn]; 47 | [self.contentView addSubview:self.commentBtn]; 48 | [self.contentView addSubview:self.shareBtn]; 49 | } 50 | return self; 51 | } 52 | 53 | - (void)layoutSubviews { 54 | [super layoutSubviews]; 55 | self.coverImageView.frame = self.contentView.bounds; 56 | self.bgImgView.frame = self.contentView.bounds; 57 | self.effectView.frame = self.bgImgView.bounds; 58 | 59 | CGFloat min_x = 0; 60 | CGFloat min_y = 0; 61 | CGFloat min_w = 0; 62 | CGFloat min_h = 0; 63 | CGFloat min_view_w = self.zf_width; 64 | CGFloat min_view_h = self.zf_height; 65 | CGFloat margin = 30; 66 | 67 | min_w = 40; 68 | min_h = min_w; 69 | min_x = min_view_w - min_w - 20; 70 | min_y = min_view_h - min_h - 80; 71 | self.shareBtn.frame = CGRectMake(min_x, min_y, min_w, min_h); 72 | 73 | min_w = CGRectGetWidth(self.shareBtn.frame); 74 | min_h = min_w; 75 | min_x = CGRectGetMinX(self.shareBtn.frame); 76 | min_y = CGRectGetMinY(self.shareBtn.frame) - min_h - margin; 77 | self.commentBtn.frame = CGRectMake(min_x, min_y, min_w, min_h); 78 | 79 | min_w = CGRectGetWidth(self.shareBtn.frame); 80 | min_h = min_w; 81 | min_x = CGRectGetMinX(self.commentBtn.frame); 82 | min_y = CGRectGetMinY(self.commentBtn.frame) - min_h - margin; 83 | self.likeBtn.frame = CGRectMake(min_x, min_y, min_w, min_h); 84 | 85 | min_x = 20; 86 | min_h = 20; 87 | min_y = min_view_h - min_h - 50; 88 | min_w = self.likeBtn.zf_left - margin; 89 | self.titleLabel.frame = CGRectMake(min_x, min_y, min_w, min_h); 90 | } 91 | 92 | - (UILabel *)titleLabel { 93 | if (!_titleLabel) { 94 | _titleLabel = [UILabel new]; 95 | _titleLabel.textColor = [UIColor whiteColor]; 96 | _titleLabel.font = [UIFont systemFontOfSize:15]; 97 | } 98 | return _titleLabel; 99 | } 100 | 101 | - (UIButton *)likeBtn { 102 | if (!_likeBtn) { 103 | _likeBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 104 | [_likeBtn setImage:[UIImage imageNamed:@"like"] forState:UIControlStateNormal]; 105 | } 106 | return _likeBtn; 107 | } 108 | 109 | 110 | - (UIButton *)commentBtn { 111 | if (!_commentBtn) { 112 | _commentBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 113 | [_commentBtn setImage:[UIImage imageNamed:@"comment"] forState:UIControlStateNormal]; 114 | } 115 | return _commentBtn; 116 | } 117 | 118 | - (UIButton *)shareBtn { 119 | if (!_shareBtn) { 120 | _shareBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 121 | [_shareBtn setImage:[UIImage imageNamed:@"share"] forState:UIControlStateNormal]; 122 | } 123 | return _shareBtn; 124 | } 125 | 126 | - (UIImage *)placeholderImage { 127 | if (!_placeholderImage) { 128 | _placeholderImage = [ZFUtilities imageWithColor:[UIColor colorWithRed:220/255.0 green:220/255.0 blue:220/255.0 alpha:1] size:CGSizeMake(1, 1)]; 129 | } 130 | return _placeholderImage; 131 | } 132 | 133 | - (void)setData:(ZFTableData *)data { 134 | _data = data; 135 | if (data.thumbnail_width >= data.thumbnail_height) { 136 | self.coverImageView.contentMode = UIViewContentModeScaleAspectFit; 137 | self.coverImageView.clipsToBounds = NO; 138 | } else { 139 | self.coverImageView.contentMode = UIViewContentModeScaleAspectFill; 140 | self.coverImageView.clipsToBounds = YES; 141 | } 142 | [self.coverImageView setImageWithURLString:data.thumbnail_url placeholder:[UIImage imageNamed:@"loading_bgView"]]; 143 | [self.bgImgView setImageWithURLString:data.thumbnail_url placeholder:[UIImage imageNamed:@"loading_bgView"]]; 144 | self.titleLabel.text = data.title; 145 | } 146 | 147 | - (UIImageView *)coverImageView { 148 | if (!_coverImageView) { 149 | _coverImageView = [[UIImageView alloc] init]; 150 | _coverImageView.userInteractionEnabled = YES; 151 | _coverImageView.tag = 10086; 152 | // _coverImageView.contentMode = UIViewContentModeScaleAspectFit; 153 | } 154 | return _coverImageView; 155 | } 156 | 157 | - (UIImageView *)bgImgView { 158 | if (!_bgImgView) { 159 | _bgImgView = [[UIImageView alloc] init]; 160 | _bgImgView.userInteractionEnabled = YES; 161 | } 162 | return _bgImgView; 163 | } 164 | 165 | - (UIView *)effectView { 166 | if (!_effectView) { 167 | if (@available(iOS 8.0, *)) { 168 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 169 | _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; 170 | } else { 171 | UIToolbar *effectView = [[UIToolbar alloc] init]; 172 | effectView.barStyle = UIBarStyleBlackTranslucent; 173 | _effectView = effectView; 174 | } 175 | } 176 | return _effectView; 177 | } 178 | 179 | @end 180 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/ZFTableViewCell.h: -------------------------------------------------------------------------------- 1 | // 2 | // ZFTableViewCell.h 3 | // ZFPlayer 4 | // 5 | // Created by 紫枫 on 2018/4/3. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "ZFTableData.h" 11 | #import "ZFTableViewCellLayout.h" 12 | 13 | @protocol ZFTableViewCellDelegate 14 | 15 | - (void)zf_playTheVideoAtIndexPath:(NSIndexPath *)indexPath; 16 | 17 | @end 18 | 19 | @interface ZFTableViewCell : UITableViewCell 20 | 21 | @property (nonatomic, strong) ZFTableViewCellLayout *layout; 22 | @property (nonatomic, strong, readonly) UIImageView *coverImageView; 23 | @property (nonatomic, copy) void(^playCallback)(void); 24 | 25 | - (void)setDelegate:(id)delegate withIndexPath:(NSIndexPath *)indexPath; 26 | 27 | - (void)showMaskView; 28 | 29 | - (void)hideMaskView; 30 | 31 | - (void)setNormalMode; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/View/ZFTableViewCell.m: -------------------------------------------------------------------------------- 1 | // 2 | // ZFTableViewCell.m 3 | // ZFPlayer 4 | // 5 | // Created by 紫枫 on 2018/4/3. 6 | // Copyright © 2018年 紫枫. All rights reserved. 7 | // 8 | 9 | #import "ZFTableViewCell.h" 10 | #import 11 | 12 | @interface ZFTableViewCell () 13 | @property (nonatomic, strong) UIImageView *headImageView; 14 | @property (nonatomic, strong) UILabel *nickNameLabel; 15 | @property (nonatomic, strong) UIImageView *coverImageView; 16 | @property (nonatomic, strong) UIView *fullMaskView; 17 | @property (nonatomic, strong) UIButton *playBtn; 18 | @property (nonatomic, strong) UILabel *titleLabel; 19 | @property (nonatomic, weak) id delegate; 20 | @property (nonatomic, strong) NSIndexPath *indexPath; 21 | 22 | @property (nonatomic, strong) UIImageView *bgImgView; 23 | 24 | @property (nonatomic, strong) UIView *effectView; 25 | 26 | @end 27 | 28 | @implementation ZFTableViewCell 29 | 30 | - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { 31 | self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; 32 | if (self) { 33 | [self.contentView addSubview:self.headImageView]; 34 | [self.contentView addSubview:self.nickNameLabel]; 35 | [self.contentView addSubview:self.bgImgView]; 36 | [self.bgImgView addSubview:self.effectView]; 37 | [self.contentView addSubview:self.coverImageView]; 38 | [self.coverImageView addSubview:self.playBtn]; 39 | [self.contentView addSubview:self.titleLabel]; 40 | [self.contentView addSubview:self.fullMaskView]; 41 | self.contentView.backgroundColor = [UIColor blackColor]; 42 | self.selectionStyle = UITableViewCellSelectionStyleNone; 43 | } 44 | return self; 45 | } 46 | 47 | - (void)setLayout:(ZFTableViewCellLayout *)layout { 48 | _layout = layout; 49 | self.headImageView.frame = layout.headerRect; 50 | self.nickNameLabel.frame = layout.nickNameRect; 51 | self.coverImageView.frame = layout.videoRect; 52 | self.bgImgView.frame = layout.videoRect; 53 | self.effectView.frame = self.bgImgView.bounds; 54 | self.titleLabel.frame = layout.titleLabelRect; 55 | self.playBtn.frame = layout.playBtnRect; 56 | self.fullMaskView.frame = layout.maskViewRect; 57 | 58 | [self.headImageView setImageWithURLString:layout.data.head placeholder:[UIImage imageNamed:@"defaultUserIcon"]]; 59 | [self.coverImageView setImageWithURLString:layout.data.thumbnail_url placeholder:[UIImage imageNamed:@"loading_bgView"]]; 60 | [self.bgImgView setImageWithURLString:layout.data.thumbnail_url placeholder:[UIImage imageNamed:@"loading_bgView"]]; 61 | self.nickNameLabel.text = layout.data.nick_name; 62 | self.titleLabel.text = layout.data.title; 63 | } 64 | 65 | - (void)setDelegate:(id)delegate withIndexPath:(NSIndexPath *)indexPath { 66 | self.delegate = delegate; 67 | self.indexPath = indexPath; 68 | } 69 | 70 | - (void)setNormalMode { 71 | self.fullMaskView.hidden = YES; 72 | self.titleLabel.textColor = [UIColor blackColor]; 73 | self.nickNameLabel.textColor = [UIColor blackColor]; 74 | self.contentView.backgroundColor = [UIColor whiteColor]; 75 | } 76 | 77 | - (void)showMaskView { 78 | [UIView animateWithDuration:0.3 animations:^{ 79 | self.fullMaskView.alpha = 1; 80 | }]; 81 | } 82 | 83 | - (void)hideMaskView { 84 | [UIView animateWithDuration:0.3 animations:^{ 85 | self.fullMaskView.alpha = 0; 86 | }]; 87 | } 88 | 89 | 90 | - (void)playBtnClick:(UIButton *)sender { 91 | if ([self.delegate respondsToSelector:@selector(zf_playTheVideoAtIndexPath:)]) { 92 | [self.delegate zf_playTheVideoAtIndexPath:self.indexPath]; 93 | } 94 | } 95 | 96 | #pragma mark - getter 97 | 98 | - (UIButton *)playBtn { 99 | if (!_playBtn) { 100 | _playBtn = [UIButton buttonWithType:UIButtonTypeCustom]; 101 | [_playBtn setImage:[UIImage imageNamed:@"new_allPlay_44x44_"] forState:UIControlStateNormal]; 102 | [_playBtn addTarget:self action:@selector(playBtnClick:) forControlEvents:UIControlEventTouchUpInside]; 103 | } 104 | return _playBtn; 105 | } 106 | 107 | - (UIView *)fullMaskView { 108 | if (!_fullMaskView) { 109 | _fullMaskView = [UIView new]; 110 | _fullMaskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8]; 111 | _fullMaskView.userInteractionEnabled = NO; 112 | } 113 | return _fullMaskView; 114 | } 115 | 116 | - (UILabel *)titleLabel { 117 | if (!_titleLabel) { 118 | _titleLabel = [UILabel new]; 119 | _titleLabel.textColor = [UIColor whiteColor]; 120 | _titleLabel.numberOfLines = 0; 121 | _titleLabel.font = [UIFont systemFontOfSize:15]; 122 | } 123 | return _titleLabel; 124 | } 125 | 126 | - (UILabel *)nickNameLabel { 127 | if (!_nickNameLabel) { 128 | _nickNameLabel = [UILabel new]; 129 | _nickNameLabel.textColor = [UIColor whiteColor]; 130 | _nickNameLabel.font = [UIFont systemFontOfSize:15]; 131 | } 132 | return _nickNameLabel; 133 | } 134 | 135 | - (UIImageView *)headImageView { 136 | if (!_headImageView) { 137 | _headImageView = [[UIImageView alloc] init]; 138 | _headImageView.userInteractionEnabled = YES; 139 | } 140 | return _headImageView; 141 | } 142 | 143 | - (UIImageView *)coverImageView { 144 | if (!_coverImageView) { 145 | _coverImageView = [[UIImageView alloc] init]; 146 | _coverImageView.userInteractionEnabled = YES; 147 | _coverImageView.tag = 100; 148 | _coverImageView.contentMode = UIViewContentModeScaleAspectFit; 149 | } 150 | return _coverImageView; 151 | } 152 | 153 | - (UIImageView *)bgImgView { 154 | if (!_bgImgView) { 155 | _bgImgView = [[UIImageView alloc] init]; 156 | _bgImgView.userInteractionEnabled = YES; 157 | } 158 | return _bgImgView; 159 | } 160 | 161 | - (UIView *)effectView { 162 | if (!_effectView) { 163 | if (@available(iOS 8.0, *)) { 164 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 165 | _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; 166 | } else { 167 | UIToolbar *effectView = [[UIToolbar alloc] init]; 168 | effectView.barStyle = UIBarStyleBlackTranslucent; 169 | _effectView = effectView; 170 | } 171 | } 172 | return _effectView; 173 | } 174 | 175 | @end 176 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/ViewController.h: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface ViewController : UIViewController 12 | 13 | 14 | @end 15 | 16 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/ViewController.m: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import "ViewController.h" 10 | #import "MPListViewController.h" 11 | #import "MPDetailViewController.h" 12 | #import "MPWaterFallViewController.h" 13 | #import "MPCardLayoutViewController.h" 14 | 15 | @interface ViewController () 16 | 17 | @property (nonatomic, strong) UITableView *tableView; 18 | @property (nonatomic, copy) NSArray *controllerArr; 19 | @property (nonatomic, copy) NSArray *demoName; 20 | 21 | @end 22 | 23 | @implementation ViewController 24 | 25 | - (void)viewDidLoad { 26 | [super viewDidLoad]; 27 | self.controllerArr = @[ 28 | @"MPListViewController", 29 | @"MPDetailViewController", 30 | @"MPWaterFallViewController", 31 | @"MPCardLayoutViewController", 32 | @"MPUserDynamicViewController" 33 | ]; 34 | self.demoName = @[ 35 | @"预加载-列表播放-无缝续播", 36 | @"预加载-抖音列表", 37 | @"瀑布流列表-转场动画演示", 38 | @"卡片布局演示", 39 | @"转场动画手势退出" 40 | ]; 41 | 42 | self.tableView = [[UITableView alloc] init]; 43 | self.tableView.delegate = self; 44 | self.tableView.dataSource = self; 45 | self.tableView.frame = self.view.bounds; 46 | [self.view addSubview:self.tableView]; 47 | } 48 | 49 | // MARK: - UITableViewDelegate, UITableViewDataSource 50 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 51 | { 52 | return self.demoName.count; 53 | } 54 | 55 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 56 | { 57 | UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ID"]; 58 | if (!cell) { 59 | cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"ID"]; 60 | } 61 | cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; 62 | cell.textLabel.text = self.demoName[indexPath.row]; 63 | return cell; 64 | } 65 | 66 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 67 | { 68 | Class cl = NSClassFromString(self.controllerArr[indexPath.row]); 69 | UIViewController *vc = [[cl alloc] init]; 70 | [self.navigationController pushViewController:vc animated:YES]; 71 | } 72 | 73 | @end 74 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/WaterFall/MPWaterFallLayout.h: -------------------------------------------------------------------------------- 1 | // 2 | // MPWaterFallLayout.h 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/1/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @class MPWaterFallLayout; 12 | 13 | @protocol MPWaterFallLayoutDataSource 14 | 15 | @required 16 | /// 获取item高度,返回itemWidth和indexPath去获取 17 | - (CGFloat)waterFallLayout: (MPWaterFallLayout *)layout itemHeightForItemWidth: (CGFloat)itemWidth atIndexPath: (NSIndexPath *)indexPath; 18 | 19 | @end 20 | 21 | @interface MPWaterFallLayout : UICollectionViewLayout 22 | 23 | @property (nonatomic, weak) id dataSource; 24 | /// 根据设置的列数,列间距,返回itemWidth 25 | @property (nonatomic, readonly) CGFloat itemWidth; 26 | /// 总共有多少列,默认为2 27 | @property (nonatomic) NSInteger column; 28 | /// 列间距,默认为0 29 | @property (nonatomic) CGFloat columnSpacing; 30 | /// 行间距 31 | @property (nonatomic) CGFloat rowSpacing; 32 | /// section与collectionView的间距,默认是(0,0,0,0) 33 | @property (nonatomic) UIEdgeInsets sectionInset; 34 | 35 | + (instancetype)waterFallLayoutWithColumn:(NSInteger)column; 36 | - (instancetype)initWIthColumn:(NSInteger)column; 37 | 38 | 39 | @end 40 | 41 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/WaterFall/MPWaterFallLayout.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPWaterFallLayout.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2020/1/16. 6 | // Copyright © 2020 Maple. All rights reserved. 7 | // 8 | 9 | #import "MPWaterFallLayout.h" 10 | 11 | @interface MPWaterFallLayout () 12 | /// 用于记录每一列的最大Y值 13 | @property (nonatomic, strong) NSMutableDictionary *maxYDic; 14 | /// attributes数组 15 | @property (nonatomic, strong) NSMutableArray *attributesArray; 16 | 17 | @end 18 | 19 | @implementation MPWaterFallLayout 20 | 21 | + (instancetype)waterFallLayoutWithColumn:(NSInteger)column 22 | { 23 | return [[self alloc] initWIthColumn:column]; 24 | } 25 | 26 | - (instancetype)init 27 | { 28 | if (self = [super init]) { 29 | self.column = 2; 30 | } 31 | return self; 32 | } 33 | 34 | - (instancetype)initWIthColumn:(NSInteger)column 35 | { 36 | if (self = [super init]) { 37 | self.column = column; 38 | } 39 | return self; 40 | } 41 | 42 | // MARK: - Layout Override 布局必须重写的方法 43 | /// 1、初始化数据源 44 | - (void)prepareLayout 45 | { 46 | [super prepareLayout]; 47 | [self.attributesArray removeAllObjects]; 48 | // 初始化字典,有几列就有几个键值对,key为列,value为列的最大y值, 49 | // 初始值为上内边距 50 | for (int i = 0; i < self.column; i++) { 51 | self.maxYDic[@(i)] = @(self.sectionInset.top); 52 | } 53 | // 获取item总数 54 | NSInteger itemCount = [self.collectionView numberOfItemsInSection:0]; 55 | 56 | // 为每个Item创建attributes存入数组中 57 | for (int i = 0; i < itemCount; i++) { 58 | // 循环调用2去计算item attribute 59 | UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForItem:i inSection:0]]; 60 | [self.attributesArray addObject:attributes]; 61 | } 62 | } 63 | 64 | /// 2、计算每个Attribute 65 | - (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath 66 | { 67 | UICollectionViewLayoutAttributes *attributes = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath]; 68 | NSAssert([self.dataSource respondsToSelector:@selector(waterFallLayout:itemHeightForItemWidth:atIndexPath:)], @"you must override waterFallLayout:itemHeightForItemWidth:atIndexPath: methods - Warning : 需要重写瀑布流的返回高度代理方法!"); 69 | CGFloat itemWidth = self.itemWidth; 70 | CGFloat itemHeight = [self.dataSource waterFallLayout:self itemHeightForItemWidth:itemWidth atIndexPath:attributes.indexPath]; 71 | 72 | /// 找出最短的一列 73 | __block NSNumber *minIndex = @(0); 74 | [self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSNumber *obj, BOOL * _Nonnull stop) { 75 | if ([self.maxYDic[minIndex] floatValue] > [obj floatValue]) { 76 | minIndex = key; 77 | } 78 | }]; 79 | // 根据最短列去计算itemX 80 | CGFloat itemX = self.sectionInset.left + (self.columnSpacing + itemWidth) * minIndex.intValue; 81 | CGFloat itemY = 0; 82 | if (self.column == 1) { 83 | // 一列情况 84 | if (indexPath.row == 0 ) { 85 | itemY = [self.maxYDic[minIndex] floatValue]; 86 | }else{ 87 | itemY = [self.maxYDic[minIndex] floatValue] + self.rowSpacing; 88 | } 89 | }else{ 90 | // 瀑布流多列情况 91 | // 第一行Cell不需要添加RowSpacing, 对应的indexPath.row = 0 && =1; 92 | if (indexPath.row == 0 || indexPath.row == 1) { 93 | itemY = [self.maxYDic[minIndex] floatValue]; 94 | }else{ 95 | itemY = [self.maxYDic[minIndex] floatValue] + self.rowSpacing; 96 | } 97 | } 98 | attributes.frame = CGRectMake(itemX, itemY , itemWidth, itemHeight); 99 | // 更新maxY 100 | self.maxYDic[minIndex] = @(CGRectGetMaxY(attributes.frame)); 101 | return attributes; 102 | } 103 | 104 | /// 3、返回数据源 105 | - (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect 106 | { 107 | return self.attributesArray; 108 | } 109 | 110 | /// 4、返回itemSize 111 | - (CGSize)collectionViewContentSize 112 | { 113 | __block NSNumber *maxIndex = @(0); 114 | // 找到最长的一列 115 | [self.maxYDic enumerateKeysAndObjectsUsingBlock:^(NSNumber *key, NSNumber *obj, BOOL * _Nonnull stop) { 116 | if ([self.maxYDic[maxIndex] floatValue] < [obj floatValue]) { 117 | maxIndex = key; 118 | } 119 | }]; 120 | CGFloat contentSizeY = [self.maxYDic[maxIndex] floatValue] + self.sectionInset.bottom; 121 | return CGSizeMake(self.collectionView.frame.size.width, contentSizeY); 122 | } 123 | 124 | // MARK: - Getter & Setter 125 | - (NSMutableDictionary *)maxYDic 126 | { 127 | if (!_maxYDic) { 128 | _maxYDic = [[NSMutableDictionary alloc] init]; 129 | } 130 | return _maxYDic; 131 | } 132 | 133 | - (NSMutableArray *)attributesArray 134 | { 135 | if (!_attributesArray) { 136 | _attributesArray = [NSMutableArray array]; 137 | } 138 | return _attributesArray; 139 | } 140 | 141 | - (CGFloat)itemWidth 142 | { 143 | CGFloat collectionViewWidth = self.collectionView.frame.size.width; 144 | if (collectionViewWidth == 0) { 145 | collectionViewWidth = [UIScreen mainScreen].bounds.size.width; 146 | } 147 | CGFloat itemWidth = (collectionViewWidth - self.sectionInset.left - self.sectionInset.right - (self.column) * self.columnSpacing * (self.column - 1)) / self.column; 148 | return itemWidth; 149 | } 150 | 151 | @end 152 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemo/main.m: -------------------------------------------------------------------------------- 1 | // 2 | // main.m 3 | // MPPlayerDemo 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | #import "AppDelegate.h" 11 | 12 | int main(int argc, char * argv[]) { 13 | NSString * appDelegateClassName; 14 | @autoreleasepool { 15 | // Setup code that might create autoreleased objects goes here. 16 | appDelegateClassName = NSStringFromClass([AppDelegate class]); 17 | } 18 | return UIApplicationMain(argc, argv, nil, appDelegateClassName); 19 | } 20 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemoTests/MPPlayerDemoTests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPPlayerDemoTests.m 3 | // MPPlayerDemoTests 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MPPlayerDemoTests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation MPPlayerDemoTests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | } 20 | 21 | - (void)tearDown { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | - (void)testExample { 26 | // This is an example of a functional test case. 27 | // Use XCTAssert and related functions to verify your tests produce the correct results. 28 | } 29 | 30 | - (void)testPerformanceExample { 31 | // This is an example of a performance test case. 32 | [self measureBlock:^{ 33 | // Put the code you want to measure the time of here. 34 | }]; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemoUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /MPPlayerDemo/MPPlayerDemoUITests/MPPlayerDemoUITests.m: -------------------------------------------------------------------------------- 1 | // 2 | // MPPlayerDemoUITests.m 3 | // MPPlayerDemoUITests 4 | // 5 | // Created by Maple on 2019/12/27. 6 | // Copyright © 2019 Maple. All rights reserved. 7 | // 8 | 9 | #import 10 | 11 | @interface MPPlayerDemoUITests : XCTestCase 12 | 13 | @end 14 | 15 | @implementation MPPlayerDemoUITests 16 | 17 | - (void)setUp { 18 | // Put setup code here. This method is called before the invocation of each test method in the class. 19 | 20 | // In UI tests it is usually best to stop immediately when a failure occurs. 21 | self.continueAfterFailure = NO; 22 | 23 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 24 | } 25 | 26 | - (void)tearDown { 27 | // Put teardown code here. This method is called after the invocation of each test method in the class. 28 | } 29 | 30 | - (void)testExample { 31 | // UI tests must launch the application that they test. 32 | XCUIApplication *app = [[XCUIApplication alloc] init]; 33 | [app launch]; 34 | 35 | // Use recording to get started writing UI tests. 36 | // Use XCTAssert and related functions to verify your tests produce the correct results. 37 | } 38 | 39 | - (void)testLaunchPerformance { 40 | if (@available(macOS 10.15, iOS 13.0, tvOS 13.0, *)) { 41 | // This measures how long it takes to launch your application. 42 | [self measureWithMetrics:@[XCTOSSignpostMetric.applicationLaunchMetric] block:^{ 43 | [[[XCUIApplication alloc] init] launch]; 44 | }]; 45 | } 46 | } 47 | 48 | @end 49 | -------------------------------------------------------------------------------- /MPPlayerDemo/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment the next line to define a global platform for your project 2 | # platform :ios, '9.0' 3 | 4 | target 'MPPlayerDemo' do 5 | # Uncomment the next line if you're using Swift or would like to use dynamic frameworks 6 | # use_frameworks! 7 | 8 | # Pods for MPPlayerDemo 9 | pod 'ZFPlayer' 10 | pod 'KTVHTTPCache' 11 | pod 'ZFPlayer/ControlView' 12 | 13 | target 'MPPlayerDemoTests' do 14 | inherit! :search_paths 15 | # Pods for testing 16 | end 17 | 18 | target 'MPPlayerDemoUITests' do 19 | inherit! :search_paths 20 | # Pods for testing 21 | end 22 | 23 | end 24 | -------------------------------------------------------------------------------- /MPPlayerDemo/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - CocoaAsyncSocket (7.6.4) 3 | - KTVCocoaHTTPServer (1.0.0): 4 | - CocoaAsyncSocket 5 | - KTVHTTPCache (2.0.1): 6 | - KTVCocoaHTTPServer 7 | - ZFPlayer (3.3.0): 8 | - ZFPlayer/Core (= 3.3.0) 9 | - ZFPlayer/ControlView (3.3.0): 10 | - ZFPlayer/Core 11 | - ZFPlayer/Core (3.3.0) 12 | 13 | DEPENDENCIES: 14 | - KTVHTTPCache 15 | - ZFPlayer 16 | - ZFPlayer/ControlView 17 | 18 | SPEC REPOS: 19 | trunk: 20 | - CocoaAsyncSocket 21 | - KTVCocoaHTTPServer 22 | - KTVHTTPCache 23 | - ZFPlayer 24 | 25 | SPEC CHECKSUMS: 26 | CocoaAsyncSocket: 694058e7c0ed05a9e217d1b3c7ded962f4180845 27 | KTVCocoaHTTPServer: df8d7b861e603ff8037e9b2138aca2563a6b768d 28 | KTVHTTPCache: 588c3eb16f6bd1e6fde1e230dabfb7bd4e490a4d 29 | ZFPlayer: 67de190f7aaad8ef4713aa2aef4ae170c663593d 30 | 31 | PODFILE CHECKSUM: eb64412a015159356b04c6cf9f71373bd54bf10e 32 | 33 | COCOAPODS: 1.8.4 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Demo演示的功能 2 | `提示:文末有相关的Demo下载链接` 3 | * ZFPlayer的列表播放 4 | * 使用KTVHTTPCache实现缓存(播放过的视频无需再下载) 5 | * 使用KTVHTTPCache实现预加载(可以实现秒播) 6 | * 自定义转场动画(实现无缝衔接的播放效果) 7 | * 瀑布流页面(双排列表展示,以及转场动画) 8 | 9 | gif演示: 10 | ![playerDemo.gif](https://upload-images.jianshu.io/upload_images/7789910-e659f2bab86829e7.gif?imageMogr2/auto-orient/strip) 11 | gif看不到的,可以到简书上看 12 | https://www.jianshu.com/p/3ffb26bf862c 13 | 14 | 15 | # 一、缓存+预加载功能 16 | 17 | ## 1、播放器mgr核心代码 18 | mgr实现ZFPlayerMediaPlayback协议,然后在初始化时,开启本地服务器 19 | ``` 20 | + (void)initialize 21 | { 22 | [KTVHTTPCache logSetConsoleLogEnable:NO]; 23 | NSError *error = nil; 24 | [KTVHTTPCache proxyStart:&error]; 25 | if (error) { 26 | NSLog(@"Proxy Start Failure, %@", error); 27 | } 28 | [KTVHTTPCache encodeSetURLConverter:^NSURL *(NSURL *URL) { 29 | // NSLog(@"URL Filter reviced URL : %@", URL); 30 | return URL; 31 | }]; 32 | [KTVHTTPCache downloadSetUnacceptableContentTypeDisposer:^BOOL(NSURL *URL, NSString *contentType) { 33 | return NO; 34 | }]; 35 | // 设置缓存最大容量 36 | [KTVHTTPCache cacheSetMaxCacheLength:1024 * 1024 * 1024]; 37 | } 38 | ``` 39 | 设置assetURL时,设置KTVHTTpCache为中间服务器,若该资源已缓存完毕,就无需代理,这个判断可以使已缓存的视频播放的更快 40 | ``` 41 | - (void)setAssetURL:(NSURL *)assetURL { 42 | if (self.player) [self stop]; 43 | // 如果有缓存,直接取本地缓存 44 | NSURL *url = [KTVHTTPCache cacheCompleteFileURLWithURL:assetURL]; 45 | if (url) { 46 | _assetURL = url; 47 | }else { 48 | // 设置代理 49 | _assetURL = [KTVHTTPCache proxyURLWithOriginalURL:assetURL]; 50 | } 51 | [self prepareToPlay]; 52 | } 53 | ``` 54 | 55 | 56 | ##2、播放器Player核心代码 57 | 创建playableProtocol,方便数据管理 58 | ``` 59 | /// 只有实现该协议的模型才能预加载 60 | @protocol XSTPlayable 61 | /// string 视频链接 62 | @property (nonatomic, copy) NSString *video_url; 63 | @end 64 | ``` 65 | 66 | 核心播放器为ZFPlayerController,为了方便管理,我们创建一个中间类包裹ZFPlayerController,且增加可以设置的预加载属性 67 | ``` 68 | @interface MPPlayerController : NSObject 69 | 70 | // 预加载上几条 71 | @property (nonatomic, assign) NSUInteger preLoadNum; 72 | /// 预加载下几条 73 | @property (nonatomic, assign) NSUInteger nextLoadNum; 74 | /// 预加载的的百分比,默认10% 75 | @property (nonatomic, assign) double preloadPrecent; 76 | /// 设置playableAssets后,马上预加载的条数 77 | @property (nonatomic, assign) NSUInteger initPreloadNum; 78 | /// set之后,先预加载几个 79 | @property (nonatomic, copy) NSArray> *playableArray; 80 | .... 81 | ``` 82 | 83 | 84 | ##3、预加载核心代码 85 | 预加载的时机是当前视频可以播放了,才进行预加载 86 | ``` 87 | - (void)playTheIndexPath:(NSIndexPath *)indexPath playable: (id)playable 88 | { 89 | // 播放前,先停止所有的预加载任务 90 | [self cancelAllPreload]; 91 | _currentPlayable = playable; 92 | [self.player playTheIndexPath:indexPath assetURL:[NSURL URLWithString:playable.video_url] scrollToTop:NO]; 93 | __weak typeof(self) weakSelf = self; 94 | self.playerReadyToPlay = ^(id _Nonnull asset, NSURL * _Nonnull assetURL) { 95 | [weakSelf preload: playable]; 96 | }; 97 | } 98 | ``` 99 | 预加载的规则是预加载当前视频的上2个,和下2个视频,逐个开启预加载,视频预加载(`核心类KTVHCDataLoader`)到10%就停止,然后开始下一个视频的预加载。这里要注意异步线程的操作,要加锁处理 100 | ``` 101 | /// 根据传入的模型,预加载上几个,下几个的视频 102 | - (void)preload: (id)resource 103 | { 104 | if (self.playableArray.count <= 1) 105 | return; 106 | if (_nextLoadNum == 0 && _preLoadNum == 0) 107 | return; 108 | NSInteger start = [self.playableArray indexOfObject:resource]; 109 | if (start == NSNotFound) 110 | return; 111 | [self cancelAllPreload]; 112 | NSInteger index = 0; 113 | for (NSInteger i = start + 1; i < self.playableArray.count && index < _nextLoadNum; i++) 114 | { 115 | index += 1; 116 | id model = self.playableArray[i]; 117 | XSTPreLoaderModel *preModel = [self getPreloadModel: model.video_url]; 118 | if (preModel) { 119 | @synchronized (self.preloadArr) { 120 | [self.preloadArr addObject: preModel]; 121 | } 122 | } 123 | } 124 | index = 0; 125 | for (NSInteger i = start - 1; i >= 0 && index < _preLoadNum; i--) 126 | { 127 | index += 1; 128 | id model = self.playableArray[i]; 129 | XSTPreLoaderModel *preModel = [self getPreloadModel: model.video_url]; 130 | if (preModel) { 131 | @synchronized (self.preloadArr) { 132 | [self.preloadArr addObject:preModel]; 133 | } 134 | } 135 | } 136 | [self processLoader]; 137 | } 138 | /// 取消所有的预加载 139 | - (void)cancelAllPreload 140 | { 141 | @synchronized (self.preloadArr) { 142 | if (self.preloadArr.count == 0) 143 | { 144 | return; 145 | } 146 | [self.preloadArr enumerateObjectsUsingBlock:^(XSTPreLoaderModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 147 | [obj.loader close]; 148 | }]; 149 | [self.preloadArr removeAllObjects]; 150 | } 151 | } 152 | 153 | - (XSTPreLoaderModel *)getPreloadModel: (NSString *)urlStr 154 | { 155 | if (!urlStr) 156 | return nil; 157 | // 判断是否已在队列中 158 | __block Boolean res = NO; 159 | @synchronized (self.preloadArr) { 160 | [self.preloadArr enumerateObjectsUsingBlock:^(XSTPreLoaderModel * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { 161 | if ([obj.url isEqualToString:urlStr]) 162 | { 163 | res = YES; 164 | *stop = YES; 165 | } 166 | }]; 167 | } 168 | if (res) 169 | return nil; 170 | NSURL *proxyUrl = [KTVHTTPCache proxyURLWithOriginalURL: [NSURL URLWithString:urlStr]]; 171 | KTVHCDataCacheItem *item = [KTVHTTPCache cacheCacheItemWithURL:proxyUrl]; 172 | double cachePrecent = 1.0 * item.cacheLength / item.totalLength; 173 | // 判断缓存已经超过10%了 174 | if (cachePrecent >= self.preloadPrecent) 175 | return nil; 176 | KTVHCDataRequest *req = [[KTVHCDataRequest alloc] initWithURL:proxyUrl headers:[NSDictionary dictionary]]; 177 | KTVHCDataLoader *loader = [KTVHTTPCache cacheLoaderWithRequest:req]; 178 | XSTPreLoaderModel *preModel = [[XSTPreLoaderModel alloc] initWithURL:urlStr loader:loader]; 179 | return preModel; 180 | } 181 | 182 | - (void)processLoader 183 | { 184 | @synchronized (self.preloadArr) { 185 | if (self.preloadArr.count == 0) 186 | return; 187 | XSTPreLoaderModel *model = self.preloadArr.firstObject; 188 | model.loader.delegate = self; 189 | [model.loader prepare]; 190 | } 191 | } 192 | 193 | /// 根据loader,移除预加载任务 194 | - (void)removePreloadTask: (KTVHCDataLoader *)loader 195 | { 196 | @synchronized (self.preloadArr) { 197 | XSTPreLoaderModel *target = nil; 198 | for (XSTPreLoaderModel *model in self.preloadArr) { 199 | if ([model.loader isEqual:loader]) 200 | { 201 | target = model; 202 | break; 203 | } 204 | } 205 | if (target) 206 | [self.preloadArr removeObject:target]; 207 | } 208 | } 209 | 210 | // MARK: - KTVHCDataLoaderDelegate 211 | - (void)ktv_loaderDidFinish:(KTVHCDataLoader *)loader 212 | { 213 | } 214 | - (void)ktv_loader:(KTVHCDataLoader *)loader didFailWithError:(NSError *)error 215 | { 216 | // 若预加载失败的话,就直接移除任务,开始下一个预加载任务 217 | [self removePreloadTask:loader]; 218 | [self processLoader]; 219 | } 220 | - (void)ktv_loader:(KTVHCDataLoader *)loader didChangeProgress:(double)progress 221 | { 222 | if (progress >= self.preloadPrecent) 223 | { 224 | [loader close]; 225 | [self removePreloadTask:loader]; 226 | [self processLoader]; 227 | } 228 | } 229 | ``` 230 | 231 | # 二、无缝衔接转场动画 232 | 这里我直接拿ZFPlayerDemo中的一个列表播放,一个抖音列表播放的例子进行演示,不熟悉转场动画的,建议自行先看看唐巧的https://blog.devtang.com/2016/03/13/iOS-transition-guide/了解,这里不多说,直接上核心代码。 233 | 234 | 1、首先必须实现代理`UINavigationControllerDelegate` 235 | ``` 236 | @interface MPDetailViewController : UIViewController 237 | ``` 238 | 239 | 2、传递player,startView,startImage等,并实现popback回调 240 | ``` 241 | - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath 242 | { 243 | ZFTableViewCell *cell = (ZFTableViewCell *)[tableView cellForRowAtIndexPath:indexPath]; 244 | NSIndexPath *currentIndexPath = [self.tableView indexPathForCell:cell]; 245 | // 点击的不是正在播放的cell,就先播放再跳转 246 | if ([currentIndexPath compare:self.tableView.zf_playingIndexPath] != NSOrderedSame) { 247 | [self.player stopCurrentPlayingCell]; 248 | self.tableView.zf_playingIndexPath = currentIndexPath; 249 | [self playTheVideoAtIndexPath:currentIndexPath scrollToTop:NO]; 250 | [self.player.currentPlayerManager.view layoutIfNeeded]; 251 | } 252 | self.tableView.zf_playingIndexPath = currentIndexPath; 253 | 254 | MPDetailViewController *vc = [[MPDetailViewController alloc] init]; 255 | vc.player = self.player; 256 | vc.index = indexPath.row; 257 | vc.startImage = cell.coverImageView.image; 258 | vc.startView = cell.coverImageView; 259 | vc.dataSource = [self.playableArray mutableCopy]; 260 | @weakify(self) 261 | vc.popbackBlock = ^{ 262 | @strongify(self) 263 | [self.player updateScrollViewPlayerToCell]; 264 | [self.player.currentPlayerManager play]; 265 | }; 266 | self.navigationController.delegate = vc; 267 | [self.navigationController pushViewController:vc animated:YES]; 268 | } 269 | ``` 270 | 271 | 3、实现`UIViewControllerAnimatedTransitioning`协议 272 | ``` 273 | /// 用于视频信息流的转场动画 274 | @interface MPTransition : NSObject 275 | 276 | /** 277 | 初始化动画 278 | 279 | @param duration 动画时长 280 | @param startView 开始视图 281 | @param startImage 开始图片 282 | @param player 播放器 283 | @param operation 动画形式 284 | @param completion 动画完成block 285 | @return 动画实例 286 | */ 287 | + (instancetype)animationWithDuration:(NSTimeInterval)duration 288 | startView:(UIView *)startView 289 | startImage:(UIImage *)startImage 290 | player: (MPPlayerController *)player 291 | operation:(UINavigationControllerOperation)operation 292 | completion:(void(^)(void))completion; 293 | 294 | @end 295 | ``` 296 | 4、分别实现push,pop的转场动画 297 | ``` 298 | @interface MPTransition() 299 | 300 | @property (nonatomic, strong) UIView *startView; 301 | @property (nonatomic, strong) UIImage *startImage; 302 | @property (nonatomic, assign) NSTimeInterval duration; 303 | @property (nonatomic, strong) MPPlayerController *player; 304 | @property (nonatomic, assign) UINavigationControllerOperation operation; 305 | @property (nonatomic, assign) void(^completion)(void); 306 | @property (nonatomic, strong) UIView *effectView; 307 | 308 | @end 309 | 310 | @implementation MPTransition 311 | 312 | + (instancetype)animationWithDuration:(NSTimeInterval)duration 313 | startView:(UIView *)startView 314 | startImage:(UIImage *)startImage 315 | player: (MPPlayerController *)player 316 | operation:(UINavigationControllerOperation)operation 317 | completion:(void(^)(void))completion 318 | { 319 | MPTransition *animation = [MPTransition new]; 320 | animation.player = player; 321 | animation.duration = duration; 322 | animation.startView = startView; 323 | animation.startImage = startImage; 324 | animation.operation = operation; 325 | animation.completion = completion; 326 | 327 | return animation; 328 | } 329 | 330 | - (void)animateTransition:(id)transitionContext { 331 | if (self.operation == UINavigationControllerOperationPush) { 332 | [self startPushAnimation: transitionContext]; 333 | }else { 334 | [self startPopAnimation: transitionContext]; 335 | } 336 | } 337 | 338 | - (void)startPushAnimation:(id)transitionContext 339 | { 340 | // 获取 fromView 和 toView 341 | UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 342 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 343 | 344 | // 添加到动画容器视图中 345 | [[transitionContext containerView] addSubview:fromView]; 346 | [[transitionContext containerView] addSubview:toView]; 347 | 348 | UIImageView *bgImgView = [[UIImageView alloc] initWithFrame:fromView.bounds]; 349 | bgImgView.contentMode = UIViewContentModeScaleAspectFill; 350 | bgImgView.image = self.startImage; 351 | UIView *colorCover = [[UIView alloc] init]; 352 | colorCover.backgroundColor = [UIColor colorWithRed:0 green:0 blue:0 alpha:0.4]; 353 | colorCover.frame = fromView.bounds; 354 | [bgImgView addSubview:colorCover]; 355 | [bgImgView addSubview:self.effectView]; 356 | [[transitionContext containerView] addSubview:bgImgView]; 357 | 358 | // 创建player容器 359 | CGRect winFrame = CGRectZero; 360 | if (self.startView) { 361 | winFrame = [self.startView convertRect:self.startView.bounds toView:nil]; 362 | } 363 | 364 | UIImageView *playerContainer = [[UIImageView alloc] initWithFrame:winFrame]; 365 | playerContainer.image = self.startImage; 366 | playerContainer.contentMode = UIViewContentModeScaleAspectFit; 367 | [[transitionContext containerView] addSubview:playerContainer]; 368 | if (self.player) { 369 | self.player.currentPlayerManager.scalingMode = self.player.videoFlowScalingMode; 370 | self.player.currentPlayerManager.view.backgroundColor = [UIColor clearColor]; 371 | [self.player updateNoramlPlayerWithContainerView:playerContainer]; 372 | } 373 | CGFloat bottomOffset = iPhoneX ? 83 : 0; 374 | NSTimeInterval duration = [self transitionDuration:transitionContext]; 375 | CGRect targetFrame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight - bottomOffset); 376 | 377 | toView.alpha = 0.0f; 378 | bgImgView.alpha = 0; 379 | [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 380 | // mask 渐变效果 381 | bgImgView.alpha = 1; 382 | playerContainer.frame = targetFrame; 383 | } completion:^(BOOL finished) { 384 | toView.alpha = 1.0f; 385 | // 移除临时视图 386 | [bgImgView removeFromSuperview]; 387 | [playerContainer removeFromSuperview]; 388 | // 结束动画 389 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 390 | if (self.completion) { 391 | self.completion(); 392 | } 393 | }]; 394 | } 395 | 396 | - (void)startPopAnimation: (id)transitionContext 397 | { 398 | // 获取 fromView 和 toView 399 | UIView *fromView = [transitionContext viewForKey:UITransitionContextFromViewKey]; 400 | UIView *toView = [transitionContext viewForKey:UITransitionContextToViewKey]; 401 | self.player.currentPlayerManager.view.backgroundColor = [UIColor blackColor]; 402 | 403 | // 添加到动画容器视图中 404 | UIView *container = [transitionContext containerView]; 405 | [container addSubview:toView]; 406 | [container addSubview:fromView]; 407 | container.backgroundColor = [UIColor clearColor]; 408 | 409 | // 添加动画临时视图到 fromView 410 | CGFloat bottomOffset = iPhoneX ? 83 : 0; 411 | CGRect normalFrame = CGRectMake(0, 0, ZFPlayer_ScreenWidth, ZFPlayer_ScreenHeight - bottomOffset); 412 | CGRect winFrame = CGRectZero; 413 | if (self.startView) { 414 | winFrame = [self.startView convertRect:self.startView.bounds toView:nil]; 415 | } 416 | 417 | // 显示图片 418 | UIImageView *imageView = [[UIImageView alloc] initWithFrame:normalFrame]; 419 | imageView.contentMode = UIViewContentModeScaleAspectFit; 420 | imageView.clipsToBounds = YES; 421 | imageView.image = self.startImage; 422 | if (self.player) { 423 | // pop回去的时候,设置回原来的scalingMode 424 | self.player.currentPlayerManager.scalingMode = ZFPlayerScalingModeAspectFill; 425 | [self.player updateNoramlPlayerWithContainerView:imageView]; 426 | } 427 | 428 | [container addSubview:imageView]; 429 | 430 | toView.alpha = 1; 431 | fromView.alpha = 1; 432 | NSTimeInterval duration = [self transitionDuration:transitionContext]; 433 | 434 | [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{ 435 | imageView.frame = winFrame; 436 | fromView.alpha = 0; 437 | } completion:^(BOOL finished) { 438 | // 移除临时视图 439 | [imageView removeFromSuperview]; 440 | 441 | // 结束动画 442 | [transitionContext completeTransition:![transitionContext transitionWasCancelled]]; 443 | 444 | if (self.completion) { 445 | self.completion(); 446 | } 447 | }]; 448 | } 449 | 450 | - (NSTimeInterval)transitionDuration:(id)transitionContext { 451 | return self.duration; 452 | } 453 | 454 | - (UIView *)effectView { 455 | if (!_effectView) { 456 | if (@available(iOS 8.0, *)) { 457 | UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleDark]; 458 | _effectView = [[UIVisualEffectView alloc] initWithEffect:effect]; 459 | } else { 460 | UIToolbar *effectView = [[UIToolbar alloc] init]; 461 | effectView.barStyle = UIBarStyleBlackTranslucent; 462 | _effectView = effectView; 463 | } 464 | } 465 | return _effectView; 466 | } 467 | 468 | @end 469 | 470 | ``` 471 | 472 | #三、相关链接 473 | * 瀑布流 474 | https://www.jianshu.com/p/7976739e9034 475 | * Demo链接 476 | https://github.com/maple1994/MPPlayerDemo 477 | * ZFPlayer 478 | https://github.com/renzifeng/ZFPlayer 479 | * KTVHttpCache 480 | https://github.com/ChangbaDevs/KTVHTTPCache 481 | * 转场动画 482 | https://blog.devtang.com/2016/03/13/iOS-transition-guide/ 483 | -------------------------------------------------------------------------------- /playerDemo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maple1994/MPPlayerDemo/5d7cb8d8310a915df2856c4cc3e2881c3f9408a1/playerDemo.gif --------------------------------------------------------------------------------