大家的项目都是只支持竖屏的吧?大多数朋友(这其中当然也包括博主),都没有做过横屏开发,这次项目刚好有这个需求,因此把横竖屏相关的心得写成一遍文章供诸位参考。

01.综述

大多数公司的项目都只支持竖屏,只有一两个界面需要同时支持横屏,就像视频 APP 一样,只有视频播放的时候需要横屏,其他时候都只允许竖屏。给出的 demo 中处理两种需要横屏的情形:

  • 第一种是录制视频时横屏
  • 第二种是播放视频时横屏

具体使用演示请前往优酷视频查看:BLLandscape Demo。

02.录制视频横屏

一般可能只需要播放视频时横屏,录制横屏一般用不到,但是如果有朋友需要做横屏视频录制,这时候就需要录制横屏处理,就像下面这样的。

这个思路是这样的:

  • 横屏的时候,首先把要横屏的 view 从原先的 superView 中移除,添加到当前的 keyWindow 上,然后做 frame 动画,将窗口的高设为 view 的宽,窗口的宽设置为 view 的高,然后将 view 的旋转 90°,执行动画,就能得到当前的效果。

  • 竖屏的时候,是一个相反的过程,先在窗口上做完动画,再将 view 插入到横屏之前的 superView 中的对应位置上。

2.1.横屏切换

我把这些实现都抽成一个 UIView 的分类,看一下实现:

// frame 转换.
- (void)landscapeExecute{self.transform = CGAffineTransformMakeRotation(M_PI_2);CGRect bounds = CGRectMake(0, 0, CGRectGetHeight(self.superview.bounds), CGRectGetWidth(self.superview.bounds));CGPoint center = CGPointMake(CGRectGetMidX(self.superview.bounds), CGRectGetMidY(self.superview.bounds));self.bounds = bounds;self.center = center;
}- (void)bl_landscapeAnimated:(BOOL)animated animations:(BLScreenEventsAnimations)animations complete:(BLScreenEventsComplete)complete{if (self.viewStatus != BLLandscapeViewStatusPortrait) {return;}self.viewStatus = BLLandscapeViewStatusAnimating;self.parentViewBeforeFullScreenSubstitute.anyObject = self.superview;self.frame_beforeFullScreen = [NSValue valueWithCGRect:self.frame];NSArray *subviews = self.superview.subviews;if (subviews.count == 1) {self.indexBeforeAnimation = 0;}else{for (int i = 0; i < subviews.count; i++) {id object = subviews[i];if (object == self) {self.indexBeforeAnimation = i;break;}}}CGRect rectInWindow = [self.superview convertRect:self.frame toView:nil];[self removeFromSuperview];self.frame = rectInWindow;[[UIApplication sharedApplication].keyWindow addSubview:self];if (animated) {[UIView animateWithDuration:0.35 animations:^{[self landscapeExecute];if (animations) {animations();}} completion:^(BOOL finished) {[self landscpeFinishedComplete:complete];}];}else{[self landscapeExecute];[self landscpeFinishedComplete:complete];}self.viewStatus = BLLandscapeViewStatusLandscape;[self refreshStatusBarOrientation:UIInterfaceOrientationLandscapeRight];
}复制代码

2.2.竖屏切换

竖屏和横屏就是一个相反的过程,这里不贴代码也不做解释了。不懂的去看源码就知道了。

2.3.注意点

2.3.1.分类中实现 weak

源码没什么难度,但是有一个细节需要注意,我们要在分类中以 weak 的内存管理策略去引用动画之前的 superView,以便我们回来做竖屏动画完成以后将当前 view 添加到动画之前的 superView 上。但是在分类中添加属性的内存管理策略中没有 weak 属性,但是有一个 OBJC_ASSOCIATION_ASSIGN,它类似我们常用的 assignassign 策略的特点就是在对象释放以后,不会主动将应用的对象置为 nil,这样会有访问僵尸对象导致应用奔溃的风险。

为了解决这个问题,我们可以创建一个替身对象,我们可以在分类中以 OBJC_ASSOCIATION_RETAIN_NONATOMIC 的策略来强引用替身对象,然后在替身对象中以 weak 的策略去引用我们真实需要保存的对象。这样就能解决这个可能导致奔溃的问题了。

2.3.2.布局

由于我们做的是 frame 动画,所以之后在这个 view 上再添加子控件的时候必须使用 frame 布局,Autolayout 布局在当前的 view 上将不会被更新,导致 UI 错乱。

03.播放视频横屏

大多数场景都是播放视频的时候横屏,比如下面这样的:

如果你在网上搜 iOS 横竖屏切换 能搜到的也就是播放视频的时候的横屏了,而这些文章似乎都是抄的某一篇文章,大家说的都一样。虽然大家抄来抄去,似乎他们在文章中写的都能解决问题,但实际上他们的文章是不能解决实际问题的。

3.1.播放视频横屏

我们来看一下控制屏幕旋转的两个方法:

@interface UIViewController (UIViewControllerRotation)
...
- (BOOL)shouldAutorotate NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
- (UIInterfaceOrientationMask)supportedInterfaceOrientations NS_AVAILABLE_IOS(6_0) __TVOS_PROHIBITED;
...
@end复制代码

可以看到可以控制屏幕方向的方法是定义在 UIViewController 里的,第一个 -shouldAutorotate 方法,系统会询问当前控制器是否支持旋转,第二个方法 -supportedInterfaceOrientations 告诉系统当前控制器支持那几个方向的旋转。

真实项目中,我们的 UI 架构可能是这样的:

我们的项目中从窗口开始,依次是一个根控制器,然后再是 UITabbarController 然后再是 UINavigationController,最后才到我们的 UIViewController,我们是某些界面需要横屏,所以必须要把系统的询问细化到每个控制器的方法才行。

结合上图,我们看下一个横竖屏事件的传递过程:

  • 先是陀螺仪捕获到一个横屏事件
  • 接下来系统会找到当前用户操作的那个 APP
  • APP 会找到当前的窗口 window
  • 窗口 window 会找到根控制器,这个时候事件终于传到我们开发者手里了
  • 对于我们自定义的根控制器,它需要把这个事件传递到 UITabbarController
  • 对于 UITabbarController,需要把事件传递到 UINavigationController
  • 对于 UINavigationController,需要把事件传递到我们自己的控制器
  • 最后在我们自己的控制器中决定某个界面是否需要横屏

等等,你我的项目是一个已经可能有上千个控制器的大工程了,如果按照这个逻辑走下去,我们要在每个控制器写这个两个方法,不敢想象。

此时我们第一要考虑的就是借助分类来实现,既简单又优雅,而且维护起来集中干净,何乐而不为?

#import <UIKit/UIKit.h>@interface UIViewController (Landscape)/*** 是否需要横屏(默认 NO, 即当前 viewController 不支持横屏).*/
@property(nonatomic) BOOL bl_shouldAutoLandscape;@end#import "UIViewController+Landscape.h"
#import <objc/runtime.h>
#import <JRSwizzle.h>@implementation UIViewController (Landscape)+ (void)load{[self jr_swizzleMethod:@selector(shouldAutorotate) withMethod:@selector(bl_shouldAutorotate) error:nil];[self jr_swizzleMethod:@selector(supportedInterfaceOrientations) withMethod:@selector(bl_supportedInterfaceOrientations) error:nil];
}- (BOOL)bl_shouldAutorotate{ // 是否支持旋转.if ([self isKindOfClass:NSClassFromString(@"BLAppRootViewController")]) {return self.childViewControllers.firstObject.shouldAutorotate;}if ([self isKindOfClass:NSClassFromString(@"UITabBarController")]) {return ((UITabBarController *)self).selectedViewController.shouldAutorotate;}if ([self isKindOfClass:NSClassFromString(@"UINavigationController")]) {return ((UINavigationController *)self).viewControllers.lastObject.shouldAutorotate;}if ([self checkSelfNeedLandscape]) {return YES;}if (self.bl_shouldAutoLandscape) {return YES;}return NO;
}- (UIInterfaceOrientationMask)bl_supportedInterfaceOrientations{ // 支持旋转的方向.if ([self isKindOfClass:NSClassFromString(@"BLAppRootViewController")]) {return [self.childViewControllers.firstObject supportedInterfaceOrientations];}if ([self isKindOfClass:NSClassFromString(@"UITabBarController")]) {return [((UITabBarController *)self).selectedViewController supportedInterfaceOrientations];}if ([self isKindOfClass:NSClassFromString(@"UINavigationController")]) {return [((UINavigationController *)self).viewControllers.lastObject supportedInterfaceOrientations];}if ([self checkSelfNeedLandscape]) {return UIInterfaceOrientationMaskAllButUpsideDown;}if (self.bl_shouldAutoLandscape) {return UIInterfaceOrientationMaskAllButUpsideDown;}return UIInterfaceOrientationMaskPortrait;
}- (void)setBl_shouldAutoLandscape:(BOOL)bl_shouldAutoLandscape{objc_setAssociatedObject(self, @selector(bl_shouldAutoLandscape), @(bl_shouldAutoLandscape), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}- (BOOL)bl_shouldAutoLandscape{return [objc_getAssociatedObject(self, _cmd) boolValue];
}- (BOOL)checkSelfNeedLandscape{NSProcessInfo *processInfo = [NSProcessInfo processInfo];NSOperatingSystemVersion operatingSytemVersion = processInfo.operatingSystemVersion;if (operatingSytemVersion.majorVersion == 8) {NSString *className = NSStringFromClass(self.class);if ([@[@"AVPlayerViewController", @"AVFullScreenViewController", @"AVFullScreenPlaybackControlsViewController"] containsObject:className]) {return YES;}if ([self isKindOfClass:[UIViewController class]] && [self childViewControllers].count && [self.childViewControllers.firstObject isKindOfClass:NSClassFromString(@"AVPlayerViewController")]) {return YES;}}else if (operatingSytemVersion.majorVersion == 9){NSString *className = NSStringFromClass(self.class);if ([@[@"WebFullScreenVideoRootViewController", @"AVPlayerViewController", @"AVFullScreenViewController"] containsObject:className]) {return YES;}if ([self isKindOfClass:[UIViewController class]] && [self childViewControllers].count && [self.childViewControllers.firstObject isKindOfClass:NSClassFromString(@"AVPlayerViewController")]) {return YES;}}else if (operatingSytemVersion.majorVersion == 10){if ([self isKindOfClass:NSClassFromString(@"AVFullScreenViewController")]) {return YES;}}return NO;
}@end复制代码

3.2.注意点

3.2.1. JPWarpViewController

当前 demo 中使用了 JPNavigationController,因为 JPNavigationController 结构的特殊性,所以这里加了一个

if ([self isKindOfClass:NSClassFromString(@"JPWarpViewController")]) {return [self.childViewControllers.firstObject supportedInterfaceOrientations];}复制代码

如果你项目中有为每个界面定制导航条的需求,你或许可以前往我的 GitHub 查看。

3.2.2.AVFullScreenViewController

当网页中有 video 标签的时候,iPhone 打开这个网页的的时候会把 video 标签替换为对应的系统的播放器,当我们点击这个视频的时候,系统会全屏进入一个视频播放界面,通过打印这个控制器我们可以看到这个控制器的类名是 AVFullScreenViewController,所以,这个界面需要横屏,就返回横屏对应的属性就可以实现这个控制器横屏。

3.2.3.实现有视频的网页需要横屏

并不是所有的网页都需要横屏,但是如果这个网页有视频,往往需要横屏,那我们怎么知道某个页面是否需要横屏,是否有视频呢?

一种方式是和 h5 约定一个事件,如果有视频就告诉原生 APP 做一个标记,将 bl_shouldAutoLandscape 置为 YES

但是我这里提供一种更加简便优雅的方式,我们的 UIWebView 是可以通过 -stringByEvaluatingJavaScriptFromString: 方法和我们交互的,所以我们可以尝试下面的方法:

#pragma mark - UIWebViewDelegate- (void)webViewDidFinishLoad:(UIWebView *)webView{NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"if(document.getElementsByTagName('video').length>0)document.getElementsByTagName('video').length;"];if (result.length && result.integerValue != 0) {self.bl_shouldAutoLandscape = YES;}
}复制代码

WebView 加载完以后,我们去查找当前的 h5 页面中有没有 Video 标签,如果有,那我们就可以拿到结果,做对应的横屏处理。

3.2.4.大坑来了

本来我们的这个 UITableViewController 是不支持横竖屏的,就像这样,注意,这个时候那个 UISwitch 按钮是关闭的。

接下来,我们把这个开关打开,这个开关对应的代码是这样:

- (void)switchValueChanged:(UISwitch *)aswitch{if (aswitch.isOn) {BLAnotherWindowViewController *vc = [BLAnotherWindowViewController new];self.anotherWindow.rootViewController = vc;[self.anotherWindow insertSubview:vc.view atIndex:0];}else{self.anotherWindow.rootViewController = nil;}
}复制代码

就是打开后会为另外一个窗口添加一个根控制器,而这个根控制器的代码是这样的:

#import "BLAnotherWindowViewController.h"@interface BLAnotherWindowViewController ()@end@implementation BLAnotherWindowViewController- (BOOL)shouldAutorotate{return YES;
}- (UIInterfaceOrientationMask)supportedInterfaceOrientations{return UIInterfaceOrientationMaskAll;
}@end复制代码

这样以后,我们观察一下控制器的表现:

看起来我们的主界面确实仍然不支持横竖屏,这是没有问题的,但是好像我们的状态栏被蓝色的这个窗口劫持了,它们俩双宿双飞,一起干了这么一个横竖屏的勾当。

我们想象一下,现在这个蓝色的窗口在最前面,我们能敏捷的观察到时这个蓝色的窗口劫持了状态栏。那如果这个蓝色的窗口在我们的主窗口后面呢,那我们根本就不会察觉到这个细节,我们能看到的就是下面这样:

第一次碰到这个 bug,我的内心是奔溃的。

我们一起来分析一下这个问题是怎么造成的。再来看一下这个横竖屏系统询问路径图,当我们有多个窗口之时,每个窗口都是平等的,那个蓝色的窗口也收到了系统的询问。

  • 还记得之前两个系统询问的方法是 UIViewController 的方法,此时如果窗口并没有 rootViewController 的话,那系统问也白问,所以蓝色窗口并不会劫持状态栏和横屏事件。
  • 如果此时蓝色窗口有 rootViewController 的话,那么该控制的返回值就会决定设备的方向。也就造成了这个 bug。

04.补充更新

又发现了一个新的 bug,现在补充一下,如果我们的应用是竖屏的,只是某些界面需要横屏,那么如果我们把项目的 info.plist 的横竖屏全部都打开的话,就像下面这样:

那么对于 plus 手机的话,如果你是横着手机打开 APP,那么首个界面肯定是会 UI 错乱的,因为这个时候 APP 还没启动完,系统会根据我们 info.plist 的配置进行初始化,所以会导致这个 bug。

现在解决方式是这样的, info.plist 我们仍然这样写,以保证刚启动 APP 的时候不至于 UI 错乱。

image.png

接下来,我们来到 AppDelegate 里返回支持的横屏方向,就像下面这么写,功能和在 info.plist 也是一样的。

- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{return self.window.rootViewController.supportedInterfaceOrientations;
}复制代码

05.最后

最后 GitHub 地址在这里 BLLandscape

我的文章集合

下面这个链接是我所有文章的一个集合目录。这些文章凡是涉及实现的,每篇文章中都有 Github 地址,Github 上都有源码。如果某篇文章刚好在你的实际开发中帮到你,又或者提供一种不同的实现思路,让你觉得有用,那就看看这句话 “坚持每天点赞的人,99%都是帅哥美女,再也不用单身了”。

我的文章集合索引

你还可以关注我自己维护的简书专题 iOS开发心得。这个专题的文章都是实打实的干货。

######如果你有问题,除了在文章最后留言,还可以在微博 @盼盼_HKbuy上给我留言,以及访问我的 Github。

[贝聊科技] iOS 终极横竖屏切换解决方案相关推荐

  1. iOS终极横竖屏切换解决方案

    大家的项目都是只支持竖屏的吧?大多数朋友(这其中当然也包括博主),都没有做过横屏开发,这次项目刚好有这个需求,因此把横竖屏相关的心得写成一遍文章供诸位参考. 01.综述 大多数公司的项目都只支持竖屏, ...

  2. iOS 横竖屏切换解决方案

    iOS 横竖屏切换解决方案 参考文章: (1)iOS 横竖屏切换解决方案 (2)https://www.cnblogs.com/qqcc1388/p/7358552.html 备忘一下.

  3. iOS 中横竖屏切换

    iOS 中横竖屏切换的功能,在开发iOS app中总能遇到.以前看过几次,感觉简单,但是没有敲过代码实现,最近又碰到了,demo尝试了几种情况,这里就做下总结. 注意 横屏两种情况是反的你知道吗? U ...

  4. iOS视频播放横竖屏切换技巧

    一.需求:横竖屏切换. 二.效果:                       三.实现: 如上图,点击工具栏的第四个按钮进行横屏切换: - (void)toolTabButtonPressed:(A ...

  5. iOS的横竖屏切换旋转(禁自动旋转)

    这次做了视频的播放器,坑啊 ,好多,这不,刚刚爬上来,就来帮后来者填坑... 首先先说下横竖屏切换旋转的坑吧,,, 1. 在AppDelegate.h文件中 声明一个变量, @property (no ...

  6. iOS 6横竖屏切换

    iOS6.0版本之前,UIViewController之间的横竖屏切换,只需设置一个函数: - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInte ...

  7. [贝聊科技]iOS 代码架构(一)如何创建一个易复用的组件

    前言 贝聊的移动客户端分别有家长端和老师端,一家公司里同时维护多个业务上有关联性的app这种情况其实很常见,例如一些提供 O2O 服务的公司,经常会分用户端和商家端.这些客户端虽然各自负责着一个业务环 ...

  8. ipad横竖屏切换解决方案

    2011年08月01日 星期一 10:09 由于ipad的横竖屏不同,所以好的应用,横竖屏的页面布局也不一样.那么就需要横竖屏的整体解决方案.先看一个横竖屏布局不一样的界面. 上面两张图是来自同一个界 ...

  9. iOS应用横竖屏切换

    一.概述:     在iOS应用中,由UIViewController来控制屏幕翻转,根据需要随设备方向自动切换.在iOS6和之前的系统之间,控制方法发生了些变化. 二.视图伸缩属性: 1.UIVie ...

最新文章

  1. 粤港澳大湾区落地首家人工智能工程院
  2. 《JavaScript高级程序设计》节点层次和DOM操作技术
  3. 网络工程:3.1 RIP(Routing Information Protocol)协议
  4. mysql数据库邮箱什么类型_MySQL的数据类型介绍
  5. 扫地机器人单扫和双扫_618买扫地机器人前必看 别图便宜 小心入坑!
  6. CoreJava Reading Note(3:Fundamental structure)
  7. Binary XML file line #23: Error inflating class android.widget.TextView
  8. Laravel 实现任务调度功能
  9. 华为数通ensp命令(二)
  10. 最近新发现的歌谱排版软件Lilypond
  11. 温湿度传感器的工作原理及应用领域你了解多少呢
  12. 局域网查看工具V1.60.exe与局域网助手(LanHelper)的试用
  13. android root写入文件,android中root用户无法往某些目录写入文件解决方法
  14. Unity+Kinect 开发脚本介绍
  15. 纽约大学文科学院计算机,2016年美国大学文科排名大全
  16. sqlserver2008 R2数据库-不允许表修改保存,阻止保存要求重新创建表的更改
  17. 五款经典GPRS无线上网卡比拼
  18. C/C++内存泄漏和野指针的区别
  19. 我写的破代码将被保存1000年?GitHub启动代码永久存储计划
  20. [前端] 键盘事件常用操作

热门文章

  1. mysql的bigint类型_MySQL中的类型:BigInt(20)与Int(20)
  2. 扬帆起航,再踏征程(一)
  3. 计算机本省 9588错误代码,win10系统共享打印机报错提示错误代码0x0000052e的解决步骤...
  4. 后台系统-医院设置信息接口开发
  5. 【CryptoZombies - 2 Solidity 进阶】004 使用view和内存中的数组来节约Gas
  6. 【汇编】 编写一个汇编语言程序,要求对键盘输入的小写字母用大写字母显示出来。
  7. element 去掉表格底部白线
  8. 解决 CentOS 7 报错: /var/run/yum.pid 已被锁定
  9. 在cesium中使用3D地形数据terrain builder的打开步骤
  10. 机器学习:K近邻算法