@guhuizaifeiyang
2019-08-23T19:59:23.000000Z
字数 13731
阅读 1187
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 us
case 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 us
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// Child wants a specific size... so be it
resultSize = 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 be
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// Child wants a specific size... let him have it
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// Child wants to be our size... find out how big it should
// be
resultSize = 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 be
resultSize = 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过程。
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (mOrientation == VERTICAL) {
measureVertical(widthMeasureSpec, heightMeasureSpec);
} else {
measureHorizontal(widthMeasureSpec, heightMeasureSpec);
}
}
/TODO:测量过程比较繁琐,后续再补充
1.Activity/View$onWindowFocusChanged
当Activity的窗口获得焦点和失去焦点的时候均会调用该方法,示例代码:
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
}
2.view.post(runnable)
@Override
protected void onStart() {
super.onStart();
view.post(new Runnable() {
@Override
public void run() {
int width = view.getMeasuredWidth();
int height = view.getMeasuredHeight();
}
});
}
3.ViewTreeObserver
当View树的状态发生改变或者内部的View的可见性发现改变时,onGlobalLayout方法将被回调。
@Override
protected void onStart() {
super.onStart();
ViewTreeObserver observer = view.getViewTreeObserver();
observer.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public 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
@Override
protected 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状态栏微技巧,带你真正理解沉浸式模式
郭霖的博客,对沉浸式讲解的作用讲解非常详细。