本文主要介绍 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…