欢迎光临散文网 会员登陆 & 注册

WMS实战解决小米等手机桌面被强制横屏布局乱问题-车机手机必学课

2023-04-07 23:35 作者:千里马学框架  | 我要投稿

## 问题背景:

https://www.bilibili.com/video/BV17m4y1r7BC/

hi,同学们:

在平常工作中其实经常会比较诡异的问题,比如下面一个场景:

国内手机桌面基本不支持横屏,都是强制竖屏模式,所以对横屏基本没有适配对应的布局,所其实这些桌面是不希望看到有横屏情况展示出来,但是经常又会又一些小场景会导致桌面被强制横屏,所以看起来的体验比较差,就经常容易让测试提bug,用户体验也很糟糕

具体复现步骤现象如下:

更多内容qqun:422901085  (https://ke.qq.com/course/5992266#term_id=106217431)

写一个强制横屏Activity,且属于透明主题的:



//强制横屏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>


然后安装到桌面点击打开对比国内几个品牌手机桌面现象如何:



这里手上只有两家手机,对比了两个发现都属于被强制横屏后,就产生了显示异常情况,虽然这个时候桌面本身不再前台,触摸不了也没啥关系,但是毕竟显示起来毕竟难看。所以对于[学习了wms横竖旋转课程](https://ke.qq.com/course/package/83580?tuin=7d4eb354)的大家能忍么?遇到这类问题是不是刚好可以拿来练练手。那下面千里马就带大家开干,,把这个显示异常bug修复了



## 知识储备


首先回顾一下横竖动画旋转时候,如果Activity一般默认都会因为横竖屏幕变化后导致relauncher,但是大家有没有想过,如果我们Activity不响应横竖屏变化比如如下:

    android:configChanges = "orientation|screenSize"

这样变化横竖屏就不会导致relauncher,但是界面依然会变化

除了Activity,其实我们其他的window也是一样,比如statusbar,navigationbar等,也是不会relauncher阿,但是他们的绘制确实又是跟随变化的。relauncher变化大家都一般比较好理解,因为activity重新建立了,自然对应window会根据最新的width和height进行surface创建和布局

那么下面来学学习一下不进行relauncher靠啥来触发界面更新?


ensureVisibilityAndConfig  ->......... onConfigurationChanged ......->WindowState.onResize


这个我们知道configlation变化会触发WindowState.onResize,会把对应的windowstate加入到

 mWmService.mResizingWindows中

 然后在RootWindowContainer中的

 performSurfacePlacementNoTrace进行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);

        }

    }


这里会调用每一个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状态直接不处理,因为相当于activity新创建了

        if (mActivityRecord != null && mActivityRecord.isRelaunching()) {

            return;

        }

              //省略


        ProtoLog.v(WM_DEBUG_RESIZE, "Reporting new frame to %s: %s", this,

                mWindowFrames.mCompatFrame);

             //把当前绘制状态变成DRAW_PENDING,以前明明是HAS_DRAW

        final boolean drawPending = mWinAnimator.mDrawState == DRAW_PENDING;

               //省略

         //准备好相关的mClientWindowFrames数据

        fillClientWindowFramesAndConfiguration(mClientWindowFrames, mLastReportedConfiguration,

                true /* useLatestConfig */, false /* relayoutVisible */);

                //省略

                //跨进程调用到客户端的ViewRootImpl中的 W中执行resize

            mClient.resized(mClientWindowFrames, reportDraw, mLastReportedConfiguration,

                    getCompatInsetsState(), forceRelayout, alwaysConsumeSystemBars, displayId,

                    mSyncSeqId, resizeMode);

      

        //省略

    }




注释已经对关键部分做了说明,大概就是如果有relauncher那就不管,把mDrawState变成pending,没有的话那当然要通知客户端进行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);

    }


这里其实跨进程后只是发送了msg到主线程,具体执行如下:



  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;



调用到了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();

    }



这方法就主要进行对应configuration更新处理,还需要重新绘制及布局,这样才会重新触发客户端进行relayout相关的操作


## 问题产生原因

从以上其实既可以知道了,也就是不是说你activity不接受config变化就可以啥事没有,你只是不会被relauncher,但是屏幕size变化,和方向一样会通过各种方式传递过来,比如上面经典的resize这一条路径当然也有直接传递configration的方式,导致我们activity界面绘制不得不根据最新的屏幕size来,所以就导致了我们开始看到的现象,本来桌面不支持横屏显示,但是因为你桌面这个时候不是前台app,你前面支持横屏,而且它还是个透明的可以看到你,所以你也要从新被显示,这个时候你的configration又是横屏的。所以就不得不跟着横屏显示


## 解决方案

1、其实有一种很简单方案,那就让透明activity不要强制横屏显示,或设置orientation为behind,但是这个显然不太现实,因为这些属于第三方应用啥的,压没办法控制,所以从透明应用角度入手不合理哈,这个属于人家正规操作,桌面显示异常

2、桌面支持横屏显示,这个理论是可以的,但是也不太现实,因为桌面横屏修改等需要波及面还是比较多的,很多都是业务类工作,工作量较大,而且也只是为了修改一个这个体验性bug,性价比低


3、是否可以考虑在透明activity显示在桌面顶部且横屏的情况下,把桌面隐藏,只显示壁纸呢?其实这个相对来说体验也挺好,毕竟透明情况下看到桌面的画面不能点击对于用户也是一种不好体验,变成只有壁纸完全也可以接受,完全看不到桌面图表紊乱层叠的问题


那么这里就选方案3进行,具体修改如下:

在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()) {//判断桌面如果属于横屏就不进行显示了

+                    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();

+            }

+        }


```

主要是针对这里触发configration变化宽高后调用reportSize时候我们只需要针对launcher这个activity做特殊处理,给app跨进程传递当前状态是否需要隐藏自己界面,这里通知app方式,我们采用了一个取巧方式:

   mClient.closeSystemDialogs("launcher_landscape_not_show");

   用的是本身自带的closeSystemDialogs方法,它可以跨进程传递string到app且对其他的影响很小,因为懒着新加入一个aidl接口,这里相当于搭车了,当然方法大家也可以采用其他更加优雅方式,我们这里主要为了快点实现功能



接下来看看app的实现:


```cpp

public void dispatchCloseSystemDialogs(String reason) {

        if ("launcher_landscape_not_show".equals(reason)) { //对隐藏情况处理

            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会导致relayout传递visibility也被改变

                        viewGroup.setVisibility(INVISIBLE);

                    }

                }

            });

            android.util.Log.i("WindowManager","###dispatchCloseSystemDialogs reason = " + reason + "  mView.setVisibility(INVISIBLE);");

            return;

        }  else if ("launcher_landscape_show".equals(reason)) {//对显示情况处理

            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会导致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);

    }

```


好了最后看看我们修改的完美结果(因为没有手机厂商代码只能aosp给大家展示):


修改前,其实可以透看桌面已经被强制横屏,还好aosp桌面支持横屏,如果国内厂商桌面不支持就和我们最开始的那个问题一样


修改后


WMS实战解决小米等手机桌面被强制横屏布局乱问题-车机手机必学课的评论 (共 条)

分享到微博请遵守国家法律