PipMenuView原理讲解-车载车机手机安卓framework系统开发

文章详细介绍了Android系统中的PipMenuView是如何创建和添加到屏幕的,包括从window添加、SurfaceFlinger图层、WindowManagerService的交互过程,以及如何通过RemoteAction和PictureInPictureParams定制悬浮窗菜单的图标和行为。
摘要由CSDN通过智能技术生成

先来了解一下的PipMenuView的是啥:

b站免费视频教程讲解:
https://www.bilibili.com/video/BV1wj411o7A9/
在这里插入图片描述

正常的情况下pip显示
在这里插入图片描述
触摸小窗pip后:
在这里插入图片描述
是否发现又多了3个按钮,但是这个窗口到底是啥呢?
这个想要知道这个画面是谁,发现dumpsys还看不出具体的窗口,最后还是通过SurfaceFlinger图层发现它的名字的:
在这里插入图片描述
知道了名字后就可以开始入手寻找。

先来看看对应的PipMenuView的创建调用堆栈

05-24 15:29:53.270   753   753 I PipDemo : attachPipMenuView
05-24 15:29:53.270   753   753 I PipDemo : java.lang.Exception
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.wm.shell.pip.phone.PhonePipMenuController.attachPipMenuView(PhonePipMenuController.java:187)
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.wm.shell.pip.phone.PhonePipMenuController.attach(PhonePipMenuController.java:164)
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.wm.shell.pip.PipTaskOrganizer.onTaskAppeared(PipTaskOrganizer.java:627)
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.wm.shell.ShellTaskOrganizer.updateTaskListenerIfNeeded(ShellTaskOrganizer.java:555)
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.wm.shell.ShellTaskOrganizer.onTaskInfoChanged(ShellTaskOrganizer.java:465)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.window.TaskOrganizer$1.lambda$onTaskInfoChanged$6$android-window-TaskOrganizer$1(TaskOrganizer.java:316)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.window.TaskOrganizer$1$$ExternalSyntheticLambda3.run(Unknown Source:4)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.os.Handler.handleCallback(Handler.java:942)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.os.Handler.dispatchMessage(Handler.java:99)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.os.Looper.loopOnce(Looper.java:201)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.os.Looper.loop(Looper.java:288)
05-24 15:29:53.270   753   753 I PipDemo : 	at android.app.ActivityThread.main(ActivityThread.java:7897)
05-24 15:29:53.270   753   753 I PipDemo : 	at java.lang.reflect.Method.invoke(Native Method)
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
05-24 15:29:53.270   753   753 I PipDemo : 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:937)

再看看对应的window添加代码:
frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java

void attachPipMenuView() {
        // In case detach was not called (e.g. PIP unexpectedly closed)
        if (mPipMenuView != null) {
            detachPipMenuView();
        }
        mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler,
                mSplitScreenController, mPipUiEventLogger);
                //注意这里调用是mSystemWindows的addView,和正常的WindowGlobal不一样哈
        mSystemWindows.addView(mPipMenuView,
                getPipMenuLayoutParams(MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */),
                0, SHELL_ROOT_LAYER_PIP);
        setShellRootAccessibilityWindow();
        // Make sure the initial actions are set
        updateMenuActions();
    }
    
    frameworks/base/libs/WindowManager/Shell/src/com/android/wm/shell/common/SystemWindows.java
     /**
     * Adds a view to system-ui window management.
     */
    public void addView(View view, WindowManager.LayoutParams attrs, int displayId,
            @WindowManager.ShellRootLayer int shellRootLayer) {
        PerDisplay pd = mPerDisplay.get(displayId);
        if (pd == null) {
            pd = new PerDisplay(displayId);
            mPerDisplay.put(displayId, pd);
        }
        pd.addView(view, attrs, shellRootLayer);
    }

这里又发现是调用了PerDisplay的addView

private class PerDisplay {
        final int mDisplayId;
        private final SparseArray<SysUiWindowManager> mWwms = new SparseArray<>();

        PerDisplay(int displayId) {
            mDisplayId = displayId;
        }

