搞透AVPlayerViewController,擺出我想要的姿勢(shì)

有那么一些時(shí)候,我們只需要簡(jiǎn)單的播放一些小視頻,本地的或者網(wǎng)上的資源,不需要各種炫酷的效果,不需要自己各種控制,只是想安安靜靜的播放完,退出!網(wǎng)上各種開(kāi)源的封裝的AVPlayer的開(kāi)源庫(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;//開(kāi)啟這個(gè)播放的時(shí)候支持(全屏)橫豎屏哦
//self.playerVC.exitsFullScreenWhenPlaybackEnds = YES;//開(kāi)啟這個(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)目開(kāi)啟了屏幕方向,自動(dòng)支持橫豎屏切換,美滋滋!上兩張圖來(lái)占占篇幅哈??!

簡(jiǎn)單播放豎屏界面

簡(jiǎn)單播放橫屏界面

但是,但是!我們有沒(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í)各位大神寫(xiě)的一個(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)原理哦!

視圖層級(jí)和手勢(shì)數(shù)組

而下面這張圖是我們需要監(jiān)聽(tīng)的音量控制的層級(jí)圖
音量控制??的層級(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ù)覽一下效果:


hidden.gif

當(dāng)然,我能寫(xiě)出來(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;//開(kāi)啟這個(gè)播放的時(shí)候支持(全屏)橫豎屏哦
    //self.playerVC.exitsFullScreenWhenPlaybackEnds = YES;//開(kāi)啟這個(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)備好開(kāi)始播放了,才把按鈕添加上去(系統(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),上圖:


close按鈕和原生工具欄同步顯示和隱藏

至此,大功告成了,不過(guò)同步顯示和隱藏過(guò)程需要大家自己去摸索合適的動(dòng)畫(huà)了,我總是踏不準(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ā)抖,找不到上岸的路!

demo地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 發(fā)現(xiàn) 關(guān)注 消息 iOS 第三方庫(kù)、插件、知名博客總結(jié) 作者大灰狼的小綿羊哥哥關(guān)注 2017.06.26 09:4...
    肇東周閱讀 15,172評(píng)論 4 61
  • 做任何事情,都要講求方式方法,這樣我們才能把事情做到完美。
    閆玉蓮閱讀 165評(píng)論 0 0
  • 粗細(xì)對(duì)比不夠強(qiáng)烈,提按一直都是問(wèn)題,楷書(shū)是,草書(shū)也是的。繼續(xù)努力加油吧。每次解決一個(gè)問(wèn)題,然后再考慮其他的。
    余一浳閱讀 538評(píng)論 0 1
  • 輕量級(jí)線程:協(xié)程 在常用的并發(fā)模型中,多進(jìn)程、多線程、分布式是最普遍的,不過(guò)近些年來(lái)逐漸有一些語(yǔ)言以first-c...
    Tenderness4閱讀 6,489評(píng)論 2 10
  • 人生會(huì)有迷茫,人生會(huì)遇彷徨,迷茫只因選擇,彷徨會(huì)遭挫折!人生十字路口,更多的時(shí)候,僅僅需要我們不忘初心,堅(jiān)定信心,...
    沐風(fēng)聽(tīng)雨wj閱讀 614評(píng)論 0 0

友情鏈接更多精彩內(nèi)容