項目上遇到這樣的需求,總體界面要橫屏,但是部分界面需要切換到豎屏,同時橫豎屏的界面都會有編輯框。
網上目前有很多資料涉及到這個的,安卓端實現很簡單,橫豎屏切換兩三行代碼就可以實現;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視圖的位移。