@guhuizaifeiyang
2019-08-23T11:59:23.000000Z
字数 13731
阅读 1348
Android自定义控件
Android开发艺术探索
WindowManagerService-窗口管理服务分析
全屏、沉浸式、fitSystemWindow使用及原理分析:全方位控制“沉浸式”的实现


在讲View的三大流程之前,先做一下准备工作,熟悉与其相关的类。
官方文档的介绍:
A MeasureSpec encapsulates the layout requirements passed from parent to child.Each MeasureSpec represents a requirement for either the width or the height.A MeasureSpec is comprised of a size and a mode.
MeasureSpec类似于测量指导,包括size(低30位)和mode(高2位)两个属性,用来规范子元素的测量标准。
很明显,size表示大小,mode有UNSPECIFIED,EXACTLY,AT_MOST三种模式:
AT_MOST:父View给了一个可用大小,子View不能大于这个值,对应wrap_content。
ViewGroup.java
protected void measureChildWithMargins(View child,int parentWidthMeasureSpec, int widthUsed,int parentHeightMeasureSpec, int heightUsed) {final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin+ widthUsed, lp.width);final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin+ heightUsed, lp.height);child.measure(childWidthMeasureSpec, childHeightMeasureSpec);}
ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {int specMode = MeasureSpec.getMode(spec);int specSize = MeasureSpec.getSize(spec);int size = Math.max(0, specSize - padding);int resultSize = 0;int resultMode = 0;switch (specMode) {// Parent has imposed an exact size on uscase MeasureSpec.EXACTLY:if (childDimension >= 0) {resultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size. So be it.resultSize = size;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent has imposed a maximum size on uscase MeasureSpec.AT_MOST:if (childDimension >= 0) {// Child wants a specific size... so be itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size, but our size is not fixed.// Constrain child to not be bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size. It can't be// bigger than us.resultSize = size;resultMode = MeasureSpec.AT_MOST;}break;// Parent asked to see how big we want to becase MeasureSpec.UNSPECIFIED:if (childDimension >= 0) {// Child wants a specific size... let him have itresultSize = childDimension;resultMode = MeasureSpec.EXACTLY;} else if (childDimension == LayoutParams.MATCH_PARENT) {// Child wants to be our size... find out how big it should// beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;} else if (childDimension == LayoutParams.WRAP_CONTENT) {// Child wants to determine its own size.... find out how// big it should beresultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size;resultMode = MeasureSpec.UNSPECIFIED;}break;}return MeasureSpec.makeMeasureSpec(resultSize, resultMode);}
普通View的MeasureSpec创建规则(图来源于Android开发艺术探索):
1. 子View的LayoutParams是dp/px的情况下,表示子View已经独立自主,确立了自己的标准,所以MeasureSpec模式都是EXACTLY,MeasureSpec大小也是自己设定的dp/px。
2. 子View的LayoutParams是match_parent的情况下,表示子View向父View看齐,所以MeasureSpec模式跟父View保持一致,见表格第二行。
3. 子View的LayoutParams是wrap_content的情况下,表示子View想决定自己的大小,但还是受父View的监管,所以MeasureSpec模式是AT_MOST。见表格第三行。
4. 父View是UNSPECIFIED的情况下有点特殊,一般不需要亲关注,可以参考源码体会。

View和ViewGroup的measure过程有所不同,View只要测量自己就可以了,而ViewGoup除了要测量自己外,还要遍历去调用子元素的measure方法完成测量。
View$measure方法会去调用View的onMeasure方法。
measure->onMeasure->setMeasuredDimension->getDefaultSize
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));}public static int getDefaultSize(int size, int measureSpec) {int result = size;int specMode = MeasureSpec.getMode(measureSpec);int specSize = MeasureSpec.getSize(measureSpec);switch (specMode) {case MeasureSpec.UNSPECIFIED:result = size;break;case MeasureSpec.AT_MOST:case MeasureSpec.EXACTLY:result = specSize;break;}return result;}
getDefaultSize中主要考虑AT_MOST和EXACTLY两种情况,getDefaultSize返回的大小就是measureSpec中的specSize,对应View的测量值。
setMeasuredDimension和getSuggestedMinimumWidth不是重点,略过。
注意:直接继承View的自定义控件要重写onMeasure方法并设置wrap_content时自身的大小。因为从前面的表格可以看出,当view的layoutParams为wrap_content时,它的specMode是AT_MOST模式。它的大小等于specSize,也就是父View当前剩余的大小。这种效果相当于在布局中使用match_parent完全一致了。
Android应用层View绘制流程与源码分析
- View的measure方法是final的,不允许重载,View子类只能重载onMeasure来完成自己的测量逻辑。
最顶层DecorView测量时的MeasureSpec是由ViewRootImpl中getRootMeasureSpec方法确定的(LayoutParams宽高参数均为MATCH_PARENT,specMode是EXACTLY,specSize为物理屏幕大小)。
ViewGroup类提供了measureChild,measureChild和measureChildWithMargins方法,简化了父子View的尺寸计算。
只要是ViewGroup的子类就必须要求LayoutParams继承子MarginLayoutParams,否则无法使用layout_margin参数。
View的布局大小由父View和子View共同决定。
使用View的getMeasuredWidth()和getMeasuredHeight()方法来获取View测量的宽高,必须保证这两个方法在onMeasure流程之后被调用才能返回有效值。
ViewGroup中没有onMeasure方法,而是提供了一个measureChildren方法去遍历所有子View的measure方法。
measureChildren->measureChild->getChildMeasureSpec->child.measure
由于不同的ViewGoup布局特性差异很大,所以ViewGoup把onMeasure方法留给其子类去实现。下面通过LinearLayout的onMeasure方法来分析ViewGroup的measure过程。
@Overrideprotected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {if (mOrientation == VERTICAL) {measureVertical(widthMeasureSpec, heightMeasureSpec);} else {measureHorizontal(widthMeasureSpec, heightMeasureSpec);}}
/TODO:测量过程比较繁琐,后续再补充
1.Activity/View$onWindowFocusChanged
当Activity的窗口获得焦点和失去焦点的时候均会调用该方法,示例代码:
@Overridepublic void onWindowFocusChanged(boolean hasFocus) {super.onWindowFocusChanged(hasFocus);if (hasFocus) {int width = view.getMeasuredWidth();int height = view.getMeasuredHeight();}}
2.view.post(runnable)
@Overrideprotected void onStart() {super.onStart();view.post(new Runnable() {@Overridepublic void run() {int width = view.getMeasuredWidth();int height = view.getMeasuredHeight();}});}
3.ViewTreeObserver
当View树的状态发生改变或者内部的View的可见性发现改变时,onGlobalLayout方法将被回调。
@Overrideprotected void onStart() {super.onStart();ViewTreeObserver observer = view.getViewTreeObserver();observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {@Overridepublic void onGlobalLayout() {int width = view.getMeasuredWidth();int height = view.getMeasuredHeight();}});}
4.view.measure(int widthMeasureSpec, int heightMeasureSpec)
手动对view进行measure,需要根据view的LayoutParams来区分:
4.1 match_parent
这种情况无法准确测量,因为不知道父view的剩余大小。
4.2 具体的数值
假设宽/高是100dp
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.EXACTLY);view.measure(widthMeasureSpec, heightMeasureSpec);
4.3 wrap_content
MeasureSpec高2位表示SpecMode,低30位表示SpecSize,所以这里使用(1<<30)-1表示View理论上能支持的最大值。
int widthMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);int heightMeasureSpec = View.MeasureSpec.makeMeasureSpec((1<<30) -1, View.MeasureSpec.AT_MOST);view.measure(widthMeasureSpec, heightMeasureSpec);
在完成了View的测量之后,接下来进行的是View的布局。
public void layout(int l, int t, int r, int b) {int oldL = mLeft;int oldT = mTop;int oldB = mBottom;int oldR = mRight;boolean changed = isLayoutModeOptical(mParent) ?setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {onLayout(changed, l, t, r, b);mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;// ...}}
直接捡重点分析
l,t,r,b分别表示view相对于parent左,上,右,下端的距离。
1. setFrame确定View的四个顶点的位置。
2. onLayout确定子View的位置,由于onLayout的实现跟具体布局有关,所以View和ViewGoup都没有实现该方法。
以LinearLayout的onLayout为例,它会遍历子View,会一级一级调用子View的layout方法,完成整个View树的layout过程。
调用逻辑是onLayout->layoutVertical->setChildFrame->child.layout
@Overrideprotected void onLayout(boolean changed, int l, int t, int r, int b) {if (mOrientation == VERTICAL) {layoutVertical(l, t, r, b);} else {layoutHorizontal(l, t, r, b);}}
getMeasuredWidth和getWidth的区别:
这两个方法都是用来获取View的宽度,一般情况下二者的值都是相同的。
不同的是getMeasuredWidth的值形成于measure过程,getWidth的值形成于layout过程。
draw过程比较简单,步骤如下:
1. 绘制背景background.draw(canvas)
2. 绘制自己(onDraw)
3. 绘制children(dispatchDraw)
4. 绘制装饰(onDrawScrollBars)
资料来源:刨根问底-论Android“沉浸式”
使用方法如下:
int flag = View.SYSTEM_UI_FLAG_FULLSCREEN;getWindow().getDecorView().setSystemUiVisibility(flag);int flag = View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;getWindow().getDecorView().setSystemUiVisibility(flag);
SYSTEM_UI_FLAG_FULLSCREEN(4.1+):隐藏状态栏,手指在屏幕顶部往下拖动,状态栏会再次出现且不会消失,另外activity界面会重新调整大小,直观感觉就是activity高度有个变小的过程。

SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN(4.1+):配合SYSTEM_UI_FLAG_FULLSCREEN一起使用,效果使得状态栏出现的时候不会挤压activity高度,状态栏会覆盖在activity之上。

SYSTEM_UI_FLAG_HIDE_NAVIGATION(4.0+)
:会使得虚拟导航栏隐藏,但同样用户可以从屏幕下边缘“拖出”且不会再次消失,同时activity界面会被挤压。

SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION(4.1+):配合 SYSTEM_UI_FLAG_HIDE_NAVIGATION 一起使用,效果使得导航栏出现的时候不会挤压activity高度,导航栏会覆盖在activity之上。

SYSTEM_UI_FLAG_LAYOUT_STABLE(4.1+):保证内容布局不随着导航栏的消失而滚动。配合上面4个flag和android:fitsSystemWindows="true"一起使用。效果如图:

SYSTEM_UI_FLAG_IMMERSIVE+SYSTEM_UI_FLAG_HIDE_NAVIGATION+SYSTEM_UI_FLAG_FULLSCREEN:
挤压的效果如果你不满意,加上SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,这会让状态栏和导航栏“悬浮”在activity之上

SYSTEM_UI_FLAG_IMMERSIVE_STICKY:和SYSTEM_UI_FLAG_IMMERSIVE相似,它被称作“粘性”的沉浸模式,这个模式会在状态栏和导航栏显示一段时间后,自动隐藏(你可以点击一下屏幕,立即隐藏)。同时需要重点说明的是,这种模式下,状态栏和导航栏出现的时候是“半透明”状态,效果如下

在Android4.4还为WindowManager.LayoutParams添加了两个flag:
FLAG_TRANSLUCENT_STATUS: 当使用这个flag时SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN会被自动添加
FLAG_TRANSLUCENT_NAVIGATION:当使用这这个个flag时SYSTEM_UI_FLAG_LAYOUT_STABLE和SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION会被自动添加。
综上,我们可以给出全屏布局和隐藏状态栏的新方案
//仅仅只是全屏布局://getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);//全屏布局并且隐藏状态栏与导航栏getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY| View.SYSTEM_UI_FLAG_LAYOUT_STABLE| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
View decorView = getWindow().getDecorView();int option = View.SYSTEM_UI_FLAG_FULLSCREEN;decorView.setSystemUiVisibility(option);ActionBar actionBar = getSupportActionBar();actionBar.hide();
if (Build.VERSION.SDK_INT >= 21) {View decorView = getWindow().getDecorView();int option = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;decorView.setSystemUiVisibility(option);getWindow().setStatusBarColor(Color.TRANSPARENT);}ActionBar actionBar = getSupportActionBar();actionBar.hide();
View decorView = getWindow().getDecorView();int option = View.SYSTEM_UI_FLAG_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_FULLSCREEN;decorView.setSystemUiVisibility(option);ActionBar actionBar = getSupportActionBar();actionBar.hide();
if (Build.VERSION.SDK_INT >= 21) {View decorView = getWindow().getDecorView();int option = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN| View.SYSTEM_UI_FLAG_LAYOUT_STABLE;decorView.setSystemUiVisibility(option);getWindow().setNavigationBarColor(Color.TRANSPARENT);getWindow().setStatusBarColor(Color.TRANSPARENT);}ActionBar actionBar = getSupportActionBar();actionBar.hide();
StatusBar的颜色更改分为两部分,一个是背景颜色的修改,一个是字体颜色的修改。
首先先说说背景颜色的修改,在Android 5.0之前,状态栏颜色并不可定制,5.0之后才可定制。首先,我们可以在主题里通过colorPrimaryDark来指定背景色,其次,我们可以调用 window.setStatusBarColor(@ColorInt int color) 来修改状态栏颜色,但是让这个方法生效有一个前提条件:
你必须给window添加FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS并且取消FLAG_TRANSLUCENT_STATUS
在Android6以后,我们只要给SystemUI加上SYSTEM_UI_FLAG_LIGHT_STATUS_BAR这个flag,就可以让字体和图标变为黑色。
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR //高亮状态栏
View.STATUS_BAR_TRANSLUCENT //半透明状态栏
View.STATUS_BAR_TRANSPARENT //透明状态栏,一般指定WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS系统会设置成全透明
View.SYSTEM_UI_FLAG_FULLSCREEN // 全屏显示,隐藏状态栏和状态栏
View.STATUS_BAR_UNHIDE // 显示状态栏,用于传递给systemui处理
View.NAVIGATION_BAR_TRANSPARENT //半透明导航栏
View.NAVIGATION_BAR_TRANSLUCENT //透明导航栏,一般指定WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS系统会设置成全透明
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // 隐藏导航栏
View.NAVIGATION_BAR_UNHIDE // 显示状态栏,传递给systemui处理
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY //身临其境的感觉,和SYSTEM_UI_FLAG_LIGHT_STATUS_BAR一起使用会隐藏状态栏和导航栏,从上面/下面滑出状态栏和导航栏,过几秒自动消失
View.SYSTEM_UI_FLAG_IMMERSIVE //身临其境的感觉,自动隐藏状态栏和导航栏,出上部/下部滑动状态栏出现,不自动隐藏
View.STATUS_BAR_TRANSIENT //进入瞬态,状态栏出来和隐藏的过程
View.NAVIGATION_BAR_TRANSIENT //进入瞬态,导航栏出来和隐藏的过程
WindowManager.LayoutParams.FLAG_FULLSCREEN //全屏显示,隐藏状态栏
WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS //导航栏状态栏透明,客户端渲染状态栏导航栏背景
WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND //强制导航栏状态栏透明,客户端渲染状态栏导航栏背景
WindowManager.LayoutParams.PRIVATE_FLAG_INHERIT_TRANSLUCENT_DECOR //当此窗口到达顶部的时候保持前一个窗口的透明状态
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS //指定半透明status bar
WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION //指定半透明nav bar
WindowManager.LayoutParams.PRIVATE_FLAG_FORCE_DRAW_STATUS_BAR_BACKGROUND 强制渲染背景色
下面这几个状态用于status bar 和nav bar 切换时候的瞬态过程
private static final int TRANSIENT_BAR_NONE = 0; //无任何状态,当隐藏完成时候设置
private static final int TRANSIENT_BAR_SHOW_REQUESTED = 1; // 请求显示
private static final int TRANSIENT_BAR_SHOWING = 2; //正在显示的过程
private static final int TRANSIENT_BAR_HIDING = 3; //正在隐藏的过程,隐藏完成window变成不可见,设置TRANSIENT_BAR_NONE
还要记住,分屏模式不允许隐藏bar
资料参考:Android状态栏微技巧,带你真正理解沉浸式模式
郭霖的博客,对沉浸式讲解的作用讲解非常详细。