iOS 16.0 播放器旋转适配

2,741 阅读4分钟

本文主要介绍 SJVideoPlayer 中旋转模块针对 iOS 16.0 的处理实现过程;

先看效果图:

Oct-19-2022 17-24-45.gif

旋转功能主要是在两个window之间来回切换, 一个是 App 本身的 window; 另一个则是由播放器创建的, 并在横屏显示的 window;

为方便区分, 这里我们把 App 的 window 起名为sourceWindow, 而横屏显示的 window 起名为fullscreenWindow;

竖屏旋转到横屏

从竖屏旋转到横屏的处理过程:

1. 将播放器视图从父视图中移除, 直接添加到 sourceWindow 上, 这个过程需要进行坐标转换, 保持播放器视图在屏幕中的位置不变;    
2. 动画旋转至横屏, 修改播放器视图的 transform, bounds, center等;
动画完毕后: 
3. 设置 fullscreenWindow 显示, 它需要直接显示成横屏指定的方向;
4. 将播放器视图从 sourceWindow 中移除, 添加到 fullscreenWindow 中;

到此就旋转完了, 示例代码如下:


    // 竖屏转横屏示例代码
    UIInterfaceOrientation fromOrientation = UIInterfaceOrientationPortrait;
    UIInterfaceOrientation toOrientation = UIInterfaceOrientationLandscapeLeft;

    CGRect screenBounds = UIScreen.mainScreen.bounds;
    CGFloat maxSize = MAX(screenBounds.size.width, screenBounds.size.height);
    CGFloat minSize = MIN(screenBounds.size.width, screenBounds.size.height);

    // 播放器父视图
    UIView *playerSuperview; 
    // 播放器视图
    UIView *playerView;
    // App 本身的 window
    UIWindow *sourceWindow = playerSuperview.window;
    CGRect sourceFrame = [playerSuperview convertRect:playerSuperview.bounds toView:sourceWindow];
    // 播放器横屏 window
    UIWindow *fullscreenWindow;

    // 0. 记录当前方向
    _currentOrientation = toOrientation;

    // 1. 将播放器视图从父视图中移除, 直接添加到 sourceWindow 上, 这个过程需要进行坐标转换, 保持播放器视图在屏幕中的位置不变; 
    [playerView removeFromSuperview];
    [playerView setFrame:sourceFrame];
    [sourceWindow addSubview:playerView]; 
         
    // 2. 动画旋转至横屏, 修改播放器视图的 transform, bounds, center等; 
    CGRect rotationBounds = (CGRect){ CGPointZero, (CGSize){maxSize, minSize} };
    CGPoint rotationCenter = (CGPoint){ maxSize * 0.5, minSize * 0.5 };
    CGAffineTransform rotationTransform = CGAffineTransformIdentity;
    switch ( toOrientation ) { 
    	case UIInterfaceOrientationLandscapeLeft:
            rotationTransform = CGAffineTransformMakeRotation(M_PI_2);
    	break;	
    	case UIInterfaceOrientationLandscapeRight:
            rotationTransform = CGAffineTransformMakeRotation(-M_PI_2);
    	break;
    }

    [UIView animateWithDuration:0.3 animations:^{ 
    	playerView.bounds = rotationBounds;
    	playerView.center = rootationCenter;
    	playerView.transform = rotationTransform;
    } completion:^(BOOL finished) { 
    	// 3. 设置 fullscreenWindow 显示, 它需要直接显示成横屏指定的方向;
    	[fullscreenWindow makeKeyAndVisible];
    	// 直接显示成横屏指定的方向(接下来讲解)
    	[self setNeedsUpdateOfSupportedInterfaceOrientations];

    	// 4. 将播放器视图从 sourceWindow 中移除, 添加到 fullscreenWindow 中;
    	[playerView removeFromSuperview];
    	playerView.transform = CGAffineTransformIdentity;
                playerView.bounds = fullscreenWindow.bounds;
                playerView.center = (CGPoint){
                    playerView.bounds.size.width * 0.5,
                    playerView.bounds.size.height * 0.5
                };
        [fullscreenWindow.rootViewController.view addSubview:playerView];
    }];
    

_currentOrientation用于记录当前旋转方向, 方便后续使用;

transform, bounds, center 这些转换就不介绍了, 我们重点关注一下如何指定 window 显示方向; 在 fullscreenWindow 显示时, 调用了setNeedsUpdateOfSupportedInterfaceOrientations, 该方法的具体实现如下:


- (void)setNeedsUpdateOfSupportedInterfaceOrientations { 
    UIWindow *sourceWindow;
    UIWindow *fullscreenWindow;

    [sourceWindow.rootViewController setNeedsUpdateOfSupportedInterfaceOrientations];
    [fullscreenWindow.rootViewController setNeedsUpdateOfSupportedInterfaceOrientations];
}

这个方法是在 iOS 16.0 之后新增的方法, 用于更新界面当前支持的方向; 在调用更新后, 相应的 AppDelegate.application:supportedInterfaceOrientationsForWindow:将会被执行;

来看 AppDelegate 中添加的处理:


