先来了解一下的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