前言
從Android app的視角看,Window是比較抽象的概念,它是View的承載者。而WindowManager顧名思義是Window的管理者,通過addView方法將View添加到Window里最終展示到屏幕上。
系列文章:
Window/WindowManager 不可不知之事
Android Window 如何確定大小/onMeasure()多次執行原因
通過本篇文章,你將了解到:
1、Window/WindowManager 創建、屬性及其使用
2、WindowManager.LayoutParams flag屬性之key/touch事件
3、View如何與Window關聯
4、WindowManager常用場景
Window/WindowManager 創建與使用
先看看一個簡單的添加View到Window的過程
private void showView() {
//獲取WindowManager實例,這里的App是繼承自Application
WindowManager wm = (WindowManager) App.getApplication().getSystemService(Context.WINDOW_SERVICE);
//設置LayoutParams屬性
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
layoutParams.height = 400;
layoutParams.width = 400;
layoutParams.format = PixelFormat.RGBA_8888;
//窗口標記屬性
layoutParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//Window類型
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
} else {
layoutParams.type = WindowManager.LayoutParams.TYPE_PHONE;
}
//構造TextView
TextView textView = new TextView(this);
textView.setBackground(new ColorDrawable(Color.WHITE));
textView.setText("hello windowManager");
//將textView添加到WindowManager
wm.addView(textView, layoutParams);
}
效果如下:
以上代碼分三個部分看:
1、獲取WindowManager對象
2、設置LayoutParams屬性
3、將View添加到Window里
I 獲取WindowManager對象
App是繼承自Application,App.getApplication()獲取當前應用的application實例,其本身也是Context。關于Context請移步:Android各種Context的前世今生
public interface WindowManager extends ViewManager
WindowManager是個接口,繼承了ViewManager,ViewManager也是個接口,來看看它的內容:
public interface ViewManager
{
//添加View, view 表示內容本身,params表示對此view位置、大小等屬性的限制
public void addView(View view, ViewGroup.LayoutParams params);
//更新view
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
//移除View
public void removeView(View view);
}
既然WindowManager是個接口,那么必然有實現它的類,答案就在:getSystemService(Context.WINDOW_SERVICE)里。
ContextImpl.java
@Override
public Object getSystemService(String name) {
return SystemServiceRegistry.getSystemService(this, name);
}
SystemServiceRegistry.java
public static Object getSystemService(ContextImpl ctx, String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
registerService(Context.WINDOW_SERVICE, WindowManager .class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService (ContextImpl ctx){
return new WindowManagerImpl(ctx);
}
});
- WindowManager實現類是WindowManagerImpl
WindowManagerImpl 內容并不多
public final class WindowManagerImpl implements WindowManager {
//WindowManagerImpl 代理類 WindowManagerGlobal單例
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
//getSystemService傳進來的Context
private final Context mContext;
//記錄構造WindowManager父Window
private final Window mParentWindow;
//關聯Activity時會賦值
private IBinder mDefaultToken;
public WindowManagerImpl(Context context) {
this(context, null);
}
private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}
public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
return new WindowManagerImpl(mContext, parentWindow);
}
//省略
}
- 其實現的add/update/remove方法最終交由WindowManagerGlobal實現。
- WindowManagerGlobal記錄著該App內所有展示的Window一些相關信息
II 設置LayoutParams屬性
WindowManager.LayoutParams 繼承自ViewGroup.LayoutParams,來看看一些我們關注的屬性:
width : 指定Window的寬度
height : 指定Window的高度
x : Window在屏幕X軸的偏移(偏移的起點是gravity設置的位置)
y : Window在屏幕Y軸的偏移(偏移的起點是gravity設置的位置)
flags :控制Window一些行為,比如能否讓下層的Window獲得點擊事件,Window能否超出屏幕展示等
type :Window類型,分為三種:
FIRST_APPLICATION_WINDOW ~ LAST_APPLICATION_WINDOW(1~99)應用窗口
FIRST_SUB_WINDOW ~ LAST_SUB_WINDOW (1000 ~ 1999)子窗口
FIRST_SYSTEM_WINDOW ~ LAST_SYSTEM_WINDOW (2000 ~ 2999)系統窗口
數值越大,層級越高,也就是層級越高的就能顯示在層級低的上邊。
gravity : Window的位置,取值自Gravity
windowAnimations : Window動畫
該例我們設置的type屬于系統窗口,系統窗口需要用戶開啟權限,對應的是設置里的:“顯示在其他應用的上層”
在Activity里檢查并獲取應用的方法如下:
public void onClick(View view) {
if (checkPermission(this)) {
showView();
} else {
Intent intent = getPermissionIntent(this);
if (intent != null) {
try {
startActivityForResult(intent, 100);
} catch (Exception e) {
Log.d("hello", "error");
}
} else {
}
}
}
public static boolean checkPermission(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return Settings.canDrawOverlays(context);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
int op = 24;
AppOpsManager manager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
try {
Class clazz = AppOpsManager.class;
Method method = clazz.getDeclaredMethod("checkOp", int.class, int.class, String.class);
return AppOpsManager.MODE_ALLOWED == (int) method.invoke(manager, op, Binder.getCallingUid(), context.getPackageName());
} catch (Exception e) {
return false;
}
} else {
return true;
}
}
public static Intent getPermissionIntent(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + context.getPackageName()));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
String brand = Build.BRAND;
if (TextUtils.isEmpty(brand)) {
return null;
}
return null;
} else {
return null;
}
}
@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == 100) {
if (checkPermission(this)) {
showView();
}
}
}
當然還需要在AndroidManifest.xml里聲明使用的權限
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
III 將View添加到Window里
wm.addView(textView, layoutParams)
WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
if (parentWindow != null) {
//調整LayoutParams
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
//省略
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
//構造ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
//用數組記錄
//mViews 存放添加到Window的view
//mRoots 存放ViewRootImpl
//mParams 存放Window參數
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
//調用ViewRootImpl setView
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
}
}
//省略
}
真正實現窗口的添加是通過ViewRootImpl setView(xx)方法
ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
//省略
int res;
//提交View展示請求(測量、布局、繪制),只是提交到隊列里
//當屏幕刷新信號到來之時從隊列取出執行
requestLayout();
try {
//添加到窗口
//進程間通信,告訴WindowManagerService為我們開辟一個Window空間
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
} catch (RemoteException e) {
} finally {
}
if (res < WindowManagerGlobal.ADD_OKAY) {
//窗口添加失敗拋出各種異常
}
//Window根View的mParent是ViewRootImpl 而其他View的mParent是其父控件
//這參數是向上遍歷View Tree的關鍵
view.assignParent(this);
//輸入事件相關 touch、key事件接收
CharSequence counterSuffix = attrs.getTitle();
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
}
}
}
關于requestLayout()請移步:Android Activity創建到View的顯示過程
使用Binder方式,ViewRootImpl與WindowManagerService建立Session進行通信
mWindowSession.addToDisplay 簡單來看看后續調用(有興趣的可以深入源碼看看)。
Session.java
public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
outInsetsState);
}
WindowManagerService.java
public int addWindow(Session session, IWindow client, int seq,
LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
InsetsState outInsetsState) {
//省略
}
WindowManager.LayoutParams flag屬性之key/touch事件
我們知道Activity實際上也是通過Window展示的,現在Activity之上添加了另一個Window,那么key/touch事件是如何決定分發給哪個Window呢?
如上圖所示,Window2 是在Window1之上,層級比Window1高,決定Window2 key/touch事件是否分發給Window1取決于WindowManager.LayoutParams flag 參數,flag默認為0。結合上圖來看看一些常用的值及其作用,當Window2使用如下參數時:
public static final int FLAG_NOT_TOUCHABLE = 0x00000010;
public static final int FLAG_NOT_FOCUSABLE = 0x00000008;
public static final int FLAG_NOT_TOUCH_MODAL = 0x00000020;
public static final int FLAG_WATCH_OUTSIDE_TOUCH = 0x00040000;
public static final int FLAG_ALT_FOCUSABLE_IM = 0x00020000;
flag默認為0,不對flag設置時,Window2默認接受所有的touch/key 事件,即使點擊區域不在Window2的范圍內。
FLAG_NOT_TOUCHABLE
表示Window 不接收所有的touch事件。此時無論點擊Window2 區域還是Window2之外的區域,touch事件都分發給了下一層Window1。而key事件則不受影響。
FLAG_NOT_FOCUSABLE
表示Window不接收輸入焦點,不和鍵盤交互。比如當Window里使用editText時,是無法彈出鍵盤的。另外一個作用就是:當點擊Window2之外的區域時,touch事件分發給了Window1,而點擊Window2區域是分發給了其自身,key事件也不會分發給Window2,而是給了Window1(該作用相當于設置了FLAG_NOT_TOUCH_MODAL)。
FLAG_NOT_TOUCH_MODAL
表示當點擊Window2之外的區域時,touch事件分發給了Window1,而key事件不受影響。當然此時Window2是能獲取焦點的,能和鍵盤交互。
FLAG_WATCH_OUTSIDE_TOUCH
該值配合FLAG_NOT_TOUCH_MODAL才會生效。意思就是當設置了FLAG_NOT_TOUCH_MODAL時,點擊Window2外部區域其收不到touch事件,但是這個時候Window2想要收到外部點擊的事件,同時又不影響事件分發給Window1,此時FLAG_WATCH_OUTSIDE_TOUCH標記就發揮其作用了。此Window2接收到ACTION_OUTSIDE類型的事件,而touch事件(down/move/up)則分發給了Window1。key事件不受影響。
FLAG_ALT_FOCUSABLE_IM
與鍵盤相關。當FLAG_NOT_FOCUSABLE沒有設置且FLAG_ALT_FOCUSABLE_IM設置時,表示無需與鍵盤交互。當FLAG_NOT_FOCUSABLE/FLAG_ALT_FOCUSABLE_IM同時設置時,表示需要與輸入法交互。FLAG_ALT_FOCUSABLE_IM單獨設置時不影響touch/key 事件。
View如何與Window關聯
通過前面的分析,并沒有發現View和Window的直接關聯,那么View的內容怎么顯示在Window上的呢?
Surface與Canvas
平時我們都是重寫View onDraw(Canvas canvas),通過Canvas繪制我們想要的效果,來看看Canvas是怎么來的:
對于軟件繪制:
ViewRootImpl.java
public final Surface mSurface = new Surface();
final Canvas canvas = mSurface.lockCanvas(dirty);
可以看出,Canvas是從Surface獲取的,那自然想到Surface和Window是否有關系呢,是怎么關聯呢?
ViewRootImpl.java
private int relayoutWindow(WindowManager.LayoutParams params, int viewVisibility,
boolean insetsPending) throws RemoteException {
//省略
//傳入SurfaceControl,在WindowManagerService里處理
int relayoutResult = mWindowSession.relayout(mWindow, mSeq, params,
(int) (mView.getMeasuredWidth() * appScale + 0.5f),
(int) (mView.getMeasuredHeight() * appScale + 0.5f), viewVisibility,
insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, frameNumber,
mTmpFrame, mPendingOverscanInsets, mPendingContentInsets, mPendingVisibleInsets,
mPendingStableInsets, mPendingOutsets, mPendingBackDropFrame, mPendingDisplayCutout,
mPendingMergedConfiguration, mSurfaceControl, mTempInsets);
if (mSurfaceControl.isValid()) {
//返回App層的Surface
mSurface.copyFrom(mSurfaceControl);
} else {
destroySurface();
}
//省略
return relayoutResult;
}
在View開啟ViewTree三大流程時,performTraversals->relayoutWindow,將Window與SurfaceControl關聯,進而關聯Surface。這樣,Window->Surface->Canvas就關聯起來了,通過Canvas將View繪制到Surface上,最終顯示出來。
而對于硬件加速來說:
每個View都有RenderNode
RenderNode.java
public @NonNull RecordingCanvas beginRecording(int width, int height) {
if (mCurrentRecordingCanvas != null) {
throw new IllegalStateException(
"Recording currently in progress - missing #endRecording() call?");
}
mCurrentRecordingCanvas = RecordingCanvas.obtain(this, width, height);
return mCurrentRecordingCanvas;
}
繪制該View的Canvas通過beginRecording獲取,Canvas繪制的操作封裝在DisplayList。
在ViewRootImpl->performTraversals
hwInitialized = mAttachInfo.mThreadedRenderer.initialize(
mSurface);
建立ThreadedRenderer和Surface關聯,而ThreadedRenderer里持有:
protected RenderNode mRootNode;
該mRootNode是整個ViewTree的根node。這樣Surface和Canvas建立了關聯。
用圖表示View、Window、Surface關系:
Window內容是通過Surface展示,而SurfaceFlinger將多個Surface合成顯示在屏幕上。
ViewManager 其他方法
上面說了添加View到Window的addView(xx)方法,接下來看看updateViewLayout(xx)和removeView(xx)方法
updateViewLayout(xx)
WindowManagerGlobal.java
public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
//設置View的params
view.setLayoutParams(wparams);
synchronized (mLock) {
//找到目標View在數組中的位置
int index = findViewLocked(view, true);
ViewRootImpl root = mRoots.get(index);
//移除舊的params
mParams.remove(index);
//添加新的params
mParams.add(index, wparams);
//ViewRootImpl重新設置params
//最終按需開啟View的三大流程
root.setLayoutParams(wparams, false);
}
}
removeView(xx)
public void removeView(View view, boolean immediate)
immediate 表示是否立即移除View,如果是false,那么通過Handler發送Message,等待下次Looper輪詢后執行。
具體工作是在ViewRootImpl里的doDie()。
void doDie() {
synchronized (this) {
//通知View已經移除
if (mAdded) {
dispatchDetachedFromWindow();
}
if (mAdded && !mFirst) {
destroyHardwareRenderer();
if (mView != null) {
try {
//通知WindowManagerService重新布局
if ((relayoutWindow(mWindowAttributes, viewVisibility, false)
& WindowManagerGlobal.RELAYOUT_RES_FIRST_TIME) != 0) {
mWindowSession.finishDrawing(mWindow);
}
} catch (RemoteException e) {
}
}
//移除surface
destroySurface();
}
}
mAdded = false;
}
//移除WindowManagerGlobal記錄的信息,比如ViewRootImpl、View數組等
WindowManagerGlobal.getInstance().doRemoveView(this);
}
WindowManager常用場景
Android里的界面展示都是通過WindowManager.addView(xx),也就是說我們看到的界面都是有個Window的。只是Window比較抽象,我們更多接觸的是View。
Activity
Activity實際上也是通過Window展示界面的,只是系統封裝好了addView的過程。我們只需要setContentView(resId),將我們的布局傳入即可。
關于setContentView(resId),請移步:Android DecorView 一窺全貌(上)
Dialog
Dialog內部也是通過addView(xx)展示
PopupWindow
與Dialog類似,只是沒有PhoneWindow
Toast
Toast與其他的系統彈框等...只要界面展示都會用到addView(xx)
Dialog/PopupWindow/Toast 更詳細的差異請移步:
Dialog PopupWindow Toast 你還有疑惑嗎