项目上遇到这样的需求,总体界面要横屏,但是部分界面需要切换到竖屏,同时横竖屏的界面都会有编辑框。
网上目前有很多资料涉及到这个的,安卓端实现很简单,横竖屏切换两三行代码就可以实现;ios端网上目前也有方案,比安卓稍微复杂点,但是也可以实现。但是涉及到界面上有编辑框,会弹出输入键盘的时候,ios端的界面就会出现异常。目前引擎对于编辑框的处理,在弹出键盘的时候,整体的ui界面会上移,使输入区域高于键盘,这样方便编辑的时候显示正在编辑的内容。但是ios端横竖屏切换了之后,弹出虚拟键盘之后,ui界面并没有正常的上移,而且虚拟键盘弹回之后,ui界面没有回到正常的位置,这里需要讨论的就是这个问题。
以下涉及到源码的地方,使用的是cocos2dx 3.10 版本的引擎,我对比了一下最新版本的引擎(3.17.1)这些代码大致上是一致的,我在两个版本的引擎上都实现了这里要讨论的功能。另外,针对的是游戏默认是横屏,在某些情况下需要切换到竖屏而实现的方案。如果游戏默认是竖屏,而在某些情况下需要切换到横屏,可能思路是一致,但是具体的改动可能会有差异。
最新调整:
解决方案中我有提到,新增了一个获取设备方向的方法,在手动旋转屏幕的时候记录当前状态栏是否是横屏,在返回设备方向,如下所示:
UIInterfaceOrientation getFixedOrientation2(BOOL landscape) { if (landscape){ return UIInterfaceOrientationPortrait; } return UIInterfaceOrientationLandscapeRight; }
引擎默认的方法是根据状态栏的方向来返回一个固定的设备方向。一开始我在测试的时候,因为没有固定状态栏,所以我在引擎已提供的方法基础上去获取所需要的设备方向,是有问题的,即 [[UIApplication sharedApplication] statusBarOrientation] 这个方法返回的方向,是实际的状态栏方向,手持设备竖屏、横屏都会返回实际的方向。但是后来,我们在选装屏幕的时候,不固定状态栏方向,会导致输入法弹出方向有问题,即ui显示横屏的时候,设备竖屏,此时输入法是以竖屏的方式弹出来。所以,在我们已经固定了状态栏方向的时候,就可以在引擎已经提供的方法的基础上,进行修改,而不用再增加额外的方法和变量了,如下:
UIInterfaceOrientation getFixedOrientation(UIInterfaceOrientation statusBarOrientation) { if (statusBarOrientation == UIInterfaceOrientationLandscapeLeft || statusBarOrientation == UIInterfaceOrientationLandscapeRight){ return UIInterfaceOrientationPortrait; } return UIInterfaceOrientationLandscapeRight; }
问题描述:
直接定位到引擎开启/弹出虚拟键盘的地方,在源码UIEditBox.cpp里面有实现:
以下是开启/弹出键盘的方法
void EditBox::openKeyboard() const { _editBoxImpl->openKeyboard(); }
引擎执行完这个之后,ios端通过 CCEAGLView-ios.mm 里面的 onUIKeyboardNotification 方法,调用到以下 UIEditBox.cpp 的方法
void EditBox::keyboardWillShow(IMEKeyboardNotificationInfo& info) { ... if (_editBoxImpl != nullptr) { _editBoxImpl->doAnimationWhenKeyboardMove(info.duration, _adjustHeight); } }
在这里有一个方法 doAnimationWhenKeyboardMove,追踪到 UIEditBoxImpl-ios.mm 里面
void EditBoxImplIOS::doAnimationWhenKeyboardMove(float duration, float distance) { if ([_systemControl isEditState] || distance < 0.0f) { [_systemControl doAnimationWhenKeyboardMoveWithDuration:duration distance:distance]; } }
再追踪到 CCUIEditBoxIOS.mm
- (void)doAnimationWhenKeyboardMoveWithDuration:(float)duration distance:(float)distance { ... [eaglview doAnimationWhenKeyboardMoveWithDuration:duration distance:distance]; }
再追踪到 CCEAGLView-ios.mm ,上部分代码
-(void) doAnimationWhenKeyboardMoveWithDuration:(float)duration distance:(float)dis { ... switch (getFixedOrientation([[UIApplication sharedApplication] statusBarOrientation])) { case UIInterfaceOrientationPortrait: self.frame = CGRectMake(originalRect_.origin.x, originalRect_.origin.y - dis, originalRect_.size.width, originalRect_.size.height); break;case UIInterfaceOrientationLandscapeRight: self.frame = CGRectMake(originalRect_.origin.x + dis, originalRect_.origin.y , originalRect_.size.width, originalRect_.size.height); break; default: break; } ... }
熟悉ios的应该知道,ui界面上移的动画就是在这里实现的,位移距离是 dis 变量控制,位移时间是 duration 变量控制。而游戏横竖屏切换之后,在弹出虚拟键盘导致ui界面显示异常的问题,也是在这里出现的,我们要调整的就是在这个地方。
解决方案:
在上面断点追踪键盘弹出的执行过程中,onUIKeyboardNotification 方法中有涉及到坐标的装换、位置的计算,doAnimationWhenKeyboardMoveWithDuration 方法则是具体的执行ui界面的位移,所以出现界面异常应该是这两个地方的计算出现了问题。
首先断点调试 onUIKeyboardNotification 方法,从命名来看这是一个监听,在系统发出准备弹出键盘、弹出键盘等一系列消息的时候会调用这个方法,另外在垂直方向这个case里面,我们看到了对y坐标进行了调整,很像我们弹出虚拟键盘的时候界面上移的效果,这里上部分代码
switch (getFixedOrientation([[UIApplication sharedApplication] statusBarOrientation])) {
... case UIInterfaceOrientationPortrait: begin.origin.y = viewSize.height - begin.origin.y - begin.size.height; end.origin.y = viewSize.height - end.origin.y - end.size.height; break;case UIInterfaceOrientationLandscapeRight: std::swap(begin.size.width, begin.size.height); std::swap(end.size.width, end.size.height); std::swap(viewSize.width, viewSize.height); tmp = begin.origin.x; begin.origin.x = begin.origin.y; begin.origin.y = tmp; tmp = end.origin.x; end.origin.x = end.origin.y; end.origin.y = tmp; break; ... }
这个switch根据当前系统状态栏的方向,来对坐标、尺寸信息做不同的装换。这里有一个方法 getFixedOrientation 会返回方向信息
UIInterfaceOrientation getFixedOrientation(UIInterfaceOrientation statusBarOrientation) { if ([[[UIDevice currentDevice] systemVersion] floatValue] >= 8.0) { statusBarOrientation = UIInterfaceOrientationPortrait; } return statusBarOrientation; }
从这个方法的实现来看,隐约感觉有问题:如果系统版本大于8.0,则返回的是一个固定的方向,如果我们做了横竖屏切换,那么这个方向应该是不同的才对吧?
所以我首先改了这个地方:
UIInterfaceOrientation getFixedOrientation2(BOOL landscape) { if (landscape){ return UIInterfaceOrientationPortrait; } return UIInterfaceOrientationLandscapeRight; }
返回不是固定的反向值,而是会根据当前选择/设置的设备方向进行调整,这里起来是一个反的,比如状态栏选择了横屏,但是返回了一个竖屏方向。在这里我主要是考虑,设备横屏的时候,弹出的虚拟键盘界面位移是正常的,上文提到的修改y坐标值思路是对的。
屏幕旋转之后,界面也应该是往上位移,所以我把 onUIKeyboardNotification 中switch的UIInterfaceOrientationLandscapeRight case里面的代码也调整了一下,跟竖屏方向的调整一样:
case UIInterfaceOrientationLandscapeRight: begin.origin.y = viewSize.height - begin.origin.y - begin.size.height; end.origin.y = viewSize.height - end.origin.y - end.size.height; break;
本来以为改好了,重新编译运行之后,发现还是有问题,所以进入到下一步修改。上文中提到 doAnimationWhenKeyboardMoveWithDuration 这个方法是最终动画执行的地方,这个方法里面也是根据设备方向做不同的操作,而且横屏状态下切换正常,竖屏异常,就直接看竖屏状态下切换的case,因为竖屏状态,getFixedOrientation返回的是横屏的方向,所以看横屏的case
case UIInterfaceOrientationLandscapeRight: self.frame = CGRectMake(originalRect_.origin.x + dis, originalRect_.origin.y , originalRect_.size.width, originalRect_.size.height); break;
从代码来看,位移操作是x轴移动dis距离,然后宽高是正常的原始宽高。但是我们在实际的体验上来看,竖屏的时候,看起来也应该是y轴的位移,且此时的宽高正好是跟横屏时相反的,所以这里先调整一下:
case UIInterfaceOrientationLandscapeRight: self.frame = CGRectMake(originalRect_.origin.x, originalRect_.origin.y - dis, originalRect_.size.height, originalRect_.size.width); break;
重新编译再执行一下,ui偏移正常了。
但是又发现了新的问题,比如我们界面显示横屏的时候,我们把设备竖着拿,此时再点编辑框弹出键盘,发现键盘是竖着弹出来的,这个也不对;同样,界面显示竖屏的时候,我们把设备横着拿,此时弹出的键盘是横着的。网上查阅资料发现,ios键盘弹出方向,是根据状态栏方向来的。所以我们在旋转屏幕的时候,同时也要锁定状态栏的方向,这里涉及到 AppController.mm/RootViewController.mm 两个部分的代码修改
static bool bRotate=false; -(NSUInteger)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window{ if (bRotate){ [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait]; } else{ [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft]; } return UIInterfaceOrientationMaskAllButUpsideDown; }
上面的操作就是锁定了状态栏方向,
if (bIsLeft){ [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationPortrait]; //[[UIDevice currentDevice] performSelector:@selector(setOrientation:) withObject:(id)UIDeviceOrientationPortrait]; SEL selector = NSSelectorFromString(@"setOrientation:"); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]]; [invocation setSelector:selector]; [invocation setTarget:[UIDevice currentDevice]]; int val = UIDeviceOrientationPortrait;//这里可以改变旋转的方向 [invocation setArgument:&val atIndex:2]; [invocation invoke]; } else{ [[UIApplication sharedApplication] setStatusBarOrientation:UIInterfaceOrientationLandscapeLeft]; //[[UIDevice currentDevice] performSelector:@selector(setOrientation:) withObject:(id)UIDeviceOrientationLandscapeRight]; SEL selector = NSSelectorFromString(@"setOrientation:"); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[UIDevice instanceMethodSignatureForSelector:selector]]; [invocation setSelector:selector]; [invocation setTarget:[UIDevice currentDevice]]; int val = UIDeviceOrientationLandscapeRight;//这里可以改变旋转的方向 [invocation setArgument:&val atIndex:2]; [invocation invoke]; }
上面的代码是旋转设备方向。网上也有另外一个方法:
[[UIDevice currentDevice] performSelector:@selector(setOrientation:) withObject:bIsLeft ? (id)UIDeviceOrientationPortrait : (id)UIDeviceOrientationLandscapeRight];
这个方法有个问题,如果设备选择了锁定竖屏,那么从横屏切换到竖屏,界面不会正常的旋转,需要拉一下状态栏才会旋转,我也不知道为什么。。。
总结:
本次修改,主要改了三个文件 AppController.mm 、RootViewController.mm、CCEAGLView-ios.mm,三个文件。要注意的地方就是,修改了屏幕旋转,如果此时还需要使用输入功能,那么还需要做进一步的改动,以适应虚拟键盘带来的ui视图的位移。