技术源于生活,服务生活 Terry 随笔

设备界面旋转

2016-05-26
Terry
iOS

UIViewController中配置旋转

  1. - (BOOL)shouldAutorotate: 标志VC是否可以自动旋转
  2. - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation: 当 shouldAutorotate 返回YES时有效,标志所支持的方向(UIInterfaceOrientationMask).当用户改变设备的方向时,系统会调用 root View 或最顶层(使用present出来的mode界面)中的supportedInterfaceOrientations函数.
  3. - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation: 优先的方向
  4. + (void)attemptRotationToDeviceOrientation: 尝试使用设备的方向旋转让所有的窗口

在用户旋转设备(手机)时,最顶层的VC要触发这些函数,有以下的情况:

  1. vc是用 presentViewController 出来的,那vc就是一个顶层窗口,自然就能触发相关函数了
  2. vc如果是UINavigationController的topViewController,那只有UINavigationController本身会触发到这些函数,因为UINavigationController就是一个UIViewController,所以一般会继承UINavigationController,并实现相关函数做处理,如下代码所示:
#pragma mark - rotation override
- (BOOL)shouldAutorotate
{
    if ([self.topViewController respondsToSelector:@selector(shouldAutorotate)])
    {
        return [self.topViewController shouldAutorotate];
    }
    return [super shouldAutorotate];
}

#ifdef __IPHONE_9_0
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
#else
- (NSUInteger)supportedInterfaceOrientations
#endif
{
    if ([self.topViewController respondsToSelector:@selector(supportedInterfaceOrientations)])
    {
        return [self.topViewController supportedInterfaceOrientations];
    }
    return [super supportedInterfaceOrientations];
}

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation
{
    if ([self.topViewController respondsToSelector:@selector(preferredInterfaceOrientationForPresentation)])
    {
        return [self.topViewController preferredInterfaceOrientationForPresentation];
    }
    return [super preferredInterfaceOrientationForPresentation];
}

关于旋转相关的几个消息

  1. UIDeviceOrientationDidChangeNotification: 只要设备旋转,则注册的对象会接收到此消息,不管是否禁止旋转
  2. UIApplicationWillChangeStatusBarOrientationNotification与UIApplicationDidChangeStatusBarOrientationNotification: 若禁止旋转,则不会接收到此消息

强制屏幕方向(横屏,竖屏)

实现方式:

  1. 对 UIDevice 的 orientation 进行处理.这是一个苹果3.0之后就不太愿意支持的方式,但对于码农来说是最方便的, 实现代码如下,有两种
//实现方式1: 
- (void)toOrientation:(UIInterfaceOrientation)aOrientation
{
    if ([[UIDevice currentDevice] respondsToSelector:@selector(setOrientation:)])
    {
        mIsCanAutorotate = YES;
        SEL selector = NSSelectorFromString(@"setOrientation:");
        NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]];
        [invocation setSelector:selector];
        [invocation setTarget:[UIDevice currentDevice]];
        int val = aOrientation;
        [invocation setArgument:&val atIndex:2];
        [invocation invoke];
        mIsCanAutorotate = NO;
    }
}

//实现方式2
- (void)toOrientation:(UIInterfaceOrientation)aOrientation
{
    //[[UIDevice currentDevice] setValue:@(UIInterfaceOrientationUnknown) forKey:@"orientation"]; //若想强制触发,或加上此句
    [[UIDevice currentDevice] setValue:@(aOrientation) forKey:@"orientation"];
}

若想进入VC就直接切换方式, 则需要在 viewDidLoad或viewWillAppear调用 toOrientation 在退出时还原原来的屏幕方向.需要在pop

  1. 第二种方式比较麻烦,你需要对状态栏及View进行旋转,比如以下代码:
/*旋转方向: UIInterfaceOrientationLandscapeLeft*/
UIViewController *vc = self.navigationController;
vc.view.transform = CGAffineTransformMakeRotation(-M_PI_2);
vc.view.frame = [UIScreen mainScreen].bounds;
//此时针对旋转的VC的 shouldAutorotate 需要返回NO, 这样setStatusBarOrientation才有效
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft animated:YES];

