在雲手機使用情境中,使用者操作的是運行於雲端的虛擬設備,而本地終端僅作為顯示與互動的載體。當雲手機發生螢幕方向變化(如從豎屏切換至橫屏)時,若本地用戶端無法同步響應,將導致畫面顯示異常(如倒置、展開、黑邊)或互動錯位,嚴重影響使用者體驗。本文介紹如何在Android與iOS用戶端SDK中實現本地螢幕方向與雲端旋轉指令的精準同步機制,確保本地介面與視頻流始終與雲端裝置保持一致的螢幕朝向,從而提供沈浸式、無感知的遠程操作體驗。
背景
傳統移動端應用通常依賴裝置感應器或系統配置自動響應旋轉螢幕。然而在雲手機架構下,螢幕方嚮應由雲端虛擬設備的狀態驅動,而非本地物理裝置的姿態。若直接啟用系統自動旋轉,會導致以下問題:
本地旋轉與雲端狀態不同步,畫面顯示錯誤。
使用者無意中轉動手機即觸發旋轉,幹擾正常操作。
視頻渲染層(如 SurfaceView/TextureView 或 Metal/OpenGL View)未適配新方向,出現裁剪或形變。
因此必須主動接管旋轉控制權,由雲端下發明確的方向指令,本地用戶端據此強制切換介面方向並同步調整視頻渲染邏輯。
方案概述
本方案在Android與iOS平台上分別實現了以下核心能力:
統一的旋轉指令接收機制
通過自訂DataChannel(如
wy_vdagent_default_dc)接收來自雲端的旋轉命令。使用標準化協議解析旋轉參數(如
rotation = 0/1/3分別對應豎屏、左橫屏、右橫屏)。
本地介面方向強制同步
Android:調用
setRequestedOrientation()動態鎖定Activity方向。iOS:重寫
supportedInterfaceOrientations並結合setNeedsUpdateOfSupportedInterfaceOrientations(iOS 16+)或私人API實現方向切換。兩者均禁用裝置自動旋轉(
shouldAutorotate = false/ 禁用感應器響應),確保僅響應雲端指令。
視頻渲染層精準適配
Android:啟用
TextureView並調用setSurfaceRotation()直接旋轉底層紋理,避免畫面失真。iOS:通過
CGAffineTransform對StreamView進行旋轉,並動態調整其中心點,確保畫面置中且比例正確。
多方向支援
同時支援Portrait(豎屏)、Landscape Left(左橫屏)和Landscape Right(右橫屏)三種方向。
正確處理Home鍵位置差異,提升iOS裝置在橫屏下的互動一致性。
實現步驟
Android SDK本地旋轉處理
// 禁止雲手機橫豎屏自適應
bundle.putBoolean(StreamView.CONFIG_DISABLE_ORIENTATION_CLOUD_CONTROL, true);
// 使用textureView
mStreamView = findViewById(R.id.stream_view);
mStreamView.enableTextureView(true);
//旋轉處理
mStreamView.getASPEngineDelegate().addDataChannel(new DataChannel("wy_vdagent_default_dc") {
@Override
protected void onReceiveData(byte[] buf) {
String str = "";
try {
str = new String(buf, "UTF-8");
} catch (UnsupportedEncodingException e) {
str = new String(buf);
}
Log.i(TAG, "wy_vdagent_default_dc dc received " + buf.length + " bytes data:" + str);
CommandUtils.parseCommand(str, new CommandUtils.CommandListener() {
@Override
public void onCameraAuthorize() {
checkStartCpd();
}
@Override
public void onRotation(int rotation) {
runOnUiThread(() -> {
mStreamView.setSurfaceRotation(rotation);
if (rotation == 1) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else if (rotation == 3) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
});
}
@Override
public void onUnknownCommand(String cmd) {
showError("未知命令:" + cmd);
}
});
}
@Override
protected void onConnectStateChanged(DataChannelConnectState state) {
Log.i(TAG, "wy_vdagent_default_dc dc connection state changed to " + state);
}
});iOS SDK本地旋轉處理
list.info配置Supported interface orientations,啟用應用對豎屏、左橫屏、右橫屏的支援。<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <array> <string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeRight</string> </array> </plist>建立
BaseViewController實現旋轉相關邏輯。@implementation BaseViewController { NSInteger mRoration; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; } - (void)switchRoration:(NSInteger)roration { mRoration = roration; // 強制旋轉到橫屏(iOS 16+ 推薦方式) if (@available(iOS 16.0, *)) { [self setNeedsUpdateOfSupportedInterfaceOrientations]; } else { // 舊方法(已廢棄,但相容) NSNumber *value = @([self supportedInterfaceOrientations]); [[UIDevice currentDevice] setValue:value forKey:@"orientation"]; } } - (UIInterfaceOrientationMask)supportedInterfaceOrientations { switch (mRoration) { case 1: return UIInterfaceOrientationMaskLandscapeLeft; case 3: return UIInterfaceOrientationMaskLandscapeRight; default: return UIInterfaceOrientationMaskPortrait; } } - (BOOL)shouldAutorotate { return false; // 允許自動旋轉到 supported 的方向 } // 可選:指定首選方向(用於 presentation) - (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation { switch (mRoration) { case 1: return UIInterfaceOrientationLandscapeLeft; case 3: return UIInterfaceOrientationLandscapeRight; default: return UIInterfaceOrientationPortrait; } } @end建立從雲端到本地UI與渲染層的指令響應通路。
self.esdAgent = [[DemoEDSAgentChannel alloc] initWithParameter:DATA_CHANNEL_NAME]; self.esdAgent.streamView = self.streamView; self.esdAgent.viewController = self; [self.streamView addDataChannel:self.esdAgent];監聽並響應雲手機旋轉指令。
@interface DemoEDSAgentChannel() <CommandListener> @property (nonatomic, assign) CGRect rect; @end @implementation DemoEDSAgentChannel - (void)setViewController:(BaseViewController *)viewController { self.rect = viewController.view.bounds; _viewController = viewController; } - (void)onConnectStateChanged:(ASPDCConnectState)state { NSLog(@"[DemoEDSAgentChannel] onConnectStateChanged %ld", state); if (state == OPEN) { // to send data } } - (void)onReceiveData:(NSData * _Nonnull)buf { NSString *string = [[NSString alloc] initWithData:buf encoding:NSUTF8StringEncoding]; NSLog(@"[DemoEDSAgentChannel] onConnectStateChanged %@", string); [CommandUtils parseCommand:string listener:self]; } #pragma mark - CommandListener - (void)onRotation:(NSInteger)value { NSLog(@"[DemoEDSAgentChannel] onRotation %ld", value); dispatch_async(dispatch_get_main_queue(), ^{ [self.viewController switchRoration:value]; if (value == 1 || value == 3) { self.streamView.center = CGPointMake(CGRectGetMidY(self.viewController.view.bounds), CGRectGetMidX(self.viewController.view.bounds)); self.streamView.transform = CGAffineTransformMakeRotation(-M_PI_2*value) ; } else { self.streamView.center = CGPointMake(CGRectGetMidX(self.rect), CGRectGetMidY(self.rect)); self.streamView.transform = CGAffineTransformIdentity; } }); }