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的新增和操作的?