0%

Window

基本原理

Window 的实现类是 PhoneWindow,它是视图系统的核心,也是一切事件的起源。

Window 系统是通过 IPC 机制实现的,使用 WindowManager 来添加和移除控件,其实现类是 WindowManagerGlobal,其内部维持着以下四个集合

1
2
3
4
ArrayList<View> mViews = new ArrayList<View>(); // Window上存在的View集合
ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
ArrayList<WindowManager.LayoutParams> mParams;
ArraySet<View> mDyingViews = new ArraySet<View>();

在 Window 上添加 View 的逻辑就是更新这几个集合

1
2
3
4
5
6
7
8
9
10
11
12
13
public void addView(View view, ViewGroup.LayoutParams params,  Display display, Window parentWindow) {
final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
}
root.setView(view, wparams, panelParentView);
}

实际的工作交给了 ViewRootImpl。

与 Activity 的关系

Window 与 Activity 实际是两个独立的系统,只不过二者有交集,Window 需要在 Activity 启动时展示其设置的布局而已

在启动活动前,要进行 WindowManager 的初始化

1
2
WindowManagerGlobal.initialize();
Activity a = performLaunchActivity(r, customIntent);

使用反射创建 Activity 对象后,将执行 attach 方法,此时将创建 PhoneWindow 并配置 WindowManager

1
2
3
4
5
6
7
8
9
10
final void attach(Context context, ActivityThread aThread ,...) {
attachBaseContext(context); //attach 回调方法
mFragments.attachHost(null);
mWindow = new PhoneWindow(this); //创建 PhoneWindow 并配置 WindowManager
mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
}

Activity 的onCreate方法也在这一步进行,到此 Window 中的 decorView 将被设置好,只是没有添加到 Window 上去。

而后是显示Activity 的 handleResumeActivity 方法,这里才利用 WindowManager 将 DecorView 添加到Window上去,参数全部由WindowManager.LayoutParams 决定。

1
2
3
4
5
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
wm.addView(decor, l);

DecorView 和 ContentParent

PhoneWindow 中会创建 DecorView 和 ContentParent 两个控件

1
2
3
4
5
6
7
8
private void installDecor() {
if (mDecor == null) {
mDecor = new DecorView(getContext(), -1);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
}
}

mContentParent 的创建较为复杂,它会根据theme配置有所不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
//1.windowIsFloating决定是否采用floating,如果是则布局以WRAP_CONTENT计算,并清除其它flag
mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
& (~getForcedWindowFlags());
if (mIsFloating) {
setLayout(WRAP_CONTENT, WRAP_CONTENT);
setFlags(0, flagsToUpdate);
} else {
setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
}
//2. Feature 参数决定了加载哪种系统布局,windowNoTitle和windowActionBar是互斥的
if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
requestFeature(FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
requestFeature(FEATURE_ACTION_BAR);
}
// 比较特殊的 Feature 参数是FEATURE_CONTENT_TRANSITIONS和FEATURE_ACTIVITY_TRANSITIONS,它采用 Transition 动画

//3. Flag 参数决定了窗口类型,如全屏类型和TRANSLUCENT_STATUS
if (a.getBoolean(R.styleable.Window_windowFullscreen, false)) {
setFlags(FLAG_FULLSCREEN, FLAG_FULLSCREEN & (~getForcedWindowFlags()));
}
if (a.getBoolean(R.styleable.Window_windowTranslucentStatus,
false)) {
setFlags(FLAG_TRANSLUCENT_STATUS, ...);
}
//特殊的 flag 包括 windowDrawsSystemBarBackgrounds,必须配合statusBarColor才能生效
// backgroundDimEnabled 背景是否模糊,设置FLAG_DIM_BEHIND,配合backgroundDimAmount生效
WindowManager.LayoutParams params = getAttributes();
//根据不同的 feature 获得不同的布局资源,并加载到 window decor.
int layoutResource = R.layout.screen_simple;
View in = mLayoutInflater.inflate(layoutResource, null);
decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
mContentRoot = (ViewGroup) in;
//返回ID为 ID_ANDROID_CONTENT 的内容控件
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
return contentParent;
}

到此为止内容控件只是一个占位控件,如在 R.layout.screen_simple 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="?attr/actionBarTheme" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

当活动启动时最终调用 setContentView 方法时将替换这个内容布局,这里将提供的布局加载到 @android:id/content 中去,并处理了设置 FEATURE_CONTENT_TRANSITIONS 的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets(); //应用 Inset
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged(); //内容回调
}
}

WindowManager.LayoutParams

对Window窗口的改变都是通过 WindowManager.LayoutParams 参数来实现的,最突出了莫过于 WindowInset 了。

1
2
3
4
5
6
7
8
@Override
public WindowInsets onApplyWindowInsets(WindowInsets insets) {
mFrameOffsets.set(insets.getSystemWindowInsets());
insets = updateColorViews(insets, true /* animate */);
insets = updateStatusGuard(insets);
updateNavigationGuard(insets);
return insets;
}