        public void addView(View view, WindowManager.LayoutParams attrs,
                @WindowManager.ShellRootLayer int shellRootLayer) {
            SysUiWindowManager wwm = addRoot(shellRootLayer);//根据shellRootLayer为SHELL_ROOT_LAYER_PIP创建出对应的SysUiWindowManager,SysUiWindowManager继承WindowlessWindowManager名字即可以看出这个东西其实没有windowstate的,WindowlessWindowManager实现了IWindowSession,这里session就和以前的session接口实现完全不一样,以前都是一般直接调用wms的接口,这里全部进行了重写,这里也就说明的为啥一定要通过session而不是直接调用wms接口了,就是为了这种动态扩展
           
            final Display display = mDisplayController.getDisplay(mDisplayId);
            //构造对应的SurfaceControlViewHost,会构造出对应的Viewrootimpl
            SurfaceControlViewHost viewRoot =
                    new SurfaceControlViewHost(
                            view.getContext(), display, wwm, true /* useSfChoreographer */);
            attrs.flags |= FLAG_HARDWARE_ACCELERATED;
            viewRoot.setView(view, attrs);//这里调用了SurfaceControlViewHost的setView,其实也最后会调用ViewRootImpl的setView
            mViewRoots.put(view, viewRoot);
            setShellRootAccessibilityWindow(shellRootLayer, view);
        }
  SysUiWindowManager addRoot(@WindowManager.ShellRootLayer int shellRootLayer) {
            SysUiWindowManager wwm = mWwms.get(shellRootLayer);
            if (wwm != null) {
                return wwm;
            }
            SurfaceControl rootSurface = null;
            ContainerWindow win = new ContainerWindow();//这里直接new的一个IWindow的binder对象进行添加到wms
            try {
                //获取一个SurfaceControl来放置pipmenuview,注意这里和不一样,以前都是wms帮忙创建对应的SurfaceControl,所以这里就是为啥dumpsys window没办法看到这里的PipMenuView的窗口
                rootSurface = mWmService.addShellRoot(mDisplayId, win, shellRootLayer);
            } catch (RemoteException e) {
            }
            //通过上面的rootSurface等构造出对应的SysUiWindowManager
            wwm = new SysUiWindowManager(mDisplayId, displayContext, rootSurface, win);
            mWwms.put(shellRootLayer, wwm);
            return wwm;
        }

看看wms端的addShellRoot:

//frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
    @Override
    public SurfaceControl addShellRoot(int displayId, IWindow client,
            @WindowManager.ShellRootLayer int shellRootLayer) {//最后目的就是获取一个可以挂载pipmenuview的SurfaceControl
      
        try {
            synchronized (mGlobalLock) {
                final DisplayContent dc = mRoot.getDisplayContent(displayId);
                if (dc == null) {
                    return null;
                }
                return dc.addShellRoot(client, shellRootLayer);
            }
        }
    }
    //frameworks/base/services/core/java/com/android/server/wm/DisplayContent.java
       SurfaceControl addShellRoot(@NonNull IWindow client,
            @WindowManager.ShellRootLayer int shellRootLayer) {
        ShellRoot root = mShellRoots.get(shellRootLayer);
       //省略
        root = new ShellRoot(client, this, shellRootLayer);//创建一个ShellRoot
        SurfaceControl rootLeash = root.getSurfaceControl();//获取一个root的SurfaceControl
         //省略
        mShellRoots.put(shellRootLayer, root);
        SurfaceControl out = new SurfaceControl(rootLeash, "DisplayContent.addShellRoot");
        return out;//把rootLeash的备份返回
    }
    //frameworks/base/services/core/java/com/android/server/wm/ShellRoot.java
ShellRoot(@NonNull IWindow client, @NonNull DisplayContent dc,
            @WindowManager.ShellRootLayer final int shellRootLayer) {
     //省略
        mClient = client;
        switch (shellRootLayer) {
            case SHELL_ROOT_LAYER_DIVIDER:
                mWindowType = TYPE_DOCK_DIVIDER;
                break;
            case SHELL_ROOT_LAYER_PIP:
                mWindowType = TYPE_APPLICATION_OVERLAY;//给出对应的window type
                break;
            default:
                throw new IllegalArgumentException(shellRootLayer
                        + " is not an acceptable shell root layer.");
        }
        //这里看到熟悉的创建了对应的windowtoken,因为有TYPE_APPLICATION_OVERLAY可以挂到层级结构树上面
        mToken = new WindowToken.Builder(dc.mWmService, client.asBinder(), mWindowType)
                .setDisplayContent(dc)
                .setPersistOnEmpty(true)
                .setOwnerCanManageAppTokens(true)
                .build();
         //这里以windowtoken为父亲创建了一个图层Shell Root Leash
        mSurfaceControl = mToken.makeChildSurface(null)
                .setContainerLayer()
                .setName("Shell Root Leash " + dc.getDisplayId())
                .setCallsite("ShellRoot")
                .build();
        mToken.getPendingTransaction().show(mSurfaceControl);
    }
    //frameworks/base/core/java/android/view/SurfaceControlViewHost.java
 /** @hide */
    public SurfaceControlViewHost(@NonNull Context c, @NonNull Display d,
            @NonNull WindowlessWindowManager wwm, boolean useSfChoreographer) {
        mWm = wwm;
        mViewRoot = new ViewRootImpl(c, d, mWm, useSfChoreographer);//创建出对应的ViewRootImpl
        addConfigCallback(c, d);//添加相关configcallback

        WindowManagerGlobal.getInstance().addWindowlessRoot(mViewRoot);//注意在调用是addWindowlessRoot哈
//省略
    }

上面代码已经清楚到了ViewRootImpl已经把对应的PipMenuView设置到了ViewRootImpl,但是好像并没有看到PipMenuView这个图层有添加到SurfaceFlinger图层,其实核心还是在ViewRootImpl

PipMenuView图层的添加到SurfaceFlinger图层的过程

以前ViewRootImpl的setView是不是有个

