本文主要介绍 SJVideoPlayer 中旋转模块针对 iOS 16.0 的处理完成进程;

先看效果图:

旋转功能主要是在两个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…