iOS横屏的深入研究
iOS横屏模式分2种:跟随系统自动旋转、强制横屏。无论哪种横屏模式,都有2中实现途径:1.重写系统旋转方法。2.对view执行transform。
注:下文只讲解iOS6以后的横屏配置,ios6以前的版本太老旧,基本没有app支持了,所以省略。
一、跟随系统自动旋转
跟随系统自动旋转就是关闭竖屏锁定,让屏幕随重力感应旋转,该模式下笔者推荐通过重写系统旋转方法实现。
1.1 app自动旋转的触发流程
我们先了解下app自动旋转的触发流程,然后才能更深入的理解如何实现旋转。
当手机的重力感应打开的时候, 如果用户旋转手机, 系统会抛发UIDeviceOrientationDidChangeNotification 事件,同时会读取plist文件获取app支持的旋转方向,如果此时在appDelegate中重写了
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window ,
那么会以重写的这个方法的返回值为准。然后会判断当前的controller是否为appDelegate的rootvc或者modal的vc,如果是则会读取该页面的以下三个属性:
- (BOOL)shouldAutorotate(是否支持自动旋转)、
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation(初始展示方向,只有modal模式下才会调用)、
- (UIInterfaceOrientationMask)supportedInterfaceOrientations(该页面支持的方向)
然后会根据preferredInterfaceOrientationForPresentation返回的结果展示初始视图。
这里需要注意一下的就是:如果shouldAutorotate返回值为YES的时候,infoplist文件中supportedInterfaceOrientations的值和你重写的该页面的supportedInterfaceOrientations返回值必须至少有一个交集,也就是这两个值与运算之后至少有一位为1来告诉系统旋转支持的方向,否则系统无法知道你到底想支持哪个方向旋转,会crash的(亲测)。如果shouldAutorotate返回值为NO时,前面两个supportedInterfaceOrientations可以没有交集,系统只会读取preferredInterfaceOrientationForPresentation的值去做初始化显示,但是保险起见,我们最好设置支持旋转方向的时候一定要保证有一个交集以上,避免苹果哪天检测严格出现不必要的crash。
1.2 自动旋转如何配置(采用重写系统旋转方法)
看了上文自动旋转的触发流程后,相信小伙伴们应该知道如何配置了,下面我还是给出详细步骤:
1.2.1 先配置app支持的旋转方向,可以有如下方式:
image.png
这种修改配置的方式其实根源就是修改infoplist文件
或者直接修改infoplist文件:
image.png
或者在appDelegate中重写:
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window {
return UIInterfaceOrientationMaskAll;
}//优先级最高
1.2.2 指定横屏页面重写相关方法
如果需要横屏的页面是appDelegate的rootvc,或者是modal下的vc,直接在该页面重写以下方法就可以,否则系统不会主动调用(不过可以通过间接的形式调用,下文会讲)。
//是否支持自动旋转
- (BOOL)shouldAutorotate{
return YES;
}
//初始的显示方向
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationLandscapeRight;
}
//支持的旋转方向
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;//此处的返回值应该和infoplist文件中的值有交集,否则进入页面立马就会crash
}
如果需要旋转的页面是被push过来的(也就非rootvc或者非modal下的vc),我们可以在跟rootvc重写以上三个方法,然后方法内部的返回值全部由顶层的子vc决定,并且也在顶层子vc重写该方法。一般情况下我们的rootvc是tabbarvc,而且tabbarvc里面全部是navgationvc,navigationvc里面的某个顶层vc才是你需要横屏的页面,具体实现如下:
tabbarvc里重写:
- (BOOL)shouldAutorotate{
return [self.selectedViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return [self.selectedViewController supportedInterfaceOrientations];
}
父类navgationvc里重写:
- (BOOL)shouldAutorotate{
return [self.topViewController shouldAutorotate];
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return [self.topViewController supportedInterfaceOrientations];
}
需要横屏的vc里重写:
- (BOOL)shouldAutorotate{
return YES;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations{
return UIInterfaceOrientationMaskAll;//此处的返回值应该和infoplist文件中的值有交集,否则旋转手机就会触发调用该方法,然后crash
}
至此,自动横屏就配置完毕了,这里需要注意的是push进入的页面不会在加载该页面的时候就调用preferredInterfaceOrientationForPresentation方法来确定初始显示方向(因为这个初始显示方向的方法是present的vc才会被调用,push模式下rootvc也不会调用,这也就是我前面push的时候父类和子类都没有重写这个方法的原因),而是取的父navigationvc的显示方向,当屏幕旋转时根据你设备的方向和该页面支持的方向确定朝哪边旋转。也就是说我们没法通过push直接进入一个横屏页面,push的页面只有触发旋转才会进行横屏,想要一进入页面就展示横屏,我们还是只能以modal的形式进入!
另外,如果想通过对view执行transform实现自动旋转也可以,我们可以通过自主监听UIDeviceOrientationDidChangeNotification的方式实现。先监听UIDeviceOrientationDidChangeNotification,然后在监听的回调中获取设备方向,根据设备方向对view做相应的transform操作。不过这种方式相对繁琐,还有一些坑,不建议使用,在此不做具体展开,关于transform的代码下文强制横屏中会讲。
二、强制横屏
2.1 重写系统旋转方法
强制系统朝某一方向显示,只需要在该页面重写如下方法:
- (BOOL)shouldAutorotate{
return NO;//关闭自动旋转
}
- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation{
return UIInterfaceOrientationLandscapeRight;//初始化朝右边显示
}
这里有个坑需要说明下:
坑1:强制某一方向横屏只能再model模式下实现,push模式下不行。小伙伴可能会问了,我按照上文自动旋转的方式把父类也重写了也不行吗?答案是不行,前面已经讲过了,push进入到一个页面的时候,是不会触发任何旋转类的方法的,只有旋转手机才会调用。
坑2:网上有些资料写的通过runtime的,调用setOrientation的形式是不可行的,该方法仅使用ios6以前的设备!
//以下仅仅使用ios6以下的设备,小伙伴不要被误导!
if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)]) {
SEL selector = NSSelectorFromString(@"setOrientation:");
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
[invocation setSelector:selector];
[invocation setTarget:[UIDevice currentDevice]];
int val = UIInterfaceOrientationLandscapeRight;
[invocation setArgument:&val atIndex:2];
[invocation invoke];
}
2.2 对view执行transform
该方法就是对当前的view执行一个90的旋转,不改变系统的显示方向。
viewDidLoad里实现如下方法:
//改变当前视图bounds的宽高
self.view.bounds = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);
//对当前视图做90度旋转
[UIView animateWithDuration:0.2 animations:^{
self.view.transform = CGAffineTransformMakeRotation(M_PI_2);
}];
viewWillAppear里:
//隐藏状态栏
[[UIApplication sharedApplication] setStatusBarHidden:YES];
viewWillDisappear里:
//恢复状态栏
[[UIApplication sharedApplication] setStatusBarHidden:NO];
这里也有个坑需要注意下:
当我们给view做transform的时候,系统的方向仍然还是竖屏,此时我们获取到的安全区域偏移量safeAreaInsets还是竖屏的,当我们需要针对iphoneX系列做布局的时候就有问题了。比如iphoneX竖屏下,我们通过self.view.safeAreaInsets获取的结果是(44,0,34,0),如果我们通过向右旋转之后,该方式获取的结果仍然还是这个值,但是横屏状态下,我们希望获取到的结果是(0,44,0,34),也就是偏移量全部向左旋转90度得到的结果,因此我们需要在通过这种方式旋转屏幕的时候,需要对偏移量做修正,我们可以在一个公共类中写一个获取安全区域的方法,实现代码如下:
/**
获取屏幕的安全区域
@param orientation 显示方向(是显示方向,非设备方向)
*/
+ (CGRect)getSafeAreaWithOrientation:(UIInterfaceOrientation)orientation{
CGRect safeRect = kScreen_Bounds;
UIEdgeInsets insets = UIEdgeInsetsZero;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 110000
//xcode baseSDK为11.0或者以上
if (@available(iOS 11.0, *)) {
insets = [UIApplication sharedApplication].keyWindow.safeAreaInsets;
}
#endif
if (orientation == UIInterfaceOrientationLandscapeLeft) {
safeRect = CGRectMake(safeRect.origin.x, safeRect.origin.y, safeRect.size.height, safeRect.size.width);
insets = UIEdgeInsetsMake(insets.left, insets.bottom, insets.right, insets.top);
} else if (orientation == UIInterfaceOrientationLandscapeRight){
safeRect = CGRectMake(safeRect.origin.x, safeRect.origin.y, safeRect.size.height, safeRect.size.width);
insets = UIEdgeInsetsMake(insets.right, insets.top, insets.left, insets.bottom);
} else if (orientation == UIInterfaceOrientationPortraitUpsideDown){
insets = UIEdgeInsetsMake(insets.bottom, insets.right, insets.top, insets.left);
}
safeRect = UIEdgeInsetsInsetRect(safeRect, insets);
return safeRect;
}
然后我们就可以根据上面返回的安全区域对view的自视图进行布局了。
三、iOS8横屏中的坑
上文我们介绍两种横屏实现的方式,在第一种通过重写系统旋转方法的方式中,存在一个iOS8的坑:iOS8系统的手机在横竖屏切换时,偶现window横竖屏的宽高不能正常变换问题。比如采用该方式实现横屏时,理论上竖屏变横屏的时候,竖屏的宽应该变成横屏的高,竖屏的高应该变成横屏的宽,但是iOS8的手机有时候就会出现无法变换的情况(查过一些资料,说这是苹果的一个bug)。
所以我们需要针对iOS8做一个横屏的补偿适配,思路是:在横屏的页面中,针对iOS8,我们可以先判断window的宽是否大于高,如果大于则是正常的不需要矫正,如果小于或者等于则对它的宽高做交换。
在viewWillAppear中实现如下代码:
//针对ios8横屏偶现的window宽高错乱问题做适配
if ([[[UIDevice currentDevice] systemVersion] floatValue] < 9.0 && [[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) {
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
CGRect windowFrame = keyWindow.frame;
if (windowFrame.size.width < windowFrame.size.height) {
windowFrame = CGRectMake(0, 0, windowFrame.size.height, windowFrame.size.width);
keyWindow.frame = windowFrame;
}
}