/*旋转方向: UIInterfaceOrientationPortrait*/
[[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait animated:YES];
UIViewController *vc = self.navigationController;
vc.view.transform = CGAffineTransformIdentity;
CGRect frame = [UIScreen mainScreen].bounds;
vc.view.frame = frame;

注意: 代码调用的位置与得到的结果有很大的关系,首先是 setStatusBarOrientation, 它针对当前VC的 shouldAutorotate 返回值来确定是否可以执行. 旋转的是整个界面,当然包括状态栏了. 比如你在 viewWillDisappear 中调用.那么 setStatusBarOrientation 针对的VC就是 UINavigationController的topViewControll(topVC),这时你就得让这个topViewControl的shouldAutorotate返回NO.当前VC(curVC) 不是 topVC, 这样一来 curVC 与 topVC 的关联就太深了.当然,这是个小问题,我们可以用扩展类或派生类来处理,我在JxCommon库中是这实现的:

//UIViewController(JxCommon)头文件

typedef NS_ENUM(NSUInteger, TAutorotateType)
{
    atDonotAutorotate,
    atCanAutorotate,
    atChildHandle //针对容器UINavigationController的topViewController或UITabBarController的selectedViewController,其它情况返回YES
};

//UIViewController(JxCommon)实现文件
#pragma mark - statusBar
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-protocol-method-implementation"
- (BOOL)shouldAutorotate
{
    switch (self.autorotateType)
    {
        case atCanAutorotate:   return YES;
        case atDonotAutorotate: return NO;
        default:
            break;
    }
    
    //atChildHandle
    UIViewController *vc;
    if ([self isKindOfClass:[UINavigationController class]])
    {
        vc = ((UINavigationController *)self).topViewController;
    }
    else if ([self isKindOfClass:[UITabBarController class]])
    {
        vc = ((UITabBarController *)self).selectedViewController;
    }
    if (vc && [vc respondsToSelector:@selector(shouldAutorotate)])
    {
        return [vc shouldAutorotate];
    }
    return YES;
    
}
#pragma clang diagnostic pop

这样在使用旋转时,代码如下:

- (void)testTransform
{
	UIViewController *vc = self.navigationController;
    UIViewController *topVC = self.navigationController.topViewController;
    TAutorotateType oldType = vc.autorotateType;
    vc.autorotateType = atDonotAutorotate;
    [UIView animateWithDuration:0.25 animations:^{
        
        if (aOrientation == UIInterfaceOrientationPortrait)
        {
            [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait animated:YES];
            
            vc.view.transform = CGAffineTransformIdentity;
            CGRect frame = [UIScreen mainScreen].bounds;
            vc.view.frame = frame;
            
            //计算 navigationBar 的位置
            CGRect r = topVC.navigationController.navigationBar.frame;
            r.origin.y = topVC.statusBarHidden ? 0 : 20;
            r.size.height = 44;
            topVC.navigationController.navigationBar.frame = r;
            logoutRect(r);
        }
        else
        {
            UIViewController *vc = self.navigationController;
            vc.view.transform = CGAffineTransformMakeRotation(-M_PI_2);
            vc.view.frame = [UIScreen mainScreen].bounds;
            [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft animated:YES];
            
        }
    } completion:^(BOOL finished) {
        
        vc.autorotateType = oldType;
    }];
}

这是一部分代码,大体没问题了,接下来是: 显示或隐藏状态栏,用下面的代码:

- (void)changedStatusBarVisible
{
	//必须修改navigationBar的位置,不能旋转后不能自动修改高度,这也是日了狗
	self.statusBarHidden = !self.statusBarHidden;
    CGRect r = self.navigationController.navigationBar.frame;
    r.size.height = 24 + (self.statusBarHidden ? 0 : 20);

    [UIView animateWithDuration:0.25 animations:^{
        
        [self setNeedsStatusBarAppearanceUpdate];
        self.navigationController.navigationBar.frame = r;
        [self.navigationController.view layoutIfNeeded];
    }];
}

在iOS9以上是没问题了,切换到iOS8.发现不能点击了,好吧,不想再研究下去了.本来一句代码的问题,使用 transform 这种方式,差点要了老命.特别是还不兼容..这也是日了狗的节奏.还是老实用 orientation 的方式来做简单

结束!


Content