欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android透明化和沉浸式状态栏实践及源码分析

程序员文章站 2023-12-09 18:21:40
本文所提到的透明状态栏其实指的是将顶部的导航栏延伸到状态栏,使之浑然一体(google官方建议状态栏颜色比导航栏的颜色略深一点),并不代表一定不设置背景色,比如导航栏是白色...

本文所提到的透明状态栏其实指的是将顶部的导航栏延伸到状态栏,使之浑然一体(google官方建议状态栏颜色比导航栏的颜色略深一点),并不代表一定不设置背景色,比如导航栏是白色,则可设置状态栏为白色,视情况而定。

相比于ios系统,android系统对于状态栏的设置就显得稍微复杂了一点。android系统提供了api 19以上对状态栏的设置接口,而直到api 23以上才提供对于icon颜色的设置,还有就是各家厂商(如魅族,小米等)对于状态栏的有自己的定制,对于需要使用浅色背景状态栏的应用,没处理好的话往往导致浅色背景,白色icon ,状态栏不分你我的悲剧。。

Android透明化和沉浸式状态栏实践及源码分析

(内心os:嗯?右上角那一个绿色的电池,用户一定知道他是状态栏对吧。)

我随即对比了一些主流app,发现在我的魅蓝2(android 5.1 flyme 4.5)上竟然都不支持透明状态栏,这对于我这种追求审美的人(其实是视觉提的需求)来说简直不能忍。在我折腾了几天之后,终于解决了这些问题,希望对大家思路有一些帮助。

言归正传,本文主要针对以下几点进行分析:1.是否隐藏状态栏(全屏模式) 2.状态栏的背景色的设置 3.状态栏icon的颜色的设置,而对于透明状态栏设置过程中,可能造成的icon颜色设置成功,而背景颜色设置失败,等等原因造成的浅色底,浅色字或深色底,深色字等错误情况的处理及兜底方案 4.源码实现。

下面我们就以上几点来讨论一下android中透明状态栏的实现。

1. 全屏模式(沉浸式状态栏)

这种情况其实用得并不多,基本上使用场景在闪屏页展示广告或logo,以及一些阅读类app需要尽可能的利用到屏幕大小,展示更多的内容。

Android透明化和沉浸式状态栏实践及源码分析

设置方法很简单,分两种,在api > 16时:

theme中定义:将自定义该activtiy的theme,并在其中添加

<item name="android:windowfullscreen">true</item>

代码中定义:

复制代码 代码如下:

window.getdecorview().setsystemuivisibility(view.system_ui_flag_layout_fullscreen)

2. 状态栏的背景色

我们都知道,在android5.0(即api > 21)时,google官方提供接口设置对应的状态栏背景色

window.setstatusbarcolor(@colorint int color);

那我们要想在android5.0以下设置背景色就真的没有办法了吗?并不是,我们发现在android4.4以后,出现了windowtranslucentstatus这一特性,所以思路如下:

  • 先设置状态栏的透明属性
  • 给根布局顶部加上一个和状态栏一样大小的矩形view,充当假的状态栏
  • 设置fitssystemwindows属性为true,此时跟布局会延伸到状态栏,处在顶部位置就是之前设置的view,这样就可以以假乱真了。 
 if (build.version.sdk_int >= build.version_codes.kitkat) {
   // 设置状态栏透明
   activity.getwindow().addflags(windowmanager.layoutparams.flag_translucent_status);
   // 生成一个状态栏大小的矩形view
   view statusview = createstatusview(activity, color);
   viewgroup decorview = (viewgroup) activity.getwindow().getdecorview();
   decorview.addview(statusview);
   // 设置根布局的参数
   viewgroup rootview = (viewgroup) ((viewgroup) activity.findviewbyid(android.r.id.content)).getchildat(0);
   rootview.setfitssystemwindows(true);
   rootview.setcliptopadding(true);
  }

设置矩形色块的代码如下:

 private static view createstatusview(activity activity, int color) {
  // 获得状态栏高度
  int resourceid = activity.getresources().getidentifier("status_bar_height", "dimen", "android");
  int statusbarheight = activity.getresources().getdimensionpixelsize(resourceid);

  // 绘制一个和状态栏一样高的矩形
  view statusview = new view(activity);
  linearlayout.layoutparams params = new linearlayout.layoutparams(viewgroup.layoutparams.match_parent,
    statusbarheight);
  statusview.setlayoutparams(params);
  statusview.setbackgroundcolor(color);
  return statusview;
 }

效果如下:

Android透明化和沉浸式状态栏实践及源码分析

3. 状态栏icon的颜色

我们通过调研发现,魅族和小米的两个厂商分别对于flyme及miui做了状态栏的定制,所以我们可以在flyme4.0以上及miuiv6以上都可以实现icon颜色的替换,结合上述的状态栏背景色的替换,我们就可以对于魅族及小米两款机型适配浅色的导航栏了。

3.1 魅族flyme4.0+

 /**
  * 魅族flyme4+以上透明状态栏
  * @param window 
  * @param dark 是否把状态栏字体及图标颜色设置为深色
  * @return 是否设置成功
  */
 public static boolean setmeizustatusbardarkicon(window window, boolean dark) {
  boolean result = false;
  if (window != null) {
   try {
    windowmanager.layoutparams lp = window.getattributes();
    field darkflag = windowmanager.layoutparams.class
      .getdeclaredfield("meizu_flag_dark_status_bar_icon");
    field meizuflags = windowmanager.layoutparams.class
      .getdeclaredfield("meizuflags");
    darkflag.setaccessible(true);
    meizuflags.setaccessible(true);
    int bit = darkflag.getint(null);
    int value = meizuflags.getint(lp);
    if (dark) {
     value |= bit;
    } else {
     value &= ~bit;
    }
    meizuflags.setint(lp, value);
    window.setattributes(lp);
    result = true;
   } catch (exception e) {
    //错误处理
   }
  }
  return result;
 }

3.2 miuiv6.0+

 /**
  * 设置状态栏字体图标为深色,需要miuiv6以上
  * @param window
  * @param dark 是否把状态栏字体及图标颜色设置为深色
  * @return 是否设置成功
  *
  */
 public static boolean setmiuistatusbarlightmode(window window, boolean dark) {
  boolean result = false;
  if (window != null) {
   class clazz = window.getclass();
   try {
    int darkmodeflag = 0;
    class layoutparams = class.forname("android.view.miuiwindowmanager$layoutparams");
    field field = layoutparams.getfield("extra_flag_status_bar_dark_mode");
    darkmodeflag = field.getint(layoutparams);
    method extraflagfield = clazz.getmethod("setextraflags", int.class, int.class);
    if(dark){
     extraflagfield.invoke(window,darkmodeflag,darkmodeflag);//状态栏透明且黑色字体
    }else{
     extraflagfield.invoke(window, 0, darkmodeflag);//清除黑色字体
    }
    result=true;
   }catch (exception e){
    //错误处理
   }
  }
  return result;
 }

效果如下:

Android透明化和沉浸式状态栏实践及源码分析

4. 设置背景色失败处理方案

4.1 解决思路

说了那么多,有童鞋可能就问了,上述说的代码可能对大多数机型有效,但是万一某些机型设置失败了呢?体验不但没提升,反倒让用户不能忍受了。

好吧,下面我们来看一下应该怎么解决。思路如下:

  • 获取状态栏的背景色,与需要设置的颜色进行比对,如果相等则设置成功。
  • 如果背景色设置失败,针对非flyme机型,我们回退状态栏icon颜色,还原到最初状态。
  • 上述为什么要强调非flyme机型,是因为用过flyme机型的童鞋肯定知道有一个透明状态栏的开关,当开启后,flyme会对状态栏进行处理,但处理效果并不和我们想要的一样(比如视图顶部为黑色背景,但是却设置了白色状态栏),并且两者都对状态栏进行操作,操作的冲突也有可能引起上述的icon颜色不明显的情况,所以我们自己定义一个colordrawable进行替换。

4.2 获取状态栏颜色

 /**
  * 获取状态栏颜色
  *
  * @param window 需要获取的activity
  * @return color 状态栏的颜色值
  */
 public static int getcolor(window window){
  if (build.version.sdk_int >= build.version_codes.lollipop) {
   return window.getstatusbarcolor();
  }else if (build.version.sdk_int >= build.version_codes.kitkat) {
   viewgroup decorview = (viewgroup) window.getdecorview();
   int count = decorview.getchildcount();
   if (count > 0 && decorview.getchildat(count - 1) instanceof statusbarview) {
    int color = color.transparent;
    drawable background = decorview.getchildat(count - 1).getbackground();
    if (background instanceof colordrawable)
     color = ((colordrawable) background).getcolor();
    return color;
   }
  }
  return -1;
 }

下面讲一下针对如何屏蔽flyme机型沉浸式状态栏打开时的影响:

 private static boolean setmeizustatuscolor(window window, @colorint int color, int alpha) {
  if (build.version.sdk_int >= build.version_codes.lollipop) {
   final view view = window.getdecorview().findviewbyid(android.r.id.statusbarbackground);
   if (view != null) {
    if (!(view.getbackground() instanceof meizucolordrawable)) {
      //替换成自定义colordrawable
     view.setbackground(new meizucolordrawable());
    }

    if (view.getbackground() instanceof meizucolordrawable) {
     meizucolordrawable colordrawable = (meizucolordrawable) view.getbackground();
     int finalcolor = statusbarutil.calculatestatuscolor(color, alpha);
     view.setbackgroundcolor(finalcolor);
     colordrawable.setcolor(finalcolor);
     return true;
    }
   }
  }
  return false;
 }

4.3 兜底方案

上面提到了一些失败判断途径,那么如果还是对一些机型无效呢?我的建议是可以在本地预埋一个开关,万一出问题,服务器可以对相应机型的沉浸式开关关掉即可,或者采用hotfix等方式来进行修复。

5. 源码分析

看过上面针对魅族和小米设置icon的源代码我们知道,共同点都是通过反射来进行调用相应的方法,或者对于相应的状态栏视图进行设置颜色。我们来看看源码,在window中有一个抽象方法是setstatusbarcolor而在phonewindow中我们找到了该方法的实现

 @override
 public void setstatusbarcolor(int color) {
  mstatusbarcolor = color;
  mforcedstatusbarcolor = true;
  if (mdecor != null) {
   mdecor.updatecolorviews(null, false /* animate */);
  }
 }

mdecor.updatecolorviews()

  private windowinsets updatecolorviews(windowinsets insets, boolean animate) {
  ...
     updatecolorviewint(mnavigationcolorviewstate, sysuivisibility, mnavigationbarcolor,
      navbarsize, navbartorightedge, 0 /* rightinset */,
      animate && !disallowanimate);

    boolean statusbarneedsrightinset = navbartorightedge
      && mnavigationcolorviewstate.present;
    int statusbarrightinset = statusbarneedsrightinset ? mlastrightinset : 0;
    updatecolorviewint(mstatuscolorviewstate, sysuivisibility, mstatusbarcolor,
      mlasttopinset, false /* matchvertical */, statusbarrightinset,
      animate && !disallowanimate);
  ...
}

那么mnavigationcolorviewstate和mstatuscolorviewstate又是什么呢?别着急,我们继续往下看

  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,
    flag_fullscreen);
  private final colorviewstate mnavigationcolorviewstate = new colorviewstate(
    system_ui_flag_hide_navigation, flag_translucent_navigation,
    gravity.bottom,
    gravity.right,
    navigation_bar_background_transition_name,
    com.android.internal.r.id.navigationbarbackground,
    0 /* hidewindowflag */);

mnavigationcolorviewstate和mstatuscolorviewstate这两个都是colorviewstate定义好的colorviewstate,主要存储了关于statusbar的一些视图信息。包括id,translucentflag,verticalgravity等等。

接着看updatecolorviewint()方法,这个方法是对一个view设置颜色的,自然这个view也就是指的状态栏的view,下面来看一下实现:

private void updatecolorviewint(final colorviewstate state, int sysuivis, int color,
    int size, boolean verticalbar, int rightmargin, boolean animate) {
    state.present = size > 0 && (sysuivis & state.systemuihideflag) == 0
     && (getattributes().flags & state.hidewindowflag) == 0
     && (getattributes().flags & flag_draws_system_bar_backgrounds) != 0;
   boolean show = state.present
     && (color & color.black) != 0
     && (getattributes().flags & state.translucentflag) == 0;

   boolean visibilitychanged = false;
   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;

    //对传入的colorviewstate的view做非空判断
   if (view == null) {
    if (show) {
     //新建一个view
     state.view = view = new view(mcontext);
     //设置背景色
     view.setbackgroundcolor(color);
     view.settransitionname(state.transitionname);
     view.setid(state.id);
     visibilitychanged = true;
     //有人可能会感觉奇怪,为什么show为true的时候,这里反倒要设置invisible。从后面代码可以知道,这里无非是想通过动画来做一个渐显操作,而不是立马显示出来。
     view.setvisibility(invisible);
     state.targetvisibility = visible;

     layoutparams lp = new layoutparams(resolvedwidth, resolvedheight,
       resolvedgravity);
     lp.rightmargin = rightmargin;
     addview(view, lp);
     updatecolorviewtranslations();
    }
   } else {
    int vis = show ? visible : invisible;
    visibilitychanged = state.targetvisibility != vis;
    state.targetvisibility = vis;
    if (show) {
     layoutparams lp = (layoutparams) view.getlayoutparams();
     if (lp.height != resolvedheight || lp.width != resolvedwidth
       || lp.gravity != resolvedgravity || lp.rightmargin != rightmargin) {
      lp.height = resolvedheight;
      lp.width = resolvedwidth;
      lp.gravity = resolvedgravity;
      lp.rightmargin = rightmargin;
      view.setlayoutparams(lp);
     }
     //设置背景色
     view.setbackgroundcolor(color);
    }
   }
   //改变状态栏visibility属性时加入动画
   if (visibilitychanged) {
    view.animate().cancel();
    if (animate) {
     if (show) {
      //显示时先设置为全透明
      if (view.getvisibility() != visible) {
       view.setvisibility(visible);
       view.setalpha(0.0f);
      }
      //关于透明度的动画插值器
      view.animate().alpha(1.0f).setinterpolator(mshowinterpolator).
        setduration(mbarenterexitduration);
     } else {
      view.animate().alpha(0.0f).setinterpolator(mhideinterpolator)
        .setduration(mbarenterexitduration)
        .withendaction(new runnable() {
         @override
         public void run() {
          state.view.setalpha(1.0f);
          state.view.setvisibility(invisible);
         }
        });
     }
    } else {
     view.setalpha(1.0f);
     view.setvisibility(show ? visible : invisible);
    }
   }
  }

6. 总结

话说了这么多,其实就是想让大家知道为什么很多app放弃在较低版本的android系统对状态栏进行视觉修改,尤其是浅色导航栏的应用。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。