关于android状态栏适配之前研究过一点,但这次在看一个开源项目的时候遇到了一个不同的实现方式,研究了一下有了一些心得体会,以文章的形式记录了下来。
参考开源项目博客地址:https://www.jianshu.com/p/69a229fb6e1d
关于状态栏的适配问题,不同的机型会存在一定的差异,但是基本原理都是一样的,以下文中所提到的实现效果都是基于公司开发机三星s6,android6.0.1,API 23实现
下面从以下三部分进行说明
1.调用系统API直接实现
2.styles.xml配置+代码调用
3.开源项目中使用到的方法
1调用系统API直接实现
这种方法简单粗暴,效果也很好,没有什么特殊要求完全可以使用这种方法实现,公司app状态栏适配原先是通过第二种方法实现,后来发现可以使用系统API直接实现,代码很简洁直接在需要适配的activity中配置如下的styles
注意
该styles.xml需要放置到values-19还是valuse-21中才能生效(具体哪个版本开始支持有点忘了,自己试下就知道)。
最终的实际效果如图所示
关于为什么需要设置colorPrimaryDark可以参考网上的图片,在分析第三种方法的时候会从源码的角度告诉大家为什么会是colorPrimaryDark属性。
上面的方法是通过配置styles.xml实现的,完全可以通过代码的方法实现同样的效果
getWindow().setStatusBarColor(Color.parseColor("#339df5"));
setContentView(R.layout.activity_main);
至此第一种方法就介绍完毕了,简单但是却非常的实用。
2styles.xml配置+代码调用
同第一种方法也需要配置一个styles.xml
该属性的作用就是将整体布局延伸到状态栏的底部,呈现出来的效果如下
此时还需要配合代码让整体布局往下移动状态栏高度的位移,代码很简单
findViewById(R.id.title).setPadding(0,96,0,0);
一行代码加上一行styles.xml配置即可完成状态栏的适配工作,比第一种方法略麻烦一点。最终效果如下
- demo代码中的状态栏高度是图简单直接写死的,实际项目中要根据不同手机设置,获取状态栏高度的代码网上很多不在做介绍。
- 最终的UI效果和第一种方法有一定的差异,可以发现状态栏的颜色更深
这里有一个地方需要注意
实现布局整体下移是通过setpadding的方式,如果通过给布局设置一个topmargin是否可行?我相信很多人都没想过这个问题吧,实际上直接给布局设置topmargin是不可行的,虽然都是位移布局,但设置topmargin最终的呈现效果如图
实现上述效果代码如下
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
lp.topMargin = 96;
findViewById(R.id.title).setLayoutParams(lp);
造成这种问题的主要原因就是view绘制的一些问题,给view设置topmargin,view绘制实际不会将topmargin的高度考虑进去,而设置padding却会将padding值也做为可绘制的范围。这是一个特别需要注意的地方。到此这二种方法也就介绍完毕了,也比较简单主要的地方就是要区分开margin和padding的区别。
3开源项目中使用到的方法
开源项目中通过使用一个工具类statusbarutil实现状态栏效果,不在这过多介绍有兴趣的可以自己去了解下,该项目中涉及到的状态栏适配和我之前所讲的两种方法最大的不同的地方就在于使用到了drawerlayout,先上两张项目中的ui效果感受下,
没有拉出drawer时UI效果
和我上面所介绍的一样,很容易实现,这里为了突出状态栏所以将项目中的其他代码给屏蔽掉了,所以看到内容是白屏效果。
拉出drawer时UI效果
难点就在于拉出drawer布局后,可以发现drawer的布局延伸到了状态栏底部,如果纯粹使用第一种方法最终的效果是这样的
换成了大红色的背景,效果有点辣眼睛,主要还是因为直接该项目源码比较麻烦索性自己弄了一个demo出来,大家明白意思就好了,可以发现drawer的布局无法延伸到状态栏底部。所以第一种方法是实现不了项目所要的效果的。
可能看到这有些小伙伴就坐不住了"这还不简单,直接使用你说的第二种方法不就可以了吗",那我只能说骚年你眼神还是不太好啊,我说的第二种方法可以很明显的看出来延伸到状态栏底部的那部分颜色更深,如图
和项目效果还是有差异,那么项目中的效果到底是如何实现的,这里确实要吐槽下项目中的实现代码,相当晦涩难懂,看完之后一脸懵逼,经过自己的摸索才发现了原理。这里简单起见我把源码中的无关代码给去掉了只留下能说明问题的部分!!
首先看布局xml的设置
drawerlayout中包含两个子布局,一个是include主布局,没什么好看的就是titlebar,另一个就是drawer布局了也很简单,这里需要特别注意的就是这个 android:fitsSystemWindows="true"的设置了!!!,它是实现最终效果的关键之一
然后看一下activity中的代码设置
findViewById(R.id.drawerLayout).setFitsSystemWindows(false);
getWindow().setStatusBarColor(Color.TRANSPARENT);
findViewById(R.id.title).setPadding(0, 96, 0, 0);
这里来逐句说明作用,首先重点说下
findViewById(R.id.title).setPadding(0, 96, 0, 0);
源码中并不是利用setpadding来做的,而是以下代码
StatusBarView statusBarView = createStatusBarView(activity, color);
contentLayout.addView(statusBarView, 0);
private static StatusBarView createStatusBarView(Activity activity, @ColorInt int color) {
// 绘制一个和状态栏一样高的矩形
StatusBarView statusBarView = new StatusBarView(activity);
LinearLayout.LayoutParams params =
new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, getStatusBarHeight(activity));
statusBarView.setLayoutParams(params);
statusBarView.setBackgroundColor(color);
return statusBarView;
}
注意
其中contentLayout就是drawerlayout布局本身,StatusBarView就是继承View换了一个名字而已。通过创建一个状态栏高度的view然后添加到布局里面起到占位的作用。这篇文章里面直接通过setpadding的方式实现目的就是为了展示另外可行的方案,但没有在其他机型上验证过,而开源项目中采用占位的形式这种方式(据说google针对不同版本的android:fitsSystemWindows实现方式不一样,有设置margin的也有设置padding的,看我文章的第二种方式就知道padding和margin在绘制背景时的差异了)。
分析完毕最后一句代码,再来看下getWindow().setStatusBarColor(Color.TRANSPARENT);这个就比较简单了,让状态栏后面的titlebar的蓝色能够显示出来。接着我们再来看下重头戏部分!
findViewById(R.id.drawerLayout).setFitsSystemWindows(false);!!!
重点就是这句代码,完全让人摸不着头脑有没有!!先在xml布局中配置
android:fitsSystemWindows="true"
一个反身又在activity中直接设置为了false,尼玛这是要搞事情啊,那还不如不设置,但是如果你直接将这句代码给注释掉你会发现UI效果变成了这样
看完之后是不是有一种一脸懵逼的感觉
在代码中设置setFitsSystemWindows为false的目的是什么?为了真正理解上述代码的含义,必须从源码中去寻找答案。
下面从布局xml中设置fitsSystemWindows开始分析,进入到drawerlayout的构造函数,可以发现关键代码
if (ViewCompat.getFitsSystemWindows(this)) {
IMPL.configureApplyInsets(this);
mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
}
当设置为true则会进入到if分支里面,configureApplyInsets最终会进入到configureApplyInsets的该方法中
public static void configureApplyInsets(View drawerLayout) {
if (drawerLayout instanceof DrawerLayoutImpl) {
drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
static class InsetsListener implements View.OnApplyWindowInsetsListener {
@Override
public WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
final DrawerLayoutImpl drawerLayout = (DrawerLayoutImpl) v;
drawerLayout.setChildInsets(insets, insets.getSystemWindowInsetTop() > 0);
return insets.consumeSystemWindowInsets();
}
}
由于
public class DrawerLayout extends ViewGroup implements DrawerLayoutImpl
drawerlayout会首先setOnApplyWindowInsetsListener,其中InsetsListener的onApplyWindowInsets方法最终会在setSystemUiVisibility方法执行后重新绘制view的时候被调用到,onApplyWindowInsets方法中的第二个参数就是描述整个窗口离屏幕的边距, insets.getSystemWindowInsetTop的返回值就是状态栏的高度,进入到
public void setChildInsets(Object insets, boolean draw) {
mLastInsets = insets;
mDrawStatusBarBackground = draw;
setWillNotDraw(!draw && getBackground() == null);
requestLayout();
}
其中成员变量mLastInsets ,mDrawStatusBarBackground 最终将在ondraw方法中被使用到
public void onDraw(Canvas c) {
super.onDraw(c);
if (mDrawStatusBarBackground && mStatusBarBackground != null) {
final int inset = IMPL.getTopInset(mLastInsets);
if (inset > 0) {
mStatusBarBackground.setBounds(0, 0, getWidth(), inset);
mStatusBarBackground.draw(c);
}
}
}
上面代码的意思就是在状态栏位置绘制一个mStatusBarBackground,而该变量的赋值位置正是在
if (ViewCompat.getFitsSystemWindows(this)) {
IMPL.configureApplyInsets(this);
mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
}
追踪到getDefaultStatusBarBackground内部可以发现
public static Drawable getDefaultStatusBarBackground(Context context) {
final TypedArray a = context.obtainStyledAttributes(THEME_ATTRS);
try {
return a.getDrawable(0);
} finally {
a.recycle();
}
}
private static final int[] THEME_ATTRS = {
android.R.attr.colorPrimaryDark
};
其中THEME_ATTRS的正是我在第一种方法中所提到的配置属性,到此我们就可以明白为什么colorPrimaryDark属性是用来设置状态栏的颜色了!
解惑了该属性的作用后,我们再回到
public static void configureApplyInsets(View drawerLayout) {
if (drawerLayout instanceof DrawerLayoutImpl) {
drawerLayout.setOnApplyWindowInsetsListener(new InsetsListener());
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}
标志位View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN正是实现全屏的关键,
setSystemUiVisibility最终会触发到view的重新绘制,在drawerlayouy的onmeasure方法中非常关键的代码,这里省略无关代码直贴关键部分
...
final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
for (int i = 0; i < childCount; i++) {
...
if (applyInsets) {
final int cgrav = GravityCompat.getAbsoluteGravity(lp.gravity, layoutDirection);
if (ViewCompat.getFitsSystemWindows(child)) {
IMPL.dispatchChildInsets(child, mLastInsets, cgrav);
} else {
IMPL.applyMarginInsets(lp, mLastInsets, cgrav);
}
}
...
}
applyInsets 结果为true,在for内部会依次获取到drawerlayout的子view并确定子view是否有fitssystem属性,由于子view没有设置所以会进入到else内部执行applyMarginInsets,贴出方法内部关键代码
lp.leftMargin = wi.getSystemWindowInsetLeft();
lp.topMargin = wi.getSystemWindowInsetTop();
lp.rightMargin = wi.getSystemWindowInsetRight();
lp.bottomMargin = wi.getSystemWindowInsetBottom();
最终该lp会被用来测绘子view的高度,其中 lp.topMargin 为状态栏高度会被各个子view考虑进来,最终的表现形式就是各个子view都向下偏移了状态栏高度的位移,而空出来的那块位置在drawerlayout的ondraw方法中被绘制上了状态栏,到此可以说fitssystem属性的作用就被完全理解了。
总结起来其实非常的简单:有fitssystem属性的控件会将内部的所有子view都往下偏移状态栏高度的位移,然后在对应状态栏的位置绘制出一个代表状态栏的背景,偏移的实质就是给子view都设置一个topmargin。
其实真正理解上面的代码后,大家就能理解我开始的问题了,为什么在xml布局中设置完fitssystem属性后反身就在activity中又设置回false,我们根据代码来分析下
在xml中设置为true的目的是为了触发drawerlayout构造函数中的
if (ViewCompat.getFitsSystemWindows(this)) {
IMPL.configureApplyInsets(this);
mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context);
}
从而触发了
drawerLayout.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
的执行,执行完该代码后会接着执行到activity中的setfitssystem(false)代码,该句代码作用很简单就是将fitssystem属性设置为false,最终在触发drawerlayout的onmeasure方法时
final boolean applyInsets = mLastInsets != null && ViewCompat.getFitsSystemWindows(this);
applyInsets 将为false从而在测绘子view时不会将topmargin设置为状态栏的高度,这样就不难理解
findViewById(R.id.drawerLayout).setFitsSystemWindows(false);
getWindow().setStatusBarColor(Color.TRANSPARENT);
findViewById(R.id.title).setPadding(0, 96, 0, 0);
为什么需要setpadding,目的就是自己手写代码实现向下偏移。
文章的最后在说下,其实仔细分析过上面我说的代码,就会发现其实完全可以不使用fitssystem属性就可以完成开源项目中的状态栏效果。代码也非常简单只需要三行代码即可
findViewById(R.id.drawerLayout).setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
getWindow().setStatusBarColor(Color.TRANSPARENT);
findViewById(R.id.title).setPadding(0, 96, 0, 0);
记得将xml中的fitsystem属性给去掉。
到此所有问题都能提到一个合适的解释,所以说一个看似简单的问题,甚至别人几行代码实现的效果,很可能包含了别人大量的心血。
上述文章就是我在分析开源项目开始的时候遇到的问题,很多东西不要停留在表面上,自己动手敲一下代码很多问题都会浮出水面。写这篇文章目的有两个:
-自己没有写过技术文章,从来都是自己记录在笔记本里供自己参考,觉得很有必要为技术分享精神贡献自己的一点点力量
-网上很多的状态栏适配的文章,大段大段的代码,将简单问题复杂化看的头晕,不少文章也只是简单停留在用的程度,解释不了自己的疑惑索性自己研究一下