@implementation AppDelegate
- (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window { 
	UIInterfaceOrientation currentOrientation;
    if ( window == fullscreenWindow )
        return 1 << currentOrientation;
    return UIInterfaceOrientationMaskPortrait;
}
@end

如上, currentOrientation就是我们之前指定过的方向, 秘诀就是这里, 只返回单个指定的方向, window 最终就会以该方向呈现界面;

至此, 竖屏至横屏的处理算是完成了, 我们再稍微做一下优化, 将步骤2和步骤3调换顺序, 使得动画之前先显示 fullscreenWindow, 这会让整体的旋转体验流畅许多.

横屏转竖屏

处理过程如下:

1. 将播放器视图从 fullscreenWindow 中移除, 添加到 sourceWindow 中, 修改 transform, bounds, center, 要保持播放器视图在屏幕中的位置不变;
2. 设置 sourceWindow 为 keyWindow, 隐藏 fullscreenWindow;
3. 旋转回竖屏, 动画修改播放器视图的 transform, bounds, center等;
动画完毕后:
4. 将播放器视图从 sourceWindow 中移除, 添加回父视图中;

示例代码如下:


    // 横屏转竖屏示例代码如下: 
    UIInterfaceOrientation fromOrientation = UIInterfaceOrientationPortrait;
    UIInterfaceOrientation toOrientation = UIInterfaceOrientationLandscapeLeft;

    CGRect screenBounds = UIScreen.mainScreen.bounds;
    CGFloat maxSize = MAX(screenBounds.size.width, screenBounds.size.height);
    CGFloat minSize = MIN(screenBounds.size.width, screenBounds.size.height);

    // 播放器父视图
    UIView *playerSuperview; 
    // 播放器视图
    UIView *playerView;
    // App 本身的 window
    UIWindow *sourceWindow = playerSuperview.window;
    CGRect sourceFrame = [playerSuperview convertRect:playerSuperview.bounds toView:sourceWindow];
    // 播放器横屏 window
    UIWindow *fullscreenWindow;
	  
    // 0. 记录当前方向
    _currentOrientation = toOrientation;

    // 1. 将播放器视图从 fullscreenWindow 中移除, 添加到 sourceWindow 中, 修改 transform, bounds, center, 要保持播放器视图在屏幕中的位置不变; 
    [playerView removeFromSuperview];
    [playerView setBounds:(CGRect){ CGPointZero, (CGSize){maxSize, minSize} }];
    [playerView setCenter:(CGPoint){ minSize * 0.5, maxSize * 0.5 }];
    switch ( fromOrientation ) { 
        case UIInterfaceOrientationLandscapeLeft:
            playerView.transform = CGAffineTransformMakeRotation(M_PI_2);
        break;
        case UIInterfaceOrientationLandscapeRight:
            playerView.transform = CGAffineTransformMakeRotation(-M_PI_2);
        break;
    }
    [sourceWindow addSubview:playerView]; 

    // 2. 设置 sourceWindow 为 keyWindow, 隐藏 fullscreenWindow;
    [UIView performWithoutAnimation:^{
        [sourceWindow becomeKeyWindow];
        [fullscreenWindow setHidden:YES];
        [self setNeedsUpdateOfSupportedInterfaceOrientations];
    }];

    // 3. 旋转回竖屏, 动画修改播放器视图的 transform, bounds, center等;
    CGRect rotationBounds = (CGRect){ CGPointZero, sourceFrame.size };
    CGPoint rotationCenter = (CGPoint){
        sourceFrame.origin.x + rotationBounds.size.width * 0.5,
        sourceFrame.origin.y + rotationBounds.size.height * 0.5,
    };
    CGAffineTransform rotationTransform = CGAffineTransformIdentity;
    [UIView animateWithDuration:0.3 animations:^{ 
    	playerView.bounds = rotationBounds;
    	playerView.center = rootationCenter;
    	playerView.transform = rotationTransform;
    } completion:^(BOOL finished) {  
    	// 4. 将播放器视图从 sourceWindow 中移除, 添加回父视图中;	
    	[playerView removeFromSuperview];
        [playerSuperview addSubview:playerView];
        playerView.transform = CGAffineTransformIdentity;
        playerView.bounds = playerSuperview.bounds;
        playerView.center = (CGPoint){
            playerView.bounds.size.width * 0.5,
            playerView.bounds.size.height * 0.5
        };
    }];
    

完善

使用中发现横屏转回竖屏的时候, sourceWindow 会先变成横屏再变回竖屏, 可能导致某些页面布局发生偏移;

定位问题发现是 sourceWindow 在 makeKeyWindow 后, 由于当前 App 还处于横屏状态, window 会被系统设置为横屏, 再变回竖屏状态, 这个过程只有再设置 keyWindow 时触发;

问题很明显了, App 需要先恢复为竖屏, 再设置 sourceWindow 为 keyWindow;

于是我们新增了 portraitOrientationFixingWindow, 使其仅支持竖屏, 先设置他为 keyWindow, 用于修复 App 方向, 最后设置 sourceWindow 为真正的 keyWindow;

END

解决探索的过程很苦逼, 也很享受, 希望苹果每次更新都整点幺蛾子, 好让我继续写安卓;

项目地址: github.com/changsanjia…