从一个Dialog的创建看Android的窗口机制(上篇)和从一个Dialog的创建看Android的窗口机制(下篇) 从应用类型窗口的创建分析了Android的窗口机制,这篇打算从系统窗口再来分析下,看看不同类型窗口的添加有何异同,选了一个比较简单的截屏窗口来看,我们都知道Android默认提供了截屏功能,通过POWER+音量下键可以完成截屏,接着从代码角度看看截屏窗口是如何添加的
在SystemUI有个TakeScreenshotService负责截屏
public class TakeScreenshotService extends Service {
......
private Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (mScreenshot == null) {
mScreenshot = new GlobalScreenshot(TakeScreenshotService.this);
}
.....
switch (msg.what) {
case WindowManager.TAKE_SCREENSHOT_FULLSCREEN:
mScreenshot.takeScreenshot(finisher, msg.arg1 > 0, msg.arg2 > 0);
break;
.....
}
}
核心实现在GlobalScreenshot里面
GlobalScreenshot构造方法中初始化了很多截屏窗口的信息,注意这个context,传的是TakeScreenshotService.this,这是个service类型的context
public GlobalScreenshot(Context context) {
......
//加载截屏窗口的布局global_screenshot
mScreenshotLayout = layoutInflater.inflate(R.layout.global_screenshot, null);
//承载截屏图片的ImageView
mScreenshotView = (ImageView) mScreenshotLayout.findViewById(R.id.global_screenshot);
......
//设置窗口相关参数,比如宽高MATCH_PARENT,type是TYPE_SCREENSHOT
mWindowLayoutParams = new WindowManager.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT, 0, 0,
WindowManager.LayoutParams.TYPE_SCREENSHOT,
WindowManager.LayoutParams.FLAG_FULLSCREEN
| WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN
| WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED,
PixelFormat.TRANSLUCENT);
mWindowLayoutParams.setTitle("ScreenshotAnimation");
mWindowLayoutParams.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
//通过service类型的context获取mWindowManager
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//截屏声音
mCameraSound = new MediaActionSound();
mCameraSound.load(MediaActionSound.SHUTTER_CLICK);
}
takeScreenshot
private void takeScreenshot(Runnable finisher, boolean statusBarVisible, boolean navBarVisible,
Rect crop) {
....
//通过native层进行截屏
mScreenBitmap = SurfaceControl.screenshot(crop, width, height, rot);
if (mScreenBitmap == null) {
notifyScreenshotError(mContext, mNotificationManager,
R.string.screenshot_failed_to_capture_text);
finisher.run();
return;
}
//截屏完成后出现的动画
startAnimation(finisher, mDisplayMetrics.widthPixels, mDisplayMetrics.heightPixels,
statusBarVisible, navBarVisible);
}
startAnimation
private void startAnimation(final Runnable finisher, int w, int h, boolean statusBarVisible,
boolean navBarVisible) {
......
//将截屏获取到的Bitmap设置进布局文件的ImageView
mScreenshotView.setImageBitmap(mScreenBitmap);
//获取焦点
mScreenshotLayout.requestFocus();
//添加截屏窗口
mWindowManager.addView(mScreenshotLayout, mWindowLayoutParams);
ValueAnimator screenshotDropInAnim = createScreenshotDropInAnimation();
ValueAnimator screenshotFadeOutAnim = createScreenshotDropOutAnimation(w, h,
statusBarVisible, navBarVisible);
mScreenshotAnimation = new AnimatorSet();
mScreenshotAnimation.playSequentially(screenshotDropInAnim, screenshotFadeOutAnim);
mScreenshotAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
//截屏动画结束后保存图片
saveScreenshotInWorkerThread(finisher);
//并移除窗口
mWindowManager.removeView(mScreenshotLayout);
//清空mScreenBitmap
mScreenBitmap = null;
mScreenshotView.setImageBitmap(null);
}
});
mScreenshotLayout.post(new Runnable() {
@Override
public void run() {
//播放截屏声音
mCameraSound.play(MediaActionSound.SHUTTER_CLICK);
mScreenshotView.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mScreenshotView.buildLayer();
//开始截屏动画
mScreenshotAnimation.start();
}
});
}
mWindowManager.addView
我们主要看截屏窗口的添加,在创建WindowManager实际创建WindowManagerImpl时使用的service类型context,是直接new的一个WindowManagerImpl
registerService(Context.WINDOW_SERVICE, WindowManager.class,
new CachedServiceFetcher<WindowManager>() {
@Override
public WindowManager createService(ContextImpl ctx) {
return new WindowManagerImpl(ctx);
}});
这时WindowManagerImpl的parentWindow为空的
public WindowManagerImpl(Context context) {
this(context, null);
}
private WindowManagerImpl(Context context, Window parentWindow) {
mContext = context;
mParentWindow = parentWindow;
}
addView调到了WindowManagerGlobal的同名方法
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
//前面说过,通过parentWindow为空,所以不会通过下面这个方法给截屏窗口
//设置token
if (parentWindow != null) {
parentWindow.adjustLayoutParamsForSubWindow(wparams);
} else {
final Context context = view.getContext();
if (context != null
&& (context.getApplicationInfo().flags
& ApplicationInfo.FLAG_HARDWARE_ACCELERATED) != 0) {
wparams.flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED;
}
}
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
......
if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
final int count = mViews.size();
for (int i = 0; i < count; i++) {
if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
panelParentView = mViews.get(i);
}
}
}
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
到现在为止,截屏这个窗口的token是空的,我们知道每一个窗口都必须有一个token对象,我们继续看它的token在哪里设置的
ViewRootImpl构造方法中创建了W对象和mWindowSession,这两个对象实现了WMS和应用端双向通信
public ViewRootImpl(Context context, Display display) {
......
mWindowSession = WindowManagerGlobal.getWindowSession();
mWindow = new W(this);
......
}
root.setView
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
......
//绘制流程,measure,layout,draw
requestLayout();
......
//将Window添加到WMS
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
mTempInsets);
}
关于WMS.addWindow在从一个Dialog的创建看Android的窗口机制(下篇) 有比较详细的分析过了,我们主要目的是看token如何设置的
WMS.addWindow
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) {
//截屏窗口此时的token还是为空的
if (token == null) {
//如果token为空并且是应用类型窗口则添加失败
if (rootType >= FIRST_APPLICATION_WINDOW && rootType <= LAST_APPLICATION_WINDOW) {
Slog.w(TAG_WM, "Attempted to add application window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是输入法类型窗口则添加失败
if (rootType == TYPE_INPUT_METHOD) {
Slog.w(TAG_WM, "Attempted to add input method window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是语音交互类型窗口则添加失败
if (rootType == TYPE_VOICE_INTERACTION) {
Slog.w(TAG_WM, "Attempted to add voice interaction window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是壁纸类型窗口则添加失败
if (rootType == TYPE_WALLPAPER) {
Slog.w(TAG_WM, "Attempted to add wallpaper window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是屏保类型窗口则添加失败
if (rootType == TYPE_DREAM) {
Slog.w(TAG_WM, "Attempted to add Dream window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是TYPE_QS_DIALOG类型窗口则添加失败
if (rootType == TYPE_QS_DIALOG) {
Slog.w(TAG_WM, "Attempted to add QS dialog window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是辅助功能覆盖类型窗口则添加失败
if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
Slog.w(TAG_WM, "Attempted to add Accessibility overlay window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
//如果token为空并且是TOAST类型窗口
if (type == TYPE_TOAST) {
//如果版本大于N MR1,则不能随意添加TOAST了
if (doesAddToastWindowRequireToken(attrs.packageName, callingUid,
parentWindow)) {
Slog.w(TAG_WM, "Attempted to add a toast window with unknown token "
+ attrs.token + ". Aborting.");
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
}
//如果token为空则等于窗口的W对象
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
//根据token或者W创建WindowToken
token = new WindowToken(this, binder, type, false, displayContent,
session.mCanAddInternalSystemWindow);
}
.....
//根据token创建WindowState,对于截屏窗口来说token是W对象
final WindowState win = new WindowState(this, session, client, token, parentWindow,
appOp[0], seq, attrs, viewVisibility, session.mUid,
session.mCanAddInternalSystemWindow);
//将窗口添加到WindowToken的WindowList
win.mToken.addWindow(win);
}
截屏窗口的type为TYPE_SCREENSHOT,不属于上面的那些类型,所以最终是通过应用端传递过来的W对象,由系统创建的一个专属token,之后会根据token创建WindowState,再将WindowState添加到WindowList,在WindowState中会计算窗口的Z轴,关于Z轴计算和窗口添加到WindowList在Android窗口Z轴计算及排列规则 已经详细分析过了,对于系统类型窗口来说是通过mBaseLayer从小到大排序,最终的窗口就按这个规则添加到了WindowToken的WindowList中