iOS 关于屏幕旋转问题
关于屏幕旋转需要理解两个概念设备方向(UIDeviceOrientation)和屏幕方向(UIInterfaceOrientation)
其中设备方向是物理方向,屏幕方向是APP内容显示方向,我们基本都是跟屏幕方向打交道,设备方向,我们只需要取当前设备方向值就OK了。
其中屏幕旋转是建立在手机加速计基础。
基础知识
1. UIDeviceOrientation(设备的物理方向)
UIDeviceOrientation
是我们手持的苹果设备(iPhone,iPad..)的当前的朝向,是实物,共有七个方向,是以home键为基础参照物的。
home键在左时,屏幕是向右旋转(UIDeviceOrientationLandscapeRight)
,home键在右时,屏幕是向左旋转(UIDeviceOrientationLandscapeLeft)
。
当前屏幕的方向通过[UIDevice currentDevice].orientation
方法获取,这个方法我们只能读取值,不能设置值,因为这是物理方向。
如果页面的不支持自动旋转功能我们获取的值只能是UIDeviceOrientationPortrait
//Portrait 表示纵向,Landscape 表示横向。
typedef NS_ENUM(NSInteger, UIDeviceOrientation) {
//未知方向,可能是设备(屏幕)斜置
UIDeviceOrientationUnknown,
//设备(屏幕)竖屏
UIDeviceOrientationPortrait, // Device oriented vertically, home button on the bottom
//竖屏,只不过上下颠倒
UIDeviceOrientationPortraitUpsideDown, // Device oriented vertically, home button on the top
//设备向左旋转横置
UIDeviceOrientationLandscapeLeft, // Device oriented horizontally, home button on the right
//设备向右旋转横置
UIDeviceOrientationLandscapeRight, // Device oriented horizontally, home button on the left
//设备(屏幕)朝上平躺
UIDeviceOrientationFaceUp, // Device oriented flat, face up
//设备(屏幕)朝下平躺
UIDeviceOrientationFaceDown // Device oriented flat, face down
} __TVOS_PROHIBITED;
2. UIInterfaceOrientation(界面的显示方向)
UIInterfaceOrientation
界面的当前旋转方向或者说是朝向(如果当前页面支持屏幕旋转就算设备旋转锁关闭了也可以强制屏幕旋转),屏幕方向和设备方向的区别是一个是可以设置一个无能为力。
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
//屏幕方向未知
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknown,
//向上正方向的竖屏
UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
//向下正方向的竖屏
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
//向右旋转的横屏
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
//向左旋转的横屏
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
} __TVOS_PROHIBITED;
其中两个枚举值中的左右旋转刚好对立,当设备向左转时屏幕是向右转的。
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
3.屏幕旋转流程
加速计是屏幕旋转的基础,依赖加速计,设备才可以判断出当前的设备方向。
当加速计检测到方向变化的时候,会发出UIDeviceOrientationDidChangeNotification
通知。
APP处理屏幕旋转的流程
- 当设备加速计检测到方向变化的时候,会发出
UIDeviceOrientationDidChangeNotification
屏幕旋转通知,这样任何关心方向变化的View都可以通过注册该通知,在设备方向变化的时候做出相应的响应。 - 设备旋转的后,APP内接收到旋转事件(通知)。
- APP通过AppDelegate通知当前程序的KeyWindow。
- KeyWindow会知会它的rootViewController,判断该View Controller所支持的旋转方向,完成旋转。
- 如果存在弹出的View Controller(模态弹的)的话,系统则会根据弹出的View Controller,来判断是否要进行旋转。
APP可以选择性的(是否)接收 UIDeviceOrientationDidChangeNotification
通知。
// 是否已经开启了设备方向改变的通知
@property(nonatomic,readonly,getter=isGeneratingDeviceOrientationNotifications)
BOOL generatesDeviceOrientationNotifications
__TVOS_PROHIBITED;
// 开启接收接收 UIDeviceOrientationDidChangeNotification 通知
- (void)beginGeneratingDeviceOrientationNotifications
__TVOS_PROHIBITED; // nestable
// 结束接收接收 UIDeviceOrientationDidChangeNotification 通知
- (void)endGeneratingDeviceOrientationNotifications__TVOS_PROHIBITED;
在 app 代理里面结束接收 设备旋转的通知事件, 后续的屏幕旋转都会失效
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 结束接收接收 UIDeviceOrientationDidChangeNotification 通知
[[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
return YES;
}
UIViewController实现屏幕旋转
在响应设备旋转时,我们可以通过UIViewController的方法实现更细致控制,当View Controller接收到Window传来的方向变化的时候,流程如下:
- 首先判断当前ViewController是否支持旋转到目标方向,如果支持的话进入流程2,否则此次旋转流程直接结束。
- 调用 willRotateToInterfaceOrientation:duration: 方法,通知View Controller将要旋转到目标方向。如果该ViewController是一个Container View Controller的话,它会继续调用其Content View Controller的该方法。这个时候我们也可以暂时将一些View隐藏掉,等旋转结束以后在现实出来。
- Window调整显示的View Controller的bounds,由于View Controller的bounds发生变化,将会触发 viewWillLayoutSubviews 方法。这个时候self.interfaceOrientation和statusBarOrientation方向还是原来的方向。
- 接着当前View Controller的 willAnimateRotationToInterfaceOrientation:duration: 方法将会被调用。系统将会把该方法中执行的所有属性变化放到动animation block中。
- 执行方向旋转的动画。
- 最后调用 didRotateFromInterfaceOrientation: 方法,通知View Controller旋转动画执行完毕。这个时候我们可以将第二部隐藏的View再显示出来。
响应过程如下图所示:
响应过程
4. 监听屏幕旋转方向
UIDeviceOrientationDidChangeNotification
//添加监听设备方向的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onDeviceOrientationChange) name:UIDeviceOrientationDidChangeNotification object:nil];
//监听设备方向的通知方法
#pragma mark - 监听设备方向和全屏
//参照坐标 手机在竖屏情况下
- (void)onDeviceOrientationChange{
UIDeviceOrientation orientation = [UIDevice currentDevice].orientation;
UIInterfaceOrientation interfaceOrientation = (UIInterfaceOrientation)orientation;
switch (interfaceOrientation) {
case UIInterfaceOrientationPortraitUpsideDown:{
NSLog(@"状态栏在手机下方 不过一般不会用到");
}
break;
case UIInterfaceOrientationPortrait:{
NSLog(@"手机在竖屏状态下");
}
break;
case UIInterfaceOrientationLandscapeLeft:{
NSLog(@"状态栏在手机左侧");
}
break;
case UIInterfaceOrientationLandscapeRight:{
NSLog(@"状态栏在手机右侧");
}
break;
default:
break;
}
}
界面发生变化状态栏改变通知
UIApplicationWillChangeStatusBarOrientationNotification
UIApplicationDidChangeStatusBarOrientationNotification
//以监听UIApplicationDidChangeStatusBarOrientationNotification通知为例
[[NSNotificationCenter defaultCenter]addObserver:self selector:@selector(handleStatusBarOrientationChange:) name:UIApplicationDidChangeStatusBarOrientationNotification object:nil];
//界面方向改变的处理
- (void)handleStatusBarOrientationChange: (NSNotification *)notification{
UIInterfaceOrientation interfaceOrientation = [[UIApplication sharedApplication] statusBarOrientation];
switch (interfaceOrientation) {
case UIInterfaceOrientationUnknown:
NSLog(@"未知方向");
break;
case UIInterfaceOrientationPortrait:
NSLog(@"界面直立");
break;
case UIInterfaceOrientationPortraitUpsideDown:
NSLog(@"界面直立,上下颠倒");
break;
case UIInterfaceOrientationLandscapeLeft:
NSLog(@"界面朝左");
break;
case UIInterfaceOrientationLandscapeRight:
NSLog(@"界面朝右");
break;
default:
break;
}
}
- (void)dealloc{
//最后在dealloc中移除通知
[[NSNotificationCenter defaultCenter]removeObserver:self];
[[UIDevice currentDevice]endGeneratingDeviceOrientationNotifications];
}
PS:手机锁定竖屏后,UIApplicationWillChangeStatusBarOrientationNotification
,UIApplicationDidChangeStatusBarOrientationNotification
和UIDeviceOrientationDidChangeNotification
通知都会失效。
具体应用
前提:想要APP支持横竖屏或者某个页面或者功能可以横竖屏切换,前提是项目支持横竖屏不然强制横屏也没用,需要我们的TARGETS中或者info.plist文件中右或者appdelegate中设置。我们在项目中屏幕旋转的方向就是这些设置的屏幕支持的方向,如果超出项目设置好的屏幕朝向APP会crash。
project > TARGETS > Gengral > Deployment Info > Device Orientation
info.plist appdelegate
项目中是否可以旋转主要由下面这三个方法控制
- (BOOL) shouldAutorotate;
- (UIInterfaceOrientationMask) supportedInterfaceOrientations;
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation;
一. shouldAutorotate(是否支持自动旋转)
- (BOOL) shouldAutorotate{
return YES;
}
shouldAutorotate作用是调用这个方法的控制器是否支持自动旋转,使用这个方法前提是需要项目支持横屏,返回值是BOOL。
情景:
- NO 当前页面不可以自动横屏,手机竖屏锁在打开或者关闭,调用强制横屏方法无用,不可以横屏。
//手机强制横屏方法
NSNumber *orientation = [NSNumber numberWithInt:UIInterfaceOrientationLandscapeRight];
[[UIDevice currentDevice] setValue:orientation forKey:@"orientation"];
- YES 当前页面支持自动旋转,手机竖屏锁打开,不可以根据手机方向旋转,调用强制横屏方法页面可以横屏。
- YES 手机竖屏锁关闭,屏幕可以根据手机朝向旋转,也可以强制旋转。
二. supportedInterfaceOrientations(当前屏幕支持的方向)
- (UIInterfaceOrientationMask) supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAllButUpsideDown;
}
supportedInterfaceOrientations作用是屏幕支持的方向有哪些,这个方法返回值是个枚举值
UIInterfaceOrientationMask
具体值有下面
typedef NS_OPTIONS(NSUInteger, UIInterfaceOrientationMask) {
//向上为正方向的竖屏
UIInterfaceOrientationMaskPortrait = (1 << UIInterfaceOrientationPortrait),
//向左移旋转的横屏
UIInterfaceOrientationMaskLandscapeLeft = (1 << UIInterfaceOrientationLandscapeLeft),
//向右旋转的横屏
UIInterfaceOrientationMaskLandscapeRight = (1 << UIInterfaceOrientationLandscapeRight),
//向下为正方向的竖屏
UIInterfaceOrientationMaskPortraitUpsideDown = (1 << UIInterfaceOrientationPortraitUpsideDown),
//向左或者向右的横屏
UIInterfaceOrientationMaskLandscape = (UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
//所有的横竖屏方向都支持
UIInterfaceOrientationMaskAll = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight | UIInterfaceOrientationMaskPortraitUpsideDown),
//支持向上的竖屏和左右方向的横屏
UIInterfaceOrientationMaskAllButUpsideDown = (UIInterfaceOrientationMaskPortrait | UIInterfaceOrientationMaskLandscapeLeft | UIInterfaceOrientationMaskLandscapeRight),
} __TVOS_PROHIBITED;
三. preferredInterfaceOrientationForPresentation
preferredInterfaceOrientationForPresentation 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)返回值是UIInterfaceOrientation是个枚举值
typedef NS_ENUM(NSInteger, UIInterfaceOrientation) {
//屏幕方向未知
UIInterfaceOrientationUnknown = UIDeviceOrientationUnknown,
//向上正方向的竖屏
UIInterfaceOrientationPortrait = UIDeviceOrientationPortrait,
//向下正方向的竖屏
UIInterfaceOrientationPortraitUpsideDown = UIDeviceOrientationPortraitUpsideDown,
//向右旋转的横屏
UIInterfaceOrientationLandscapeLeft = UIDeviceOrientationLandscapeRight,
//向左旋转的横屏
UIInterfaceOrientationLandscapeRight = UIDeviceOrientationLandscapeLeft
} __TVOS_PROHIBITED;
四. 具体实现
以上三个方法如果是在随便写的一个demo中完全OK,但是要是在完整项目中,会无效。
原因:
测试结果是当前控制器(页面)是否支持旋转是由根视图控制器控制的也就是rootViewController
,跟视图控制器如果没有重写上面三个方法默认是支持自动旋转的。因为随便写的demo里面的ViewControl就是根视图控制器所以有效。
一般情况下我们的根视图控制器要么是navigationController要么是tabbarController也有ViewControl。所以我们在根视图控制器下重写上面三个方法,把是否支持横竖屏给需要横竖屏的页面控制器来控制,或者写一个category。
下面是navigationController重写的方法其他的一样
1. 导航根视图控制器
导航根视图控制器下重写方法:
#import "HPNavigationController.h"
@implementation HPNavigationController
- (BOOL) shouldAutorotate{
NSLog(@"%@",[UIApplication sharedApplication].keyWindow.rootViewController);
return [self.visibleViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask) supportedInterfaceOrientations{
return [self.visibleViewController supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return [self.visibleViewController preferredInterfaceOrientationForPresentation];
}
@end
category重写方法:
#import "UINavigationController+HPToolBar.h"
@implementation UINavigationController (HPToolBar)
- (BOOL) shouldAutorotate{
return [self.topViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask) supportedInterfaceOrientations{
return [self.topViewController supportedInterfaceOrientations];
}
- (UIInterfaceOrientation) preferredInterfaceOrientationForPresentation{
return [self.topViewController preferredInterfaceOrientationForPresentation];
}
- (UIViewController *)childViewControllerForStatusBarStyle{
return self.topViewController;
}
- (UIViewController *)childViewControllerForStatusBarHidden{
return self.topViewController;
}
@end
1. tabBar根视图控制器
tabBar根视图控制器下重写方法:
#import "HPUITabBarController.h"
@implementation HPUITabBarController
- (BOOL) shouldAutorotate{
return [self.selectedViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask) supportedInterfaceOrientations{
return [self.selectedViewController supportedInterfaceOrientations];
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return [self.selectedViewController preferredInterfaceOrientationForPresentation];
}
@end
category重写方法:
#import "UITabBarController+HPToolBar.h"
@implementation UITabBarController (HPToolBar)
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
UIViewController *vc = self.viewControllers[self.selectedIndex];
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [nav.topViewController shouldAutorotate];
} else {
return [vc shouldAutorotate];
}
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
UIViewController *vc = self.viewControllers[self.selectedIndex];
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [nav.topViewController supportedInterfaceOrientations];
} else {
return [vc supportedInterfaceOrientations];
}
}
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
UIViewController *vc = self.viewControllers[self.selectedIndex];
if ([vc isKindOfClass:[UINavigationController class]]) {
UINavigationController *nav = (UINavigationController *)vc;
return [nav.topViewController preferredInterfaceOrientationForPresentation];
} else {
return [vc preferredInterfaceOrientationForPresentation];
}
}
@end
ViewController根视图控制器 category重写方法
#import "UIViewController+HPToolBar.h"
@implementation UIViewController (HPToolBar)
// 是否支持自动转屏
- (BOOL)shouldAutorotate {
return NO;
}
// 支持哪些屏幕方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
return UIInterfaceOrientationMaskPortrait;
}
// 默认的屏幕方向(当前ViewController必须是通过模态出来的UIViewController(模态带导航的无效)方式展现出来的,才会调用这个方法)
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
return UIInterfaceOrientationPortrait;
}
@end
写过这三个方法后就可以在需要旋转的控制器下重写这三个方法,可以实现自动旋转或者强制旋转,不过强制旋转的前提是shouldAutorotate
这个方法的返回值为YES
不然强制旋转无效。上面的执行顺序是先找根视图下的方法,如果有会直接回调。如果没有会找有没有navigationController的类别,有就回调,没有就直接默认为YES。如果根视图控制器没有这重写这三个方法,会找当前ViewControll继承的视图父类或者类别。在把横屏配置打开情况下如果没有重写这三个方法页面只支持竖屏(实测)。如果想要某个页面支持旋转只需要在支持旋转的控制下重写这三个方法。
有关shouldAutorotate
调用没反应的问题上面有解释,是因为视图是否旋转是由根视图控制器控制,只要重写了上面的方法,就没问题。