有那么一些時(shí)候,我們只需要簡(jiǎn)單的播放一些小視頻雾叭,本地的或者網(wǎng)上的資源悟耘,不需要各種炫酷的效果,不需要自己各種控制织狐,只是想安安靜靜的播放完暂幼,退出!網(wǎng)上各種開源的封裝的AVPlayer的開源庫(kù)移迫,各有千秋旺嬉,但是集成進(jìn)來(lái)有感覺(jué)動(dòng)靜太大了,大把大把的控件和控制代理等等厨埋,頭都大了邪媳!對(duì)于我這種菜逼,慌得一批~~所以我就在系統(tǒng)提供的AVPlayerViewController動(dòng)起了手腳荡陷!
AVPlayerViewController的最簡(jiǎn)單使用
#import "ViewController.h"
#import <AVKit/AVKit.h>
@interface ViewController ()
@property (nonatomic, strong) NSString *videoUrl;
@property (nonatomic, strong)AVPlayerViewController *playerVC;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self.videoUrl = [[NSBundle mainBundle] pathForResource:@"guideMovie1" ofType:@"mov"];
self.videoUrl = @"http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4";
/*
因?yàn)槭?http 的鏈接雨效,所以要去 info.plist里面設(shè)置
App Transport Security Settings
Allow Arbitrary Loads = YES
*/
self.playerVC = [[AVPlayerViewController alloc] init];
self.playerVC.player = [AVPlayer playerWithURL:[self.videoUrl hasPrefix:@"http"] ? [NSURL URLWithString:self.videoUrl]:[NSURL fileURLWithPath:self.videoUrl]];
self.playerVC.view.frame = self.view.bounds;
self.playerVC.showsPlaybackControls = YES;
//self.playerVC.entersFullScreenWhenPlaybackBegins = YES;//開啟這個(gè)播放的時(shí)候支持(全屏)橫豎屏哦
//self.playerVC.exitsFullScreenWhenPlaybackEnds = YES;//開啟這個(gè)所有 item 播放完畢可以退出全屏
[self.view addSubview:self.playerVC.view];
if (self.playerVC.readyForDisplay) {
[self.playerVC.player play];
}
}
@end
就是這么簡(jiǎn)單,我們就可以播放網(wǎng)絡(luò)或者本地視頻啦废赞,簡(jiǎn)潔大氣上檔次徽龟,還是暗黑風(fēng)格哦,如果項(xiàng)目開啟了屏幕方向唉地,自動(dòng)支持橫豎屏切換据悔,美滋滋!上兩張圖來(lái)占占篇幅哈T稀屠尊!
但是,但是耕拷!我們有沒(méi)有發(fā)現(xiàn)讼昆,它沒(méi)有退出按鈕,這叫我如何退出呢骚烧!這怎么難倒我浸赫,加個(gè)導(dǎo)航控制器嵌套,so easy~~這時(shí)候你又會(huì)發(fā)現(xiàn)赃绊,播放界面的按鈕被蓋住了(肯定有人會(huì)說(shuō)既峡,進(jìn)來(lái)的時(shí)候設(shè)置導(dǎo)航欄隱藏就好啦,然后播放完顯示導(dǎo)航欄不就好了碧查。运敢。校仑。)接著往下看
那么我們來(lái)監(jiān)聽(tīng)播放狀態(tài)來(lái)控制導(dǎo)航欄的顯示和隱藏吧!
我們?cè)?code>viewDidload里面增加這個(gè)監(jiān)聽(tīng),這個(gè) block 監(jiān)聽(tīng)方式是我學(xué)習(xí)各位大神寫的一個(gè)分類传惠,不必自己釋放 observer
//添加監(jiān)聽(tīng)播放狀態(tài)
[self HF_addNotificationForName:AVPlayerItemDidPlayToEndTimeNotification block:^(NSNotification *notification) {
NSLog(@"我播放結(jié)束了迄沫!");
[self.navigationController setNavigationBarHidden:NO animated:YES];
}];
if (self.playerVC.readyForDisplay) {
[self.playerVC.player play];
}
在viewDidAppear
里面隱藏導(dǎo)航欄
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self.navigationController setNavigationBarHidden:YES animated:YES];
}
這樣看起來(lái)蠻不錯(cuò)的,達(dá)到我們預(yù)期效果了卦方,就是進(jìn)去隱藏導(dǎo)航欄羊瘩,播放結(jié)束顯示導(dǎo)航欄!
但是這個(gè)視頻短盼砍,如果是很長(zhǎng)的怎么辦尘吗?等到看完才可以退出么,那就太坑爹了浇坐,有木有睬捶!辣么有沒(méi)有機(jī)制監(jiān)聽(tīng)到AVPlayerViewController點(diǎn)擊播放界面顯示工具欄的時(shí)候一并顯示導(dǎo)航欄呢?這時(shí)候就要借助著名的AOP框架Aspects
了!在做這件事之前吗跋,我們先看下AVPlayerViewController的視圖層級(jí)和手勢(shì)數(shù)組了侧戴!
首先看下面一張圖,好好利用視圖層級(jí)分析器跌宛,可以看透很多東西的實(shí)現(xiàn)原理哦酗宋!
而下面這張圖是我們需要監(jiān)聽(tīng)的音量控制的層級(jí)圖
從上圖,我們可以知道單擊和雙擊手勢(shì)是添加到了AVPlayerViewControllerContentView上疆拘,那我們?cè)趺磾r截它做事情呢蜕猫?接下來(lái)看我的:
#import "ViewController.h"
#import <AVKit/AVKit.h>
#import "NSObject+BlockObserver.h"
#import <Aspects/Aspects.h>
@interface ViewController ()
@property (nonatomic, strong) NSString *videoUrl;
@property (nonatomic, strong)AVPlayerViewController *playerVC;
//增加兩個(gè)屬性先
//記錄音量控制的父控件,控制它隱藏顯示的 view
@property (nonatomic, weak)UIView *volumeSuperView;
//記錄我們 hook 的對(duì)象信息
@property (nonatomic, strong)id<AspectToken>hookAVPlaySingleTap;
@end
先增加兩個(gè)屬性哎迄,用來(lái)記錄我們需要操作的 view 和 hook 的對(duì)象信息回右!然后在viewDidload
里面增加以下代碼:
//首先獲取我們的手勢(shì)真正的執(zhí)行類 UIGestureRecognizerTarget
//然后手勢(shì)觸發(fā)的方法 selector 為:_sendActionWithGestureRecognizer:
Class UIGestureRecognizerTarget = NSClassFromString(@"UIGestureRecognizerTarget");
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
_hookAVPlaySingleTap = [UIGestureRecognizerTarget aspect_hookSelector:@selector(_sendActionWithGestureRecognizer:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo>info,UIGestureRecognizer *gest){
if (gest.numberOfTouches == 1) {
//AVVolumeButtonControl
if (!self.volumeSuperView) {
UIView *view = [gest.view findViewByClassName:@"AVVolumeButtonControl"];
if (view) {
while (view.superview) {
view = view.superview;
if ([view isKindOfClass:[NSClassFromString(@"AVTouchIgnoringView") class]]) {
self.volumeSuperView = view;
// [view addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[view HF_addObserverForKeyPath:@"hidden" block:^(__weak id object, id oldValue, id newValue) {
NSLog(@"newValue ==%@",newValue);
BOOL isHidden = [(NSNumber *)newValue boolValue];//記得要轉(zhuǎn)換哦,不然沒(méi)效果漱挚!
dispatch_async(dispatch_get_main_queue(), ^{
[self.navigationController setNavigationBarHidden:isHidden animated:YES];
});
}
}
}
}
}
} error:nil];
#pragma clang diagnostic pop
if (self.playerVC.readyForDisplay) {
[self.playerVC.player play];
}
其中[gest.view findViewByClassName:@"AVVolumeButtonControl"]
方法的實(shí)現(xiàn)如下:
- (UIView *)findViewByClassName:(NSString *)className
{
UIView *view;
if ([NSStringFromClass(self.class) isEqualToString:className]) {
return self;
} else {
for (UIView *child in self.subviews) {
view = [child findViewByClassName:className];
if (view != nil) break;
}
}
return view;
}
設(shè)置完翔烁,我們需要在 viewDidDisappear
里面去釋放我們 hook 的對(duì)象,防止反復(fù)添加 hook 和引用的問(wèn)題!
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
[self.navigationController setNavigationBarHidden:NO animated:YES];
[self.hookAVPlaySingleTap remove];
}
完事旨涝,我們來(lái)驗(yàn)證一下蹬屹!看看能不能達(dá)到我們的預(yù)期效果,點(diǎn)擊播放界面白华,實(shí)現(xiàn)播放工具欄和導(dǎo)航欄的同步顯示隱藏!我們可以預(yù)覽一下效果:
當(dāng)然慨默,我能寫出來(lái),就說(shuō)明我驗(yàn)證過(guò)啦弧腥!不過(guò)厦取,在這個(gè)基礎(chǔ)上,我肯定是想做的更自然一點(diǎn)點(diǎn)管搪,更高大上一點(diǎn)點(diǎn)啦虾攻!所以我會(huì)自定義一個(gè)控件添加到播放控制器的 view 上铡买,同一風(fēng)格,做到真正的全屏播放台谢,拋棄導(dǎo)航欄寻狂!因此,我們需要再添加一個(gè)控件屬性
//增加一個(gè)關(guān)閉按鈕
@property (nonatomic, strong) UIControl *closeControl;
//懶加載
- (UIControl *)closeControl
{
if (!_closeControl) {
_closeControl = [[UIControl alloc] init];
[_closeControl addTarget:self action:@selector(dimissSelf) forControlEvents:UIControlEventTouchUpInside];
_closeControl.backgroundColor = [UIColor colorWithRed:0.14 green:0.14 blue:0.14 alpha:0.8];
_closeControl.tintColor = [UIColor colorWithWhite:1 alpha:0.55];
NSBundle *bundle = [NSBundle bundleForClass:[self class]];
UIImage *normalImage = [UIImage imageNamed:@"closeAV" inBundle:bundle compatibleWithTraitCollection:nil];
[_closeControl.layer setContents:(id)normalImage.CGImage];
_closeControl.layer.contentsGravity = kCAGravityCenter;
_closeControl.layer.cornerRadius = 17;
_closeControl.layer.masksToBounds = YES;
}
return _closeControl;
}
- (void)dimissSelf
{
if (self.navigationController.viewControllers.count >1) {
[self.navigationController popViewControllerAnimated:YES];
}
else
{
[self dismissViewControllerAnimated:YES completion:nil];
}
}
然后在viewDidLoad
里面修改代碼如下朋沮,主要是注釋掉導(dǎo)航欄顯示和隱藏的代碼:
- (void)viewDidLoad {
[super viewDidLoad];
// self.videoUrl = [[NSBundle mainBundle] pathForResource:@"guideMovie1" ofType:@"mov"];
self.videoUrl = @"http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4";
/*
因?yàn)槭?http 的鏈接,所以要去 info.plist里面設(shè)置
App Transport Security Settings
Allow Arbitrary Loads = YES
*/
self.playerVC = [[AVPlayerViewController alloc] init];
self.playerVC.player = [AVPlayer playerWithURL:[self.videoUrl hasPrefix:@"http"] ? [NSURL URLWithString:self.videoUrl]:[NSURL fileURLWithPath:self.videoUrl]];
self.playerVC.view.frame = self.view.bounds;
self.playerVC.showsPlaybackControls = YES;
//self.playerVC.entersFullScreenWhenPlaybackBegins = YES;//開啟這個(gè)播放的時(shí)候支持(全屏)橫豎屏哦
//self.playerVC.exitsFullScreenWhenPlaybackEnds = YES;//開啟這個(gè)所有 item 播放完畢可以退出全屏
[self.view addSubview:self.playerVC.view];
//添加監(jiān)聽(tīng)播放狀態(tài)
[self HF_addNotificationForName:AVPlayerItemDidPlayToEndTimeNotification block:^(NSNotification *notification) {
NSLog(@"我播放結(jié)束了缀壤!");
// [self.navigationController setNavigationBarHidden:NO animated:YES];
}];
Class UIGestureRecognizerTarget = NSClassFromString(@"UIGestureRecognizerTarget");
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wundeclared-selector"
_hookAVPlaySingleTap = [UIGestureRecognizerTarget aspect_hookSelector:@selector(_sendActionWithGestureRecognizer:) withOptions:AspectPositionBefore usingBlock:^(id<AspectInfo>info,UIGestureRecognizer *gest){
if (gest.numberOfTouches == 1) {
//AVVolumeButtonControl
if (!self.volumeSuperView) {
UIView *view = [gest.view findViewByClassName:@"AVVolumeButtonControl"];
if (view) {
while (view.superview) {
view = view.superview;
if ([view isKindOfClass:[NSClassFromString(@"AVTouchIgnoringView") class]]) {
self.volumeSuperView = view;
// [view addObserver:self forKeyPath:@"hidden" options:NSKeyValueObservingOptionNew|NSKeyValueObservingOptionOld context:nil];
[view HF_addObserverForKeyPath:@"hidden" block:^(__weak id object, id oldValue, id newValue) {
NSLog(@"newValue ==%@",newValue);
BOOL isHidden = [(NSNumber *)newValue boolValue];
dispatch_async(dispatch_get_main_queue(), ^{
// [self.navigationController setNavigationBarHidden:isHidden animated:YES];
[self.closeControl setHidden:isHidden];
});
}];
break;
}
}
}
}
}
} error:nil];
#pragma clang diagnostic pop
//這里必須監(jiān)聽(tīng)到準(zhǔn)備好開始播放了樊拓,才把按鈕添加上去(系統(tǒng)控件的懶加載機(jī)制,我們才能獲取到合適的 view 去添加)塘慕,不然很突兀筋夏!
[self.playerVC.player HF_addObserverForKeyPath:@"status" block:^(__weak id object, id oldValue, id newValue) {
AVPlayerStatus status = [newValue integerValue];
if (status == AVPlayerStatusReadyToPlay) {
UIView *avTouchIgnoringView = self->_playerVC.view;
[avTouchIgnoringView addSubview:self.closeControl];
//這里判斷是否劉海屏,不同機(jī)型的放置位置不一樣图呢!
BOOL ishairScreen = [self.view isHairScreen];
CGFloat margin = ishairScreen ?90:69;
[self.closeControl mas_makeConstraints:^(MASConstraintMaker *make) {
make.right.mas_equalTo(avTouchIgnoringView).offset(-margin);
make.top.mas_equalTo(avTouchIgnoringView).offset(ishairScreen ?27:6);
make.width.mas_equalTo(60);
make.height.mas_equalTo(47);
}];
[avTouchIgnoringView setNeedsLayout];
}
}];
if (self.playerVC.readyForDisplay) {
[self.playerVC.player play];
}
}
//別忘了釋放資源
- (void)dealloc
{
self.playerVC = nil;
}
來(lái)來(lái)來(lái)条篷,上圖:
至此,大功告成了蛤织,不過(guò)同步顯示和隱藏過(guò)程需要大家自己去摸索合適的動(dòng)畫了赴叹,我總是踏不準(zhǔn)點(diǎn)!當(dāng)然指蚜,橫豎屏和強(qiáng)制橫屏的方案我這里也大概提一下吧乞巧,主要思路是監(jiān)聽(tīng)屏幕的旋轉(zhuǎn)通知:
//獲取設(shè)備旋轉(zhuǎn)方向的通知,即使關(guān)閉了自動(dòng)旋轉(zhuǎn),一樣可以監(jiān)測(cè)到設(shè)備的旋轉(zhuǎn)方向
[[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
//旋轉(zhuǎn)屏幕通知
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onDeviceOrientationChange:)
name:UIDeviceOrientationDidChangeNotification
object:nil
];
/**
* 旋轉(zhuǎn)屏幕通知
*/
- (void)onDeviceOrientationChange:(NSNotification *)notification{
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
UIInterfaceOrientation interfaceOrientation = (UIInterfaceOrientation)orientation;
switch (interfaceOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:{
}
break;
case UIInterfaceOrientationPortrait:
[self changeOrientation:UIInterfaceOrientationPortrait];
}
break;
case UIInterfaceOrientationLandscapeLeft:
[self changeOrientation:UIInterfaceOrientationLandscapeLeft];
}
break;
case UIInterfaceOrientationLandscapeRight:{
[self changeOrientation:UIInterfaceOrientationLandscapeRight];
}
break;
default:
break;
}
}
-(void)changeOrientation:(UIInterfaceOrientation)orientation{
if (orientation == UIInterfaceOrientationPortrait) {
[self setNeedsStatusBarAppearanceUpdate];
[self forceOrientationPortrait];
}else{
[self setNeedsStatusBarAppearanceUpdate];
[self forceOrientationLandscape];
}
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
//刷新
[UIViewController attemptRotationToDeviceOrientation];
}
//強(qiáng)制橫屏
- (void)forceOrientationLandscape
{
[[UIApplication sharedApplication].delegate.isForceLandscape = YES;
[[UIApplication sharedApplication].delegate.isForcePortrait = NO;
[[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:self.view.window];
}
//強(qiáng)制豎屏
- (void)forceOrientationPortrait
{
[[UIApplication sharedApplication].delegate.isForceLandscape = NO;
[[UIApplication sharedApplication].delegate.isForcePortrait = YES;
[[UIApplication sharedApplication].delegate application:[UIApplication sharedApplication] supportedInterfaceOrientationsForWindow:self.view.window];
}
在 AppDelegate.h 里面添加以下屬性
#import <UIKit/UIKit.h>
@interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
//添加旋轉(zhuǎn)需要的屬性
@property (nonatomic, assign) BOOL isForceLandscape;
@property (nonatomic, assign) BOOL isForcePortrait;
@end
在 AppDelegate.m 里面實(shí)現(xiàn)以下方法
-(UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{
if (self.isForceLandscape) {
return UIInterfaceOrientationMaskLandscape;
}else if (self.isForcePortrait){
return UIInterfaceOrientationMaskPortrait;
}
return UIInterfaceOrientationMaskPortrait;
}
今天的小菜逼裝完了,總感覺(jué)自己在這個(gè)行業(yè)里瑟瑟發(fā)抖摊鸡,找不到上岸的路绽媒!