全屏方案及各自的优缺点

根据一篇博客以及人家的思路重新写了这个 demo 博客地址见本文末尾。优缺点和实现思路都写在了 demo 中,demo见 GitHub

首先需要新建播放视频所在的 View 在此我以一个 imageView 代替, 其代码如下


#import <UIKit/UIKit.h>

typedef NS_ENUM(NSInteger, PlayViewState)
{
    PlayViewStateSmallScreen, // 小屏状态
    PlayViewStateAnimating,   // 动画中的状态
    PlayViewStateFullScreen   // 全屏状态
};

@interface PlayView : UIImageView

@property (nonatomic, assign)PlayViewState state;
@property (nonatomic, weak)UIView * playViewParent; // 记录父视图
@property (nonatomic, assign)CGRect playViewFrame;  // 记录在父视图中的 frame

- (instancetype)initWithFrame:(CGRect)frame;
@end

#import "PlayView.h"

@implementation PlayView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        self.image = [UIImage imageNamed:@"timg"];
        self.contentMode = UIViewContentModeScaleAspectFit;
        self.userInteractionEnabled = YES;
    }
    return self;
}
@end

第一种实现方案: 播放器所在的 View 的父视图在 window 和控制器的 view之间相互切换

#import <UIKit/UIKit.h>

@interface FirstViewController : UIViewController

@end

#import "FirstViewController.h"
#import "Header.h"
#import "PlayView.h"
@interface FirstViewController ()
@property (nonatomic, weak)PlayView * playView;
@end

@implementation FirstViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"First";
    self.view.backgroundColor = [UIColor whiteColor];
    PlayView * playView = [[PlayView alloc] initWithFrame:CGRectMake(0, 64, KSCREEN_WIDTH, KSCREEN_WIDTH * 9 / 16)];
    playView.backgroundColor = [UIColor blackColor];
    self.playView = playView;
    
    [self.view addSubview:playView];
    

    UITapGestureRecognizer * tap =[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHundle:)];
    [self.playView addGestureRecognizer:tap];
}

/*
 * 思路:从小屏进入全屏时,将播放器所在的view放置到window上,用transform的方式做一个旋转动画,最终让view完全覆盖window。 从全屏回到小屏时,用transform的方式做旋转动画,最终让播放器所在的view回到原先的parentView
 
 * 优缺点:这种方式在实现上相对简单,因为仅仅旋转了播放器所在的view,view controller和device的方向均始终为竖直(portrait)。但最大的问题就是全屏时status bar的方向依然是竖直的,虽然之前通过全屏时隐藏statusBar来掩盖了这个问题,但这同时导致了用户无法在视频全屏时看到时间、网络情况等,体验有待改善。
 */


- (void)tapHundle:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded) {
        if (self.playView.state == PlayViewStateFullScreen) {
            [self exitFullScreen]; // 小屏
        }
        if (self.playView.state == PlayViewStateSmallScreen) {
            [self enterFullScreen]; // 全屏
        }
    }
}




/*
 * 全屏
 */
- (void)enterFullScreen
{
    if (self.playView.state != PlayViewStateSmallScreen)
    {
        return;
    }
    self.playView.state = PlayViewStateAnimating;
    self.playView.playViewParent = self.playView.superview;
    self.playView.playViewFrame = self.playView.frame;
    
    
    // 计算控制器的 View 上的 palyView的 frame 相对于 window的 frame
    CGRect rectInWindow = [self.view convertRect:self.playView.frame toView:[UIApplication sharedApplication].keyWindow];
    [self.playView removeFromSuperview];
    self.playView.frame = rectInWindow;
    [[UIApplication sharedApplication].keyWindow addSubview:self.playView];
    __weak typeof(self) weakSelf = self;
    [UIView animateWithDuration:0.5 animations:^{
        weakSelf.playView.transform = CGAffineTransformMakeRotation(M_PI_2     );
        weakSelf.playView.bounds = CGRectMake(0, 0, CGRectGetHeight(weakSelf.playView.playViewParent.bounds), CGRectGetWidth(weakSelf.playView.playViewParent.bounds));
        weakSelf.playView.center = CGPointMake(CGRectGetMidX(weakSelf.playView.superview.bounds), CGRectGetMidY(weakSelf.playView.superview.bounds));
        
    } completion:^(BOOL finished) {
        weakSelf.playView.state = PlayViewStateFullScreen;
    }];
    
}
/*
 * 退出全屏
 */
- (void)exitFullScreen
{
    if (self.playView.state != PlayViewStateFullScreen ) {
        return;
    }
    self.playView.state = PlayViewStateAnimating;
    __weak typeof(self) weakSelf = self;
    // 计算播放器父View 上的 playView 的 frame 相对于窗口的 frame
    CGRect frame = [self.playView.playViewParent convertRect:self.playView.playViewFrame toView:[UIApplication sharedApplication].keyWindow];
    [UIView animateWithDuration:0.5 animations:^{
        
        weakSelf.playView.transform = CGAffineTransformIdentity;
        weakSelf.playView.frame = frame;
    } completion:^(BOOL finished) {
        [weakSelf.playView removeFromSuperview];
        weakSelf.playView.frame = weakSelf.playView.playViewFrame;
        [weakSelf.playView.playViewParent addSubview:weakSelf.playView];
        weakSelf.playView.state = PlayViewStateSmallScreen;
    }];
}

/*
 * 屏幕旋转参考
 * http://www.2cto.com/kf/201504/393303.html
 */
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskPortrait;
}


第二种实现方案:模态一个控制器,在模态出的控制器上全屏放置播放器的 view, 因此需要两个类退出全屏和进入全屏做动画的类,其代码如下
1、进入全屏做转场动画的类

#import <UIKit/UIKit.h>
@class PlayView;

@interface EnterFullScreenTransition : NSObject <UIViewControllerAnimatedTransitioning>

- (instancetype)initWithPlayView:(PlayView *)playView;
@end

#import "EnterFullScreenTransition.h"

@interface EnterFullScreenTransition ()

@property (nonatomic, strong)UIView * playView;
@end
@implementation EnterFullScreenTransition


- (instancetype)initWithPlayView:(PlayView *)playView
{
    if (self = [super init]) {
        self.playView = (UIView *)playView;
    }
    return self;
}

- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    
    // 取到将要被模态的控制器
    UIViewController * presentVc = [transitionContext viewControllerForKey:UITransitionContextToViewControllerKey];
    // 取到将要被模态的 view
    UIView * presentView = [transitionContext viewForKey:UITransitionContextToViewKey];
    
    // containerView是动画过程中提供的暂时容器。切换时的动画将在这个容器中进行
    CGRect smallPlayViewFrame = [[transitionContext containerView] convertRect:self.playView.bounds fromView:self.playView];
    
    presentView.bounds = self.playView.bounds;
    presentView.transform = CGAffineTransformMakeRotation(M_PI_2);
    presentView.center = CGPointMake(CGRectGetMidX(smallPlayViewFrame), CGRectGetMidY(smallPlayViewFrame));
    
    [[transitionContext containerView] addSubview:presentView];
    
    self.playView.frame  = presentView.bounds;
    [presentView  addSubview:self.playView];
    
    
    CGRect presentViewFinalFrame = [transitionContext finalFrameForViewController:presentVc];
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
        presentView.transform = CGAffineTransformIdentity;
        presentView.frame = presentViewFinalFrame;
        self.playView.frame = presentView.bounds;
    } completion:^(BOOL finished) {
        [transitionContext completeTransition:YES];
    }];

}


2、退出全屏做转场动画的类

#import <UIKit/UIKit.h>
@class PlayView;

@interface ExitFullScreenTransition : NSObject <UIViewControllerAnimatedTransitioning>

- (instancetype)initWithPlayView:(PlayView *)playView;
@end

#import "ExitFullScreenTransition.h"
#import "PlayView.h"
@interface ExitFullScreenTransition ()

@property (nonatomic, weak)PlayView * playView;
@end

@implementation ExitFullScreenTransition

- (instancetype)initWithPlayView:(PlayView *)playView
{
    if (self = [super init]) {
        self.playView = playView;
    }
    return self;
}


- (NSTimeInterval)transitionDuration:(nullable id <UIViewControllerContextTransitioning>)transitionContext
{
    return 0.5;
}

- (void)animateTransition:(id<UIViewControllerContextTransitioning>)transitionContext
{
    UIView * fromView = [transitionContext viewForKey:UITransitionContextFromViewKey];
    CGRect smallPlayViewFrame = [[transitionContext containerView] convertRect:self.playView.playViewFrame fromView:self.playView.playViewParent];
    
    [UIView animateWithDuration:[self transitionDuration:transitionContext] delay:0.0 options:UIViewAnimationOptionLayoutSubviews animations:^{
                         fromView.transform = CGAffineTransformIdentity;
                         fromView.frame = smallPlayViewFrame;
                         self.playView.frame = fromView.bounds;
                     }
    completion:^(BOOL finished) {
                         self.playView.frame = self.playView.playViewFrame;
                         [self.playView.playViewParent addSubview:self.playView];
                         [fromView removeFromSuperview];
                         [transitionContext completeTransition:YES];
    }];

    
}
@end

3、实现方案所需要的两个类(小屏显示、全屏显示 j即被模态的类)


#import <UIKit/UIKit.h>

@interface SecondViewController : UIViewController

@end

#import "SecondViewController.h"
#import "PlayView.h"
#import "Header.h"
#import "FullScreenViewController.h"
#import "EnterFullScreenTransition.h"
#import "ExitFullScreenTransition.h"
@interface SecondViewController ()<UIViewControllerTransitioningDelegate>
@property (nonatomic, weak)PlayView * playView;
@property (nonatomic, weak)FullScreenViewController * fullScreenVC;
@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.title = @"Second";
    
    PlayView * playView = [[PlayView alloc] initWithFrame:CGRectMake(0, 64, KSCREEN_WIDTH, KSCREEN_WIDTH * 9 / 16)];
    playView.backgroundColor = [UIColor blackColor];
    self.playView = playView;
    
    [self.view addSubview:playView];
    
    UITapGestureRecognizer * tap =[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHundle:)];
    [self.playView addGestureRecognizer:tap];
}

/*
 * 思路二:
 * 在一个只支持Portrait的ViewController上,present一个只支持Landscape的ViewController,通过改写ViewController之间的转场动画,既能高度自定义全屏动画,也能让StatusBar在视频全屏时横向显示。
 * 缺点一:部分控件依靠 window 尺寸布局,导致全屏动画过程中布局错乱
   原因:present 过程中,ios 对 presentingVC 的 frame 经过了两次变化
      1、由于 window 的 bounds 从竖直(height -> width)的状态转为横向(width ->)的状态由于 autoresing 的作用presentingVC的 view 也变成了横向状态
      2、系统给presentingVC的 view 增加了 transform 使其旋转了90度 让 presentingVC 的 view 看起来还是竖直方向
   结果:如果一个presentingVC的 view的子视图根据 window 布局,在第一次变化的时候宽高已经对调,这样就会导致第二次变化时这个子视图布局错乱。
 * 缺点二:屏幕渲染 bug 导致半边黑屏的问题,腾讯体育APP 就有半边黑屏的问题
 * 缺点三:UIScreen 长宽互换的 bug(iOS10)
 */


- (void)tapHundle:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded) {
        if (self.playView.state == PlayViewStateSmallScreen) {
            [self enterFullScreen]; // 全屏
        }
        if (self.playView.state == PlayViewStateFullScreen) {
            [self exitFullScreen];  // 退出全屏
        }
    }
}
/*
 * 全屏
 */
- (void)enterFullScreen
{
    if (self.playView.state != PlayViewStateSmallScreen) {
        return;
    }
    self.playView.state = PlayViewStateAnimating;
    
    // 记录最初的 frame 和父视图
    self.playView.playViewFrame= self.playView.frame;
    self.playView.playViewParent = self.view;
    [self.playView removeFromSuperview];
    
    FullScreenViewController * fullScreenVC = [[FullScreenViewController alloc] init];

    fullScreenVC.transitioningDelegate = self;
    __weak typeof(self) weakSelf = self;
    fullScreenVC.modalPresentationStyle = UIModalPresentationOverFullScreen;
    fullScreenVC.modalPresentationCapturesStatusBarAppearance = true;
   [self presentViewController:fullScreenVC animated:YES completion:^{
       weakSelf.playView.state = PlayViewStateFullScreen;
   }];
    
    self.fullScreenVC = fullScreenVC;
    
    
    
}


- (void)exitFullScreen
{
    if (self.playView.state != PlayViewStateFullScreen ) {
        return;
    }
    self.playView.state = PlayViewStateAnimating;
    __weak typeof(self) weakSelf = self;
    [self.fullScreenVC dismissViewControllerAnimated:YES completion:^{
        weakSelf.playView.state = PlayViewStateSmallScreen;
    }];
}


- (UIInterfaceOrientationMask)supportedInterfaceOrientations {

    return UIInterfaceOrientationMaskPortrait;
}


- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForPresentedController:(UIViewController *)presented presentingController:(UIViewController *)presenting sourceController:(UIViewController *)source
{
    return [[EnterFullScreenTransition alloc] initWithPlayView:self.playView];
}


- (nullable id <UIViewControllerAnimatedTransitioning>)animationControllerForDismissedController:(UIViewController *)dismissed
{
    return [[ExitFullScreenTransition alloc] initWithPlayView:self.playView];
}
@end

#import <UIKit/UIKit.h>

@interface FullScreenViewController : UIViewController

@end


#import "FullScreenViewController.h"
#import "Header.h"


@interface FullScreenViewController ()

@end

@implementation FullScreenViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor redColor];
    
}

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    return UIInterfaceOrientationMaskLandscapeLeft;
}

- (BOOL)prefersStatusBarHidden {
    return NO;
}
@end

第三种实现方案:在方案一的基础上,调用UIApplication的setStatusBarOrientation:animated:方法来改变statusBar的方向 同时重写当前的ViewController的shouldAutorotate方法,返回NO
关于其中的坑点 我已在代码中有所标注

#import <UIKit/UIKit.h>

@interface ThirdViewController : UIViewController

@end


#import "ThirdViewController.h"
#import "Header.h"
#import "PlayView.h"
@interface ThirdViewController ()
@property (nonatomic, weak)PlayView * playView;
@end

@implementation ThirdViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"Third";
    self.view.backgroundColor = [UIColor whiteColor];
    PlayView * playView = [[PlayView alloc] initWithFrame:CGRectMake(0, 64, KSCREEN_WIDTH, KSCREEN_WIDTH * 9 / 16)];
    playView.backgroundColor = [UIColor blackColor];
    self.playView = playView;
    
    [self.view addSubview:playView];
    
    
    UITapGestureRecognizer * tap =[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapHundle:)];
    [self.playView addGestureRecognizer:tap];
}

/*
 * 思路:从小屏进入全屏时,将播放器所在的view放置到window上,用transform的方式做一个旋转动画,最终让view完全覆盖window。 从全屏回到小屏时,用transform的方式做旋转动画,最终让播放器所在的view回到原先的parentView
 
 * 优缺点:这种方式在实现上相对简单,因为仅仅旋转了播放器所在的view,view controller和device的方向均始终为竖直(portrait)。但最大的问题就是全屏时status bar的方向依然是竖直的,虽然之前通过全屏时隐藏statusBar来掩盖了这个问题,但这同时导致了用户无法在视频全屏时看到时间、网络情况等,体验有待改善。
 */


- (void)tapHundle:(UITapGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateEnded) {
        if (self.playView.state == PlayViewStateFullScreen) {
            [self exitFullScreen]; // 小屏
        }
        if (self.playView.state == PlayViewStateSmallScreen) {
            [self enterFullScreen]; // 全屏
        }
    }
}




/*
 * 全屏
 */
- (void)enterFullScreen
{
    if (self.playView.state != PlayViewStateSmallScreen)
    {
        return;
    }
    self.playView.state = PlayViewStateAnimating;
    self.playView.playViewParent = self.playView.superview;
    self.playView.playViewFrame = self.playView.frame;
    
    
    // 计算控制器的 View 上的 palyView的 frame 相对于 window的 frame
    CGRect rectInWindow = [self.view convertRect:self.playView.frame toView:[UIApplication sharedApplication].keyWindow];
    [self.playView removeFromSuperview];
    self.playView.frame = rectInWindow;
    [[UIApplication sharedApplication].keyWindow addSubview:self.playView];
    __weak typeof(self) weakSelf = self;
    [UIView animateWithDuration:0.5 animations:^{
        weakSelf.playView.transform = CGAffineTransformMakeRotation(M_PI_2     );
        weakSelf.playView.bounds = CGRectMake(0, 0, CGRectGetHeight(weakSelf.playView.playViewParent.bounds), CGRectGetWidth(weakSelf.playView.playViewParent.bounds));
        weakSelf.playView.center = CGPointMake(CGRectGetMidX(weakSelf.playView.superview.bounds), CGRectGetMidY(weakSelf.playView.superview.bounds));
        
    } completion:^(BOOL finished) {
        weakSelf.playView.state = PlayViewStateFullScreen;
    }];
    [self refreshStatusBarOrientation:UIInterfaceOrientationLandscapeRight];
    
}
/*
 * 退出全屏
 */
- (void)exitFullScreen
{
    if (self.playView.state != PlayViewStateFullScreen ) {
        return;
    }
    self.playView.state = PlayViewStateAnimating;
    __weak typeof(self) weakSelf = self;
    // 计算播放器父View 上的 playView 的 frame 相对于窗口的 frame
    CGRect frame = [self.playView.playViewParent convertRect:self.playView.playViewFrame toView:[UIApplication sharedApplication].keyWindow];
    [UIView animateWithDuration:0.5 animations:^{
        
        weakSelf.playView.transform = CGAffineTransformIdentity;
        weakSelf.playView.frame = frame;
    } completion:^(BOOL finished) {
        [weakSelf.playView removeFromSuperview];
        weakSelf.playView.frame = weakSelf.playView.playViewFrame;
        [weakSelf.playView.playViewParent addSubview:weakSelf.playView];
        weakSelf.playView.state = PlayViewStateSmallScreen;
    }];
    [self refreshStatusBarOrientation:UIInterfaceOrientationPortrait];
}


- (void)refreshStatusBarOrientation:(UIInterfaceOrientation)interfaceOrientation {
    [[UIApplication sharedApplication] setStatusBarOrientation:interfaceOrientation animated:YES];
}

/* 如果只有 viewController 的话, 以下方法一定会走,但如果项目中既有 TabVC、又有 NavVC 要在父类中也实现如下方法,否则,以下方法不会走,会被拦截
 
 * tabVC: - (BOOL)shouldAutorotate
            {
                return self.selectedViewController.shouldAutorotate;
            }
 
 * navVC: - (BOOL)shouldAutorotate
            {
                return self.topViewController.shouldAutorotate;
            }
 */

- (BOOL)shouldAutorotate
{
    return NO;
}

最后:
1、大家不喜勿喷,喜欢的可以 star
2、参考链接

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,222评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,455评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,720评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,568评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,696评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,879评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,028评论 3 409
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,773评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,220评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,550评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,697评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,360评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,002评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,782评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,010评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,433评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,587评论 2 350

推荐阅读更多精彩内容