1、初识Window
val layoutParams = WindowManager.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE shl WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL shl WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
windowManager.addView(imageView,layoutParams)
复制代码
- Window类型
- 系统级WIndow:系统级别的Window需要声明权限才能创建,如Toast
- 应用级Window:系统的活动窗口,如:Activity
- 子Window:附属在父Window中,如:Dialog
- FLAG
- FLAG_NOT_FOCUSABLE:表示不获取焦点,事件最终会传递到底层的Window
- FLAG_NOT_TOUCH_MODEL:只处理自己区域内的点击事件,区域之外的传递给底层Window
- FLAG_SHOW_WHEN_LOCKED:开启此模式可以显示在锁屏上
2、Window工作过程
Window是一个抽象概念,它是以View的形式存在,每个Window都对应着View和ViewRootImpl,Window和View之间通过ViewRootImpl建立联系
2.1、Window的添加过程
Window的整个添加过程可分为两部分执行:
- WindowManager
- WindowManagerService
View的添加是从调用windowManager.addView()开始的,其实点开windowManager只是一个继承ViewManager的接口,在活动中真正执行任务的是它的实现类WindowMangerImpl,因此方法会执行到WindowMangerImpl.addView(),但WindowMangerImpl 是个聪明的类,在addView()中除了验证设置LayoutParams的合法性之外,它又将所有的工作都桥接给WindowManagerGlobal执行:
@Override
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);//验证params的合法性
mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow); // 直接交给WindowManagerGlobal处理
}
复制代码
- WindowManagerGlobal
在具体执行方法前先介绍下WindowManagerGlobal中的各个集合的作用(见下面注释),在Window工作的整个过程他们时刻保存着Window和View的运行状态
private final ArrayList<View> mViews = new ArrayList<View>(); // 保存所有Window对应的View
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>(); // 保存所有Window对应的ViewRootImpl
private final ArrayList<WindowManager.LayoutParams> mParams = // 保存所有Window对应的WindowManager.LayoutParams
new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>(); // 保存正在被删除的View
复制代码
- WindowManagerGlobal.addView()
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
root.setView(view, wparams, panelParentView); // View的绘制
复制代码
上面是addView()中的部分代码,它执行了以下几个操作:
- 创建ViewRootImpl的实例
- 设置View的布局参数
- 分别在集合中保存view、root和params
在保存了相关数据后,View真正的执行就是setView()这一句开始,下面看看ViewRootImpl中是如何实现View的测量绘制的
- ViewRootImpl.setView()
ViewRootImpl是View中的最高层级,属于所有View的根(但ViewRootImpl不是View,只是实现了ViewParent接口),实现了View和WindowManager之间的通信协议
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
......
requestLayout();//对View进行第一次测量和绘制
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel); //调用WindowSession的addTodiaplay()添加窗口
}
}
复制代码
requestLayout()内调用scheduleTraversals(),scheduleTraversals()中 会获取主线程的Handler然后发送消息执行TraversalRunnable实例,TraversalRunnable是Runnable的实现类,在run()方法中执行oTraversal() ,然后方法会执行到performTraversals()
- performTraversals()
//调用performMeasure完成Window内视图的测量
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;
if (lp.horizontalWeight > 0.0f) {
width += (int) ((mWidth - width) * lp.horizontalWeight);
childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(width,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (lp.verticalWeight > 0.0f) {
height += (int) ((mHeight - height) * lp.verticalWeight);
childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(height,
MeasureSpec.EXACTLY);
measureAgain = true;
}
if (measureAgain) {
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
}
......
final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
|| mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
performLayout(lp, mWidth, mHeight); //完成View的布局Layout
}
......
performDraw();//对View的绘制
复制代码
performTraversals方法中,依次调用了performMeasure、performLayout、performDraw三个方法,这三个方法中又分别调用View或ViewGroupde的measure、layout和draw方法,完成了View的测量、布局和绘制;
- WindowSession使用Binder机制调用IWindowSession接口,内部调用WindowManagerService.addWindow()添加,到此所有的操作就执行到了WindowManagerService中,关于WindowManagerService的工作过程请参考Android窗口管理分析
@Override
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets,
Rect outOutsets, InputChannel outInputChannel) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
outContentInsets, outStableInsets, outOutsets, outInputChannel);
}
复制代码
2.2、Window删除过程
删除过程和添加一样最后执行任务的都是WindowManagerGlobal,先看下WindowManagerGlobal的removeView()方法:
int index = findViewLocked(view, true);
View curView = mRoots.get(index).getView();
removeViewLocked(index, immediate);
复制代码
removeView()中主要执行三个步骤:
- 获取当前操作View的index
- 获取mRoots中保存的ViewRootImpl实例
- 调用removeViewLocked执行删除
- removeViewLocked():获取要删除的View执行删除操作
ViewRootImpl root = mRoots.get(index);
View view = root.getView(); // 获取ViewRootImpl保存的View
boolean deferred = root.die(immediate); // 调用die()执行删除View
mDyingViews.add(view); // 将要删除的View添加到mDyingViews
复制代码
- die():发送删除消息
boolean die(boolean immediate) {
if (immediate && !mIsInTraversal) {
doDie();
return false;
}
mHandler.sendEmptyMessage(MSG_DIE);
return true;
}
复制代码
在die()方法中根据传入的immediate执行同步或异步删除:
- 同步删除:直接调用doDie()方法执行删除
- 异步删除:发送Handler消息调用doDie()
- doDie():真正执行View的删除
mView.dispatchDetachedFromWindow();
mWindowSession.remove(mWindow);
mView.onDetachedFromWindow();
WindowManagerGlobal.getInstance().doRemoveView(this);
复制代码
doDie是真正发起删除的地方,执行操作如下:
- 调用用mWindowSession最终调用WindowMangerService.removeWindow()
- 调用View的onDetachedFromWindow()执行View的移除操作
- 移除mRoots、mParams、mDyingView中保存的View信息
2.3、Window更新过程
- WindowManagerGlobal.updateViewLayout()
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
view.setLayoutParams(wparams); // 设置新的LyaoutParams
synchronized (mLock) {
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index); //更新root,mParams集合中的数据
mParams.remove(index);
mParams.add(index, wparams);//替换mParams中保存的wparams
root.setLayoutParams(wparams, false); // 更新View
}
}
复制代码
执行过程见上面注释,在 root.setLayoutParams中会触发ViewRootImpl的scheduleTraversals实现View的测量、布局、绘制;
3、实例
3.1、Activity中Window的添加
借用网络上的一幅图展示Activity的层次关系:- PhoneWindow:Activity的活动窗口
- DecorView:所有视图的根View,其中包含标题栏和content
- ContentView:布局容器设置的layout文件被加载到其中
- window的创建
- 在Activity.attach()方法中使用PolicyManager.makeNewWindow()创建PhoneWIndow
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
复制代码
- window设置视图
//Activity中
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID); //调用PhoneWindow的setContentView()
initWindowDecorActionBar();
}
复制代码
此处的getWindow()得到的就是前面创建的PhoneWindow ,所以setContentView()最终是在PhoneWindow中执行的
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
}
复制代码
setContentView()方法中,首先判断contentParent是否空,如果为空则执行installDecor(),installDecor()中有两处代码比较明显,分别是初始化DecorView和mContentParent,下面分别看看这两个方法
- generateDecor():创建DecorView实例
protected DecorView generateDecor(int featureId) {
return new DecorView(context, featureId, this, getAttributes()); //初始化DecorView,此时只是一个FrameLayout
}
复制代码
- generateLayout():加载布局文件并初始化mContentParent
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//根据加载后的布局查找content
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT); // 加载DecorView布局中的content容器
public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content; //content的id
//mDecor.onResourcesLoaded()
final View root = inflater.inflate(layoutResource, null); //加载原始布局文件:包含标题栏和content
addView(root,new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT)); //将加载的布局添加到DecorView中
复制代码
在generateLayout中完成了布局layout文件的加载,具体细节如下:
- 加载getWindowStyle中的属性值
- 根据设置的style初始化Layout的WindowManager.LayoutParams 和选择系统的布局资源layoutResource
- 设置DecorView的背景、标题、颜色等状态
- 然后调用mDecor.onResourcesLoaded()加载layoutResource到DecorView中
- 根据资源id获取布局中的contentParent容器
- 将View添加到DecorView的contentParent容器中
@Override
public void setContentView(int layoutResID) {
mLayoutInflater.inflate(layoutResID, mContentParent); //加载布局到mContentParent中
}
复制代码
- 加载完布局后回调onContentChange(),通知Activity加载完毕
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
复制代码
到此DecorView和contentParent初始化已经完成,DecorView中加载了一个具有TitleView和ContentView的布局,并且加载的layoutResID也已加载到ContentView中,所以关于DecorView内部的工作已经完成,但DecorView未被添加到Window中,所以此时界面仍是不可见
- DecorView添加到Window()
ActivityThread的handleResumeActivity()中调用Activity的makeVisible()方法,makeVisible中调用WindowManager.addView()将DecorView添加到PhoneWindow中,到此布局资源展示在屏幕上
//handleResumeActivity
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes()); //将DecorView添加到PhoneWindow中
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE); //设置DecorView可见
}
复制代码
3.2、Dialog中Window的添加
- Dialog使用
val dialog = Dialog(this,R.style.Base_ThemeOverlay_AppCompat_Dialog)
dialog.setContentView(R.layout.dialog)
dialog.show()
dialog.cancel()
复制代码
- 创建Window
从上面使用可以看出,dialog设置布局时和Activity都是使用setContentView,所以其执行初始化的过程和Activity一致,只是在将DecorView添加到Window时有所不同
- 将DecorView添加到Window
public void show() { //在Dialog显示时添加到Window中
mWindowManager.addView(mDecor, l); // 添加DecorView
}
复制代码
- dialog关闭时通过WindowManager移除DectorView
mWindowManager.removeViewImmediate(mDecor);
复制代码
3.3、Toast中Window的创建
- 使用
Toast.makeText(this,"Toast",Toast.LENGTH_SHORT).show()
复制代码
- makeText():makeText执行了Toast的文件加载和和设置
Toast result = new Toast(context, looper); //创建Toast实例,并传入队列Loop
LayoutInflater inflate = (LayoutInflater)
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); //加载Toast布局并设置View
TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
tv.setText(text); //设置Toast的信息
result.mNextView = v; // 复制给mNextView
result.mDuration = duration; //设置Toast的弹出时长
return result;
复制代码
- Toast的显示
INotificationManager service = getService();
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
service.enqueueToast(pkg, tn, mDuration);
复制代码
针对上面方法中做几点说明:
- service是INotificationManager的代理类,此处是IPC通信;
- TN 是ITransientNotification的代理类
- mNextView是本次Toast加载的View
- service.enqueueToast()将Toast加入消息队列
Toast最终回调TN中的show方法,show()中发送Message到Handle,然后调用handleShow()
public void handleShow(IBinder windowToken) {
handleHide();
mParams.x = mX;
mParams.y = mY;
mParams.verticalMargin = mVerticalMargin;
mParams.horizontalMargin = mHorizontalMargin;
mParams.packageName = packageName;
mParams.hideTimeoutMilliseconds = mDuration ==
Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
mParams.token = windowToken;
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
mWM.addView(mView, mParams);
}
复制代码
handleShow()中执行以下操作:
- 调用handleHide()隐藏前一个Toast
- 设置Toast的mParams参数,如:坐标、mDuration
- 调用WindowManager的addView()添加View
WindowManagerService是如何执行Window的添加和操作的?