updateColorViews 方法的作用主要是处理横竖屏情况下的 WindowInsets ,并通过更新 statusBar 和 navigationBar 的布局参数来处理 WindowInsets事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private WindowInsets updateColorViews(WindowInsets insets, boolean animate) {
WindowManager.LayoutParams attrs = getAttributes();
int sysUiVisibility = attrs.systemUiVisibility | getWindowSystemUiVisibility();
boolean disallowAnimate = (mLastWindowFlags ^ attrs.flags)
& FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS != 0;
mLastWindowFlags = attrs.flags;
mLastTopInset = Math.min(insets.getStableInsetTop(),
insets.getSystemWindowInsetTop());
mLastBottomInset = Math.min(insets.getStableInsetBottom(),
insets.getSystemWindowInsetBottom());
mLastRightInset = Math.min(insets.getStableInsetRight(),
insets.getSystemWindowInsetRight());
boolean navBarToRightEdge = mLastBottomInset == 0 && mLastRightInset > 0;
int navBarSize = navBarToRightEdge ? mLastRightInset : mLastBottomInset;
updateColorViewInt(mNavigationColorViewState, sysUiVisibility, mNavigationBarColor, navBarSize, navBarToRightEdge, 0, animate && !disallowAnimate);

boolean statusBarNeedsRightInset = navBarToRightEdge
&& mNavigationColorViewState.present;
int statusBarRightInset = statusBarNeedsRightInset ? mLastRightInset : 0;
updateColorViewInt(mStatusColorViewState, sysUiVisibility, mStatusBarColor,
mLastTopInset, false, statusBarRightInset,
animate && !disallowAnimate);
return insets;
}

默认情况下 ColorViewState 配置了一个参数 systemUiHideFlag, 取值为 SYSTEM_UI_FLAG_FULLSCREEN,如果设置为改制,更新方法 updateColorViewInt 无效。

与 Dialog 的关系

Dialog 的构造方法如下

1
2
3
4
5
6
7
8
9
10
11
Dialog(@NonNull Context context, @StyleRes int themeResId) { 
//1.从配置文件中虚招主题,并构建 ContextThemeWrapper
TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
mContext = new ContextThemeWrapper(context, themeResId);
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//2.创建 PhoneWindow 对象并居中
final Window w = new PhoneWindow(mContext);
w.setGravity(Gravity.CENTER);
}

这里明白的显示了一个 Dialog 就是一个 Window,显示什么内容也遵循 Window 的规律,使用 setContentView 来添加自定义布局。

至于显示则是利用 WindowManager 将准备好的 DecorView 添加到 Window 上去。

1
2
3
4
5
6
public void show() {
mDecor = mWindow.getDecorView();
mWindowManager.addView(mDecor, l);
mShowing = true;
sendShowMessage();
}

同理 hide() 方法实际就是隐藏 DecorView,dismiss 方法就是使用 WindowManager 移除 DecorView。

Dialog 的若干子类不过是配置它的布局而已,此外 Dilog 没有生命周期。

状态栏染色

我们已经知道状态栏的染色是通过添加View来完成的,其必须满足条件

1
2
3
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
<item name="android:windowTranslucentStatus">false</item>
<item name="android:statusBarColor">#FF786312</item> //非必须

动态染色可以采用如下办法

1
getWindow().setStatusBarColor(Color.MAGENTA);

实际上这个 View 是这样定义的

1
2
3
4
5
6
7
private final ColorViewState mStatusColorViewState = new ColorViewState(
SYSTEM_UI_FLAG_FULLSCREEN, FLAG_TRANSLUCENT_STATUS,
Gravity.TOP,
Gravity.LEFT,
STATUS_BAR_BACKGROUND_TRANSITION_NAME,
com.android.internal.R.id.statusBarBackground, //id
FLAG_FULLSCREEN);

我们也完全可以这样做

1
2
View view = findViewById(android.R.id.statusBarBackground);//获取 StatusColorView
view.setBackgroundColor(Color.YELLOW);

如果播放一个动画,就能完全看清楚这个 View

1
v.animate().setDuration(2000).setInterpolator(new CycleInterpolator(0.5f)).translationY(200).start();

这一过程的实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
* Update a color view
*
* @param state the color view to update.
* @param sysUiVis the current systemUiVisibility to apply.
* @param color the current color to apply.
* @param size the current size in the non-parent-matching dimension.
* @param verticalBar if true view is attached to a vertical edge, otherwise to a
* horizontal edge,
* @param rightMargin rightMargin for the color view.
* @param animate if true, the change will be animated.
*/
private void updateColorViewInt(final ColorViewState state, int sysUiVis, int color,
int size, boolean verticalBar, int rightMargin, boolean animate) {
//1.判断是否存在更新的条件,包括 inset 的尺寸要有,flag 不能有 hideWindowFlag,必须有 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
state.present = size > 0 && (sysUiVis & state.systemUiHideFlag) == 0
&& (getAttributes().flags & state.hideWindowFlag) == 0
&& (getAttributes().flags & FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) != 0;
//2.判断是否要显示,包括颜色不能是黑色,flag 不能配置成 translucent
boolean show = state.present
&& (color & Color.BLACK) != 0
&& (getAttributes().flags & state.translucentFlag) == 0;
View view = state.view;
int resolvedHeight = verticalBar ? LayoutParams.MATCH_PARENT : size;
int resolvedWidth = verticalBar ? size : LayoutParams.MATCH_PARENT;
int resolvedGravity = verticalBar ? state.horizontalGravity : state.verticalGravity;

//通过添加一个带颜色的 View ,并更新其 LayoutParams 参数实现效果。
if (view == null) {
state.view = view = new View(mContext);
view.setBackgroundColor(color);
view.setVisibility(INVISIBLE);
LayoutParams lp = new LayoutParams(resolvedWidth, resolvedHeight,
resolvedGravity);
lp.rightMargin = rightMargin;
addView(view, lp);
updateColorViewTranslations(); //使用动画效果
} else {
LayoutParams lp = (LayoutParams) view.getLayoutParams();
lp.height = resolvedHeight;
lp.width = resolvedWidth;
lp.gravity = resolvedGravity;
lp.rightMargin = rightMargin;
view.setLayoutParams(lp);
view.setBackgroundColor(color);
}
if (animate) {
view.animate().alpha(1.0f).setInterpolator(mShowInterpolator).
setDuration(mBarEnterExitDuration);
}
}

这里 windowDrawsSystemBarBackgrounds 和 windowTranslucentStatus 是互相排斥的,想要改变状态栏颜色,必须使用前者,禁止后者。后者一旦生效,上述方法将无效,View 不会被创建。

WindowSystemUiVisibility

SystemUiVisibility 能够完成的动作完全可以通过设置 Window 来完成,但好处在于随时可以清除效果。

SystemUiVisibility 的设置必染色要复杂,涉及状态栏和布局的变化,不过在回调中也会执行更新方法

1
2
3
public void onWindowSystemUiVisibilityChanged(int visible) {
updateColorViews(null, true);
}
  • View.SYSTEM_UI_FLAG_FULLSCREEN 这种情况下,状态栏将彻底消失,内容布局扩展到全屏模式
    • SYSTEM_UI_FLAG_IMMERSIVE 默认情况下,触摸下拉会出现通知栏,必须手动清除才能取消 SYSTEM_UI_FLAG_FULLSCREEN 的效果;配合该标记能够下拉出状态栏,且立即清除效果
    • SYSTEM_UI_FLAG_IMMERSIVE_STICKY 下拉出原生未染色的的状态栏,且不清除效果
  • View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN 内容布局到全屏,且给ActionBar设置一个padding显示状态栏,一旦清除,布局回收,该Padding还在
  • View.SYSTEM_UI_FLAG_LAYOUT_STABLE 配合上述设置,使得更改永久生效,清除之后布局不回收

Toolbar

最后在说下 ActionBar 和 Toolbar 的关系

设置 Toolbar 的方法如下,首先要判断是否已经存在 ActionBar,通常如果使用 FEATURE_ACTION_BAR 则会初始化 ActionBar, 实现类是 WindowDecorActionBar

1
2
3
4
5
6
7
@Nullable
public ActionBar getActionBar() {
Window window = getWindow();
if (!window.hasFeature(Window.FEATURE_ACTION_BAR) || mActionBar != null) return;
mActionBar = new WindowDecorActionBar(this);
return mActionBar;
}

如果没有,可以正常进行,新建的实现类是 ToolbarActionBar。

1
2
3
4
5
6
7
8
9
public void setActionBar(@Nullable Toolbar toolbar) {
if (getActionBar() instanceof WindowDecorActionBar) {
throw new IllegalStateException("This Activity already has an action bar supplied " + "by the window decor. Do not request Window.FEATURE_ACTION_BAR and set " +
"android:windowActionBar to false in your theme to use a Toolbar instead.");
}
ToolbarActionBar tbab = new ToolbarActionBar(toolbar, getTitle(), this);
mActionBar = tbab;
mActionBar.invalidateOptionsMenu();
}

可见 ActionBar 和 Toolbar 是表里的关系。你可以处理菜单相关的事项

1
2
3
4
5
Menu getMenu();
inflateMenu(R.menu.menu_main);
boolean showOverflowMenu(); //通过反射修改显示 OverflowMenu
boolean hideOverflowMenu();
void setOnMenuItemClickListener(OnMenuItemClickListener listener);

也可以使用动画

1
ViewPropertyAnimator toolbarAnimator = toolbar.animate();