WMS實(shí)戰(zhàn)解決小米等手機(jī)桌面被強(qiáng)制橫屏布局亂問(wèn)題-車機(jī)手機(jī)必學(xué)課

## 問(wèn)題背景:
https://www.bilibili.com/video/BV17m4y1r7BC/
hi,同學(xué)們:
在平常工作中其實(shí)經(jīng)常會(huì)比較詭異的問(wèn)題,比如下面一個(gè)場(chǎng)景:
國(guó)內(nèi)手機(jī)桌面基本不支持橫屏,都是強(qiáng)制豎屏模式,所以對(duì)橫屏基本沒(méi)有適配對(duì)應(yīng)的布局,所其實(shí)這些桌面是不希望看到有橫屏情況展示出來(lái),但是經(jīng)常又會(huì)又一些小場(chǎng)景會(huì)導(dǎo)致桌面被強(qiáng)制橫屏,所以看起來(lái)的體驗(yàn)比較差,就經(jīng)常容易讓測(cè)試提bug,用戶體驗(yàn)也很糟糕
具體復(fù)現(xiàn)步驟現(xiàn)象如下:
更多內(nèi)容qqun:422901085? (https://ke.qq.com/course/5992266#term_id=106217431)
寫一個(gè)強(qiáng)制橫屏Activity,且屬于透明主題的:
//強(qiáng)制橫屏AndroidManifest.xml
<activity
? ? ? ? ? ? android:name=".NoSplashMainActivity2"
? ? ? ? ? ? android:exported="true"
? ? ? ? ? ? android:screenOrientation = "landscape"
? ? ? ? ? ? android:label="@string/title_activity_no_splash_main2"
? ? ? ? ? ? android:theme="@style/AppTheme.NoActionBar">
? ? ? ? ? ? <!--? ? ? ? ? ? android:configChanges = "orientation|screenSize"-->
? ? ? ? ? ? <intent-filter>
? ? ? ? ? ? ? ? <action android:name="android.intent.action.MAIN" />
? ? ? ? ? ? ? ? <category android:name="android.intent.category.LAUNCHER" />
? ? ? ? ? ? </intent-filter>
? ? ? ? </activity>
//Activity透明主題,values/styles.xml
? ? <style name="AppTheme.NoActionBar">
? ? ? ? <item name="android:windowBackground">#00000000</item>
? ? ? ? <item name="windowActionBar">false</item>
? ? ? ? <item name="windowNoTitle">true</item>
? ? ? ? <item name="android:windowIsTranslucent">true</item>
? ? </style>
然后安裝到桌面點(diǎn)擊打開(kāi)對(duì)比國(guó)內(nèi)幾個(gè)品牌手機(jī)桌面現(xiàn)象如何:


這里手上只有兩家手機(jī),對(duì)比了兩個(gè)發(fā)現(xiàn)都屬于被強(qiáng)制橫屏后,就產(chǎn)生了顯示異常情況,雖然這個(gè)時(shí)候桌面本身不再前臺(tái),觸摸不了也沒(méi)啥關(guān)系,但是畢竟顯示起來(lái)畢竟難看。所以對(duì)于[學(xué)習(xí)了wms橫豎旋轉(zhuǎn)課程](https://ke.qq.com/course/package/83580?tuin=7d4eb354)的大家能忍么?遇到這類問(wèn)題是不是剛好可以拿來(lái)練練手。那下面千里馬就帶大家開(kāi)干,,把這個(gè)顯示異常bug修復(fù)了
## 知識(shí)儲(chǔ)備
首先回顧一下橫豎動(dòng)畫旋轉(zhuǎn)時(shí)候,如果Activity一般默認(rèn)都會(huì)因?yàn)闄M豎屏幕變化后導(dǎo)致relauncher,但是大家有沒(méi)有想過(guò),如果我們Activity不響應(yīng)橫豎屏變化比如如下:
? ? android:configChanges = "orientation|screenSize"
這樣變化橫豎屏就不會(huì)導(dǎo)致relauncher,但是界面依然會(huì)變化
除了Activity,其實(shí)我們其他的window也是一樣,比如statusbar,navigationbar等,也是不會(huì)relauncher阿,但是他們的繪制確實(shí)又是跟隨變化的。relauncher變化大家都一般比較好理解,因?yàn)閍ctivity重新建立了,自然對(duì)應(yīng)window會(huì)根據(jù)最新的width和height進(jìn)行surface創(chuàng)建和布局
那么下面來(lái)學(xué)學(xué)習(xí)一下不進(jìn)行relauncher靠啥來(lái)觸發(fā)界面更新?
ensureVisibilityAndConfig? ->......... onConfigurationChanged ......->WindowState.onResize
這個(gè)我們知道configlation變化會(huì)觸發(fā)WindowState.onResize,會(huì)把對(duì)應(yīng)的windowstate加入到
?mWmService.mResizingWindows中
?然后在RootWindowContainer中的
?performSurfacePlacementNoTrace進(jìn)行handleResizingWindows()
frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
?private void handleResizingWindows() {
? ? ? ? for (int i = mWmService.mResizingWindows.size() - 1; i >= 0; i--) {
? ? ? ? ? ? WindowState win = mWmService.mResizingWindows.get(i);
? ? ? ? ? ? if (win.mAppFreezing || win.getDisplayContent().mWaitingForConfig) {
? ? ? ? ? ? ? ? // Don't remove this window until rotation has completed and is not waiting for the
? ? ? ? ? ? ? ? // complete configuration.
? ? ? ? ? ? ? ? continue;
? ? ? ? ? ? }
? ? ? ? ? ? win.reportResized();
? ? ? ? ? ? mWmService.mResizingWindows.remove(i);
? ? ? ? }
? ? }
這里會(huì)調(diào)用每一個(gè)WindowState的reportResized方法:
frameworks/base/services/core/java/com/android/server/wm/WindowState.java
void reportResized() {
? ? ? ? if (mActivityRecord!= null && new ComponentName("com.android.launcher3","com.android.launcher3.uioverrides.QuickstepLauncher").equals(mActivityRecord.mActivityComponent)) {
? ? ? ? ? ? ? ? ? ? //省略
? ? ? ? ? ? ? ? ? ? //如果是isRelaunching狀態(tài)直接不處理,因?yàn)橄喈?dāng)于activity新創(chuàng)建了
? ? ? ? if (mActivityRecord != null && mActivityRecord.isRelaunching()) {
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? ? ? ? //省略
? ? ? ? ProtoLog.v(WM_DEBUG_RESIZE, "Reporting new frame to %s: %s", this,
? ? ? ? ? ? ? ? mWindowFrames.mCompatFrame);
? ? ? ? ? ? ?//把當(dāng)前繪制狀態(tài)變成DRAW_PENDING,以前明明是HAS_DRAW
? ? ? ? final boolean drawPending = mWinAnimator.mDrawState == DRAW_PENDING;
? ? ? ? ? ? ? ?//省略
? ? ? ? ?//準(zhǔn)備好相關(guān)的mClientWindowFrames數(shù)據(jù)
? ? ? ? fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration,
? ? ? ? ? ? ? ? true /* useLatestConfig */, false /* relayoutVisible */);
? ? ? ? ? ? ? ? //省略
? ? ? ? ? ? ? ? //跨進(jìn)程調(diào)用到客戶端的ViewRootImpl中的 W中執(zhí)行resize
? ? ? ? ? ? mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,
? ? ? ? ? ? ? ? ? ? getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,
? ? ? ? ? ? ? ? ? ? mSyncSeqId, resizeMode);
? ? ??
? ? ? ? //省略
? ? }
注釋已經(jīng)對(duì)關(guān)鍵部分做了說(shuō)明,大概就是如果有relauncher那就不管,把mDrawState變成pending,沒(méi)有的話那當(dāng)然要通知客戶端進(jìn)行resize
那么到客戶端看看:
frameworks/base/core/java/android/view/ViewRootImpl.java
? ? ? @Override
? ? ? ? public void resized(ClientWindowFrames frames, boolean reportDraw,
? ? ? ? ? ? ? ? MergedConfiguration mergedConfiguration, InsetsState insetsState,
? ? ? ? ? ? ? ? boolean forceLayout, boolean alwaysConsumeSystemBars, int displayId, int syncSeqId,
? ? ? ? ? ? ? ? int resizeMode) {
? ? ? ? ? ? final ViewRootImpl viewAncestor = mViewAncestor.get();
? ? ? ? ? ? if (viewAncestor != null) {
? ? ? ? ? ? ? ? viewAncestor.dispatchResized(frames, reportDraw, mergedConfiguration, insetsState,
? ? ? ? ? ? ? ? ? ? ? ? forceLayout, alwaysConsumeSystemBars, displayId, syncSeqId, resizeMode);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? ?@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
? ? private void dispatchResized(ClientWindowFrames frames, boolean reportDraw,
? ? ? ? ? ? MergedConfiguration mergedConfiguration, InsetsState insetsState, boolean forceLayout,
? ? ? ? ? ? boolean alwaysConsumeSystemBars, int displayId, int syncSeqId, int resizeMode) {
? ? ? ? Message msg = mHandler.obtainMessage(reportDraw ? MSG_RESIZED_REPORT : MSG_RESIZED);
? ? ? ? SomeArgs args = SomeArgs.obtain();
? ? ? ?//省略
? ? ? ? args.arg1 = sameProcessCall ? new ClientWindowFrames(frames) : frames;
? ? ? ? args.arg2 = sameProcessCall && mergedConfiguration != null
? ? ? ? ? ? ? ? ? new MergedConfiguration(mergedConfiguration) : mergedConfiguration;
? ? ? ? args.arg3 = insetsState;
? ? ? ? args.argi1 = forceLayout ? 1 : 0;
? ? ? ? args.argi2 = alwaysConsumeSystemBars ? 1 : 0;
? ? ? ? args.argi3 = displayId;
? ? ? ? args.argi4 = syncSeqId;
? ? ? ? args.argi5 = resizeMode;
? ? ? ? msg.obj = args;
? ? ? ? mHandler.sendMessage(msg);
? ? }
這里其實(shí)跨進(jìn)程后只是發(fā)送了msg到主線程,具體執(zhí)行如下:
? case MSG_RESIZED:
? ? ? ? ? ? ? ? case MSG_RESIZED_REPORT: {
? ? ? ? ? ? ? ? ? ? final SomeArgs args = (SomeArgs) msg.obj;
? ? ? ? ? ? ? ? ? ? mInsetsController.onStateChanged((InsetsState) args.arg3);
? ? ? ? ? ? ? ? ? ? handleResized(msg.what, args);
? ? ? ? ? ? ? ? ? ? args.recycle();
? ? ? ? ? ? ? ? ? ? break;
調(diào)用到了handleResized:
private void handleResized(int msg, SomeArgs args) {
? ? ? ? if (!mAdded) {
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? final ClientWindowFrames frames = (ClientWindowFrames) args.arg1;
? ? ? ? final MergedConfiguration mergedConfiguration = (MergedConfiguration) args.arg2;
? ? ? ? final boolean forceNextWindowRelayout = args.argi1 != 0;
? ? ? ? final int displayId = args.argi3;
? ? ? ? final int resizeMode = args.argi5;
? ? ? ? final Rect frame = frames.frame;
? ? ? ? final Rect displayFrame = frames.displayFrame;
? ? ? ? if (mTranslator != null) {
? ? ? ? ? ? mTranslator.translateRectInScreenToAppWindow(frame);
? ? ? ? ? ? mTranslator.translateRectInScreenToAppWindow(displayFrame);
? ? ? ? }
? ? ? ? final boolean frameChanged = !mWinFrame.equals(frame);
? ? ? ? final boolean configChanged = !mLastReportedMergedConfiguration.equals(mergedConfiguration);
? ? ? ? final boolean displayChanged = mDisplay.getDisplayId() != displayId;
? ? ? ? final boolean resizeModeChanged = mResizeMode != resizeMode;
? ? ? ? if (msg == MSG_RESIZED && !frameChanged && !configChanged && !displayChanged
? ? ? ? ? ? ? ? && !resizeModeChanged && !forceNextWindowRelayout) {
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? mPendingDragResizing = resizeMode != RESIZE_MODE_INVALID;
? ? ? ? mResizeMode = resizeMode;
? ? ? ? if (configChanged) {
? ? ? ? ? ? // If configuration changed - notify about that and, maybe, about move to display.
? ? ? ? ? ? performConfigurationChange(mergedConfiguration, false /* force */,
? ? ? ? ? ? ? ? ? ? displayChanged ? displayId : INVALID_DISPLAY /* same display */);
? ? ? ? } else if (displayChanged) {
? ? ? ? ? ? // Moved to display without config change - report last applied one.
? ? ? ? ? ? onMovedToDisplay(displayId, mLastConfigurationFromResources);
? ? ? ? }
? ? ? ? setFrame(frame);
? ? ? ? mTmpFrames.displayFrame.set(displayFrame);
? ? ? ? if (mDragResizing && mUseMTRenderer) {
? ? ? ? ? ? boolean fullscreen = frame.equals(mPendingBackDropFrame);
? ? ? ? ? ? for (int i = mWindowCallbacks.size() - 1; i >= 0; i--) {
? ? ? ? ? ? ? ? mWindowCallbacks.get(i).onWindowSizeIsChanging(mPendingBackDropFrame, fullscreen,
? ? ? ? ? ? ? ? ? ? ? ? mAttachInfo.mVisibleInsets, mAttachInfo.mStableInsets);
? ? ? ? ? ? }
? ? ? ? }
? ? ? ? mForceNextWindowRelayout = forceNextWindowRelayout;
? ? ? ? mPendingAlwaysConsumeSystemBars = args.argi2 != 0;
? ? ? ? mSyncSeqId = args.argi4 > mSyncSeqId ? args.argi4 : mSyncSeqId;
? ? ? ? if (msg == MSG_RESIZED_REPORT) {
? ? ? ? ? ? reportNextDraw();
? ? ? ? }
? ? ? ? if (mView != null && (frameChanged || configChanged)) {
? ? ? ? ? ? forceLayout(mView);
? ? ? ? }
? ? ? ? requestLayout();
? ? }
這方法就主要進(jìn)行對(duì)應(yīng)configuration更新處理,還需要重新繪制及布局,這樣才會(huì)重新觸發(fā)客戶端進(jìn)行relayout相關(guān)的操作
## 問(wèn)題產(chǎn)生原因
從以上其實(shí)既可以知道了,也就是不是說(shuō)你activity不接受config變化就可以啥事沒(méi)有,你只是不會(huì)被relauncher,但是屏幕size變化,和方向一樣會(huì)通過(guò)各種方式傳遞過(guò)來(lái),比如上面經(jīng)典的resize這一條路徑當(dāng)然也有直接傳遞configration的方式,導(dǎo)致我們activity界面繪制不得不根據(jù)最新的屏幕size來(lái),所以就導(dǎo)致了我們開(kāi)始看到的現(xiàn)象,本來(lái)桌面不支持橫屏顯示,但是因?yàn)槟阕烂孢@個(gè)時(shí)候不是前臺(tái)app,你前面支持橫屏,而且它還是個(gè)透明的可以看到你,所以你也要從新被顯示,這個(gè)時(shí)候你的configration又是橫屏的。所以就不得不跟著橫屏顯示
## 解決方案
1、其實(shí)有一種很簡(jiǎn)單方案,那就讓透明activity不要強(qiáng)制橫屏顯示,或設(shè)置orientation為behind,但是這個(gè)顯然不太現(xiàn)實(shí),因?yàn)檫@些屬于第三方應(yīng)用啥的,壓沒(méi)辦法控制,所以從透明應(yīng)用角度入手不合理哈,這個(gè)屬于人家正規(guī)操作,桌面顯示異常
2、桌面支持橫屏顯示,這個(gè)理論是可以的,但是也不太現(xiàn)實(shí),因?yàn)樽烂鏅M屏修改等需要波及面還是比較多的,很多都是業(yè)務(wù)類工作,工作量較大,而且也只是為了修改一個(gè)這個(gè)體驗(yàn)性bug,性價(jià)比低
3、是否可以考慮在透明activity顯示在桌面頂部且橫屏的情況下,把桌面隱藏,只顯示壁紙呢?其實(shí)這個(gè)相對(duì)來(lái)說(shuō)體驗(yàn)也挺好,畢竟透明情況下看到桌面的畫面不能點(diǎn)擊對(duì)于用戶也是一種不好體驗(yàn),變成只有壁紙完全也可以接受,完全看不到桌面圖表紊亂層疊的問(wèn)題
那么這里就選方案3進(jìn)行,具體修改如下:
在WidnowState類的reportResized方法加入如下:
```cpp
? ? ?void reportResized() {
? ? ? ? // If the activity is scheduled to relaunch, skip sending the resized to ViewRootImpl now
? ? ? ? // since it will be destroyed anyway. This also prevents the client from receiving
? ? ? ? // windowing mode change before it is destroyed.
? ? ? ? if (mActivityRecord != null && mActivityRecord.isRelaunching()) {
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ??
+? ? ? ? if (mActivityRecord!= null && new ComponentName("com.android.launcher3","com.android.launcher3.uioverrides.QuickstepLauncher").equals(mActivityRecord.mActivityComponent)) {
+? ? ? ? ? ? android.util.Log.i("WindowManager","#### mActivityRecord1 = " + mActivityRecord + " mWindowFrames = " + mWindowFrames.mCompatFrame);
+? ? ? ? ? ? try {
+
+? ? ? ? ? ? ? ? if (mWindowFrames.mCompatFrame.width() > mWindowFrames.mCompatFrame.height()) {//判斷桌面如果屬于橫屏就不進(jìn)行顯示了
+? ? ? ? ? ? ? ? ? ? if (mActivityRecord.isVisible()) {
+? ? ? ? ? ? ? ? ? ? ? ? android.util.Log.i("WindowManager","#### closeSystemDialogs launcher_landscape_not_show? mActivityRecord = " + mActivityRecord + " mWindowFrames = " + mWindowFrames);
+? ? ? ? ? ? ? ? ? ? ? ? mClient.closeSystemDialogs("launcher_landscape_not_show");//通知讓launcher不要顯示
+? ? ? ? ? ? ? ? ? ? }
+? ? ? ? ? ? ? ? } else {
+? ? ? ? ? ? ? ? ? ? mClient.closeSystemDialogs("launcher_landscape_show");//通知讓launcher顯示
+? ? ? ? ? ? ? ? }
+? ? ? ? ? ? }catch (Exception e) {
+? ? ? ? ? ? ? ? e.printStackTrace();
+? ? ? ? ? ? }
+? ? ? ? }
```
主要是針對(duì)這里觸發(fā)configration變化寬高后調(diào)用reportSize時(shí)候我們只需要針對(duì)launcher這個(gè)activity做特殊處理,給app跨進(jìn)程傳遞當(dāng)前狀態(tài)是否需要隱藏自己界面,這里通知app方式,我們采用了一個(gè)取巧方式:
? ?mClient.closeSystemDialogs("launcher_landscape_not_show");
? ?用的是本身自帶的closeSystemDialogs方法,它可以跨進(jìn)程傳遞string到app且對(duì)其他的影響很小,因?yàn)閼兄录尤胍粋€(gè)aidl接口,這里相當(dāng)于搭車了,當(dāng)然方法大家也可以采用其他更加優(yōu)雅方式,我們這里主要為了快點(diǎn)實(shí)現(xiàn)功能
接下來(lái)看看app的實(shí)現(xiàn):
```cpp
public void dispatchCloseSystemDialogs(String reason) {
? ? ? ? if ("launcher_landscape_not_show".equals(reason)) { //對(duì)隱藏情況處理
? ? ? ? ? ? mHandler.post(new Runnable() {
? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? public void run() {
? ? ? ? ? ? ? ? ? ? if (mView instanceof? DecorView) {
? ? ? ? ? ? ? ? ? ? ? ? ViewGroup viewGroup = ((DecorView)mView).findViewById(ID_ANDROID_CONTENT);//注意這里只能ID_ANDROID_CONTENT不能mView哦,如果直接mView會(huì)導(dǎo)致relayout傳遞visibility也被改變
? ? ? ? ? ? ? ? ? ? ? ? viewGroup.setVisibility(INVISIBLE);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? ? ? android.util.Log.i("WindowManager","###dispatchCloseSystemDialogs reason = " + reason + "? mView.setVisibility(INVISIBLE);");
? ? ? ? ? ? return;
? ? ? ? }? else if ("launcher_landscape_show".equals(reason)) {//對(duì)顯示情況處理
? ? ? ? ? ? mHandler.post(new Runnable() {
? ? ? ? ? ? ? ? @Override
? ? ? ? ? ? ? ? public void run() {
? ? ? ? ? ? ? ? ? ? if (mView instanceof? DecorView) {
? ? ? ? ? ? ? ? ? ? ? ? ViewGroup viewGroup = ((DecorView)mView).findViewById(ID_ANDROID_CONTENT);//注意這里只能ID_ANDROID_CONTENT不能mView哦,如果直接mView會(huì)導(dǎo)致relayout傳遞visibility也被改變
? ? ? ? ? ? ? ? ? ? ? ? viewGroup.setVisibility(VISIBLE);
? ? ? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? }
? ? ? ? ? ? });
? ? ? ? ? ? android.util.Log.i("WindowManager","###dispatchCloseSystemDialogs reason = " + reason + "? mView.setVisibility(VISIBLE);");
? ? ? ? ? ? return;
? ? ? ? }
? ? ? ? Message msg = Message.obtain();
? ? ? ? msg.what = MSG_CLOSE_SYSTEM_DIALOGS;
? ? ? ? msg.obj = reason;
? ? ? ? mHandler.sendMessage(msg);
? ? }
```
好了最后看看我們修改的完美結(jié)果(因?yàn)闆](méi)有手機(jī)廠商代碼只能aosp給大家展示):
修改前,其實(shí)可以透看桌面已經(jīng)被強(qiáng)制橫屏,還好aosp桌面支持橫屏,如果國(guó)內(nèi)廠商桌面不支持就和我們最開(kāi)始的那個(gè)問(wèn)題一樣



修改后