 res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(), userId,
                            mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
                            mTempControls);

注意这里的mWindowSession已经是我们的WindowlessWindowManager它对这里有进行对应的重写:


 public int addToDisplayAsUser(IWindow window, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, int userId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
        return addToDisplay(window, attrs, viewVisibility, displayId, requestedVisibilities,
                outInputChannel, outInsetsState, outActiveControls);
    }
    @Override
    public int addToDisplay(IWindow window, WindowManager.LayoutParams attrs,
            int viewVisibility, int displayId, InsetsVisibilities requestedVisibilities,
            InputChannel outInputChannel, InsetsState outInsetsState,
            InsetsSourceControl[] outActiveControls) {
            //注意这里就是创建了一个画面图层名字就是PipMenuView
        final SurfaceControl.Builder b = new SurfaceControl.Builder(mSurfaceSession)
                .setFormat(attrs.format)
                .setBLASTLayer()
                .setName(attrs.getTitle().toString())
                .setCallsite("WindowlessWindowManager.addToDisplay");
        attachToParentSurface(window, b);//把PipMenuView图层挂到前面说的rootSurface
        final SurfaceControl sc = b.build();
  //省略
    }
      protected void attachToParentSurface(IWindow window, SurfaceControl.Builder b) {
        b.setParent(mRootSurface);
    }

上面就已经讲解清楚了PipMenuView这个画面是怎么一回事,为啥dumpsys window看不到,但是surfaceflinger结构树可以看到
在这里插入图片描述

时序图:
在这里插入图片描述

pip展示MenuView时候app自定义相关 button原理:

app层是可以控制的:

 if (mPictureInPictureParamsBuilder == null) {
                mPictureInPictureParamsBuilder = new PictureInPictureParams.Builder();
            }
            RemoteAction action = new RemoteAction(Icon.createWithResource(this,R.mipmap.pasue),"hello","world", PendingIntent.getActivity(
                    this, 1,
                    new Intent(),
                    PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT
            ));
            List<RemoteAction> list =new ArrayList<>();
            list.add(action);
            list.add(action);
            list.add(action);
            mPictureInPictureParamsBuilder.setActions(list);
            if (videoView != null) {

                // Calculate the aspect ratio of the PiP screen. 计算video的纵横比
                int mVideoWith = videoView.getWidth();
                int mVideoHeight = videoView.getHeight();
                Log.i("lsm","enterPiPMode mVideoWith = " + mVideoWith + " mVideoHeight = " + mVideoHeight);
                if (mVideoWith != 0 && mVideoHeight != 0) {
                    //设置param宽高比,根据宽高比例调整初始参数
                    Rational aspectRatio = new Rational(mVideoWith, mVideoHeight);
                    mPictureInPictureParamsBuilder.setAspectRatio(aspectRatio);
                }

            }

展示出来的效果如下:
在这里插入图片描述

可以看到多了3个图标,这个就是google留给app自己可以定制的接口部分
这里最主要采用的方案就是RemoteView方案,因为PipMenuView属于systemui进程
这里几个ActionView属于各个app进程自己定制的。

在这里插入图片描述

remoteview也可以响应相关的点击事件,不过都是通过pendingintent方式
具体可以参考千里马课程demo

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

千里马学框架

帮助你了,就请我喝杯咖啡

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值