Android之Translucent Bar半透明栏和immersive bar沉浸式的区别

  在Android的Material Design出现后,一个更简洁,更舒服,更清爽的界面是开发者们所追求动,也是一个成功app的前提。那么怎么让app看起来更加舒服呢?这里有一个方法。

  让app看起来整体统一,整体统一也就是一体化的意思,怎么做到一体化呢,有两种方式,网上对于两种不同的方式经常混在一起讲,不过两者都是一体化的的概念

  1. Translucent bar(半透明的状态栏):状态栏和导航栏半透明,颜色可以随着app的样式进行相应的调整,和app的内容风格进行融合。这个是Google在SDK19中,提出的一个概念,也通过增加了api进行了一定的支持。本博客提供一个工具类能够更方便的操作Translucent bar——DyeingBarHelper

  2. 沉浸式状态栏:没有状态栏和导航栏,让一个app完整的展示在屏幕上。

从字面上的意思大概可以区分这两种模式的区别,下面也通过例子进行一个更加直观的说明,同时也进行一个实现。在继续看之前,要先了解下Status Bar(最顶部的有电池Wi-Fi信息的一栏)和NavigationBar(底部的返回键,home键,menu键,这个不是所有机子都有的,主要看是不是做在屏幕内了,还是独立于屏幕外)

Tanslucent Bar的实现——DyeingBarHelper(仓库地址)

  这种模式的主要效果是让app的整体风格保持一致,看起来更加清爽,舒服,界面统一。
在android的19之后,就有提供设置设置状态栏为半透明的方法:

// 设置Navigtion bar半透明
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
// 设置Status bar半透明  
getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);

还可以通过xml设置,然后再activity中设置theme

<style name="AppTheme" parent="AppBaseTheme">
    <!-- Status Bar -->
    <item name="android:windowTranslucentStatus">true</item>
    <!-- Navigation Bar -->
    <item name="android:windowTranslucentNavigation">true</item>
</style>

设置了半透明之后需要注意,整个的app会拉伸,也就是可使用的布局空间变成一整个屏幕,即空间会被status bar遮挡,再xml中可以设置如下代码,使得特定的组件不会被移动到顶部。我的建议是,如果你想和原来一样的话,直接在布局的最外层在加一层layout,在这个layout上使用一下代码,这样其他的布局都和原来一致,也不会有遮挡的问题。

android:fitsSystemWindows="true"

在android的21之后,就有提供API可以对状态栏进行颜色的更改:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    getWindow().setStatusBarColor(Color.GREEN);
    getWindow().setNavigationBarColor(Color.GREEN);
    mText.setText("看,轻而易举的实现了translucent bar的效果,但是目前可进行定制的能力不强");
} else {
    mText.setText("系统api要求21以上");
}

尽管Google已经提供给开发者更好的api去开发app,但是这远远达不到我们想随意操作的地步,所以,为了能够更加便捷的随心所欲的开发更具有一体化感觉的app,更改System UI也就是Status Bar和Navigation Bar是一个尤为重要的步骤。这里通过介绍一个工具类DyeingBarHelper来进行如何自定义System UI的分享。

原理

  System UI指的是Decor View,也就是一个app的窗口Window的最基本的组件view,也就是RootView,而DecorView是一个FrameLayout,其子view仅包括了一层LinearLayout,而LinearLaoyout->FrameLaoyout->LinearLayout之后就是开发者可以添加app界面的一个布局位置
  而由于SystemUI的实现是在LinearLayout层,而现在提供的API能够将SystemUI设置为半透明,那么我们要做的就是在FrameLayout中,把预设好的view塞到SystemUI的位置,这样我们就可以随心所欲的对view进行订制。

DyeingBarHelper的使用
// 设置状态栏和导航栏为透明,否则无法达到效果
DyeingBarHelper.setBarTranslucent(this);
// DyeingBarHelper的初始化
DyeingBarHelper helper = new DyeingBarHelper(this);
// 提供自定义navigation bar和status bar
// DyeingBarHelper helper = new DyeingBarHelper(this, statusView, navigationView);

// 设置颜色
helper.setStatusBarColor(Color.BLUE);
helper.setNavigationBarColor(Color.BLUE);
// 设置透明度
helper.setStatusBarViewAlpha(0);
helper.setNavigationBarViewAlpha(0);
// 设置可见
helper.setStatusBarViewVisibility(View.VISIBLE);
helper.setNavigationBarViewVisibility(View.VISIBLE);
// 设置背景
helper.setStatusBarViewBackground(drawable);
helper.setNavigationBarViewBackground(drawable);
示例

这里写图片描述

DyeingBarHelper的实现

工程主要包括三个类,
1. DyeingBarHelper.java 作用:设置SystemUI。
2. SystemBarConfig.java 作用:获取System UI的一些设置,比如StatusBar的高度等
3. TintingUtil.java 作用:帮助染色,从一个view中获取它的背景颜色,比如最多的颜色,平均颜色。方便之处,可以从title bar中获取颜色,使得整个app颜色达到一体化,可以看demo中的slideview的测试。

下面主要介绍一下DyeingBarHelper的整体思路:
1.判断是否设置了System UI透明。这个可以通过flag进行和Window的flag进行与运算。看其标志位是否被设置了。这个方法只适合于api19以后,前文有提及。若要设置透明度,前文也有提及。

// 以下方法在需要在api 19中使用
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    // 判断是否在theme里面定义了bar得透明
    int[] attrs = new int[]{android.R.attr.windowTranslucentStatus,
            android.R.attr.windowTranslucentStatus};
    TypedArray typedArray = mContext.obtainStyledAttributes(attrs);
    try {
        isStatusBarTranslucent = typedArray.getBoolean(0, false);
        isStatusBarTranslucent = typedArray.getBoolean(1, false);
    } finally {
        typedArray.recycle();
    }

    // 判断是否在代码中设置了bar透明
    Window window = ((Activity) mContext).getWindow();
    WindowManager.LayoutParams layoutParams = window.getAttributes();
    int bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS;
    if ((bits & layoutParams.flags) != 0) {
        isStatusBarTranslucent = true;
    }
    bits = WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION;
    if ((bits & layoutParams.flags) != 0) {
        isNavigationBarTranslucent = true;
    }
}

2.不是所有的机子都有Navigation Bar的,所以 要判断一下是否有NavigationBar,这个方法实在android中的源码学习而来,学会看android源码,才能进阶哇。

/**
 * 在Android l以上判断是否有NavigationBar的方法,通过查看phoneWindowManager源码可以知道这个方法
 *
 * @return
 */
public boolean hasNavigationBar() {
    boolean hasNavigationBar = false;
    Resources rs = mContext.getResources();
    int id = rs.getIdentifier("config_showNavigationBar", "bool", "android");
    if (id > 0) {
        hasNavigationBar = rs.getBoolean(id);
    }
    try {
        Class systemPropertiesClass = Class.forName("android.os.SystemProperties");
        Method m = systemPropertiesClass.getMethod("get", String.class);
        String navBarOverride = (String) m.invoke(systemPropertiesClass, "qemu.hw.mainkeys");
        if ("1".equals(navBarOverride)) {
            hasNavigationBar = false;
        } else if ("0".equals(navBarOverride)) {
            hasNavigationBar = true;
        }
    } catch (Exception e) {
        Log.w(TAG, e);
    }

    return hasNavigationBar;
}


3.如果有status bar和navigation bar那么就可以把自定义view塞到它们的位置上去了,由于获得的decor view是frame layout 所以,layoutparams使用frame layout的,同时要注意横屏和竖屏,通过判断手机当前的方向。另外需要注意的就是,如何获得status bar的高度和navigation bar的高度,主要是通过resource.getIdentifier()方法获取,当然首先是要知道对应的字段。

// 判断当前手机的方向
this.isPortrait = (res.getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT);

/**
 * 获取Status Bar的高度
 */
public int getStatusBarHeight() {
    String key = "status_bar_height";
    return getInternalDimensionSize(key);
}

/**
 * 获取Navigation Bar的高度
 *
 * @return
 */
public int getNavigationBarHeight() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        if (hasNavigationBar()) {
            String key;
            if (isPortrait) {
                key = "navigation_bar_height";
            } else {
                key = "navigation_bar_height_landscape";
            }
            return getInternalDimensionSize(key);
        }
    }
    return 0;
}

/**
 * 获取Navigation Bar的宽度
 *
 * @return
 */
public int getNavigationBarWidth() {
    String key = "navigation_bar_width";
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
        if (hasNavigationBar()) {
            return getInternalDimensionSize(key);
        }
    }
    return 0;
}

/**
 * 获取指定资源的值
 *
 * @param key
 * @return
 */
private int getInternalDimensionSize(String key) {
    int result = 0;
    int resourceId = res.getIdentifier(key, "dimen", "android");
    if (resourceId > 0) {
        result = res.getDimensionPixelSize(resourceId);
    }
    return result;
}

/**
 * 设置顶部Status Bar的view
 *
 * @param decorViewGroup
 */
private void setStatusBarView(ViewGroup decorViewGroup) {
    if (mStatusBarView == null) {
        mStatusBarView = new View(mContext);
    }
    FrameLayout.LayoutParams params;
    params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
            mConfig.getStatusBarHeight());
    params.gravity = Gravity.TOP;
    // 如果是横屏,则应该减去右边navigation bar的宽度,防止挡住navigation bar
    if (!mConfig.isNavigationBarAtBottom()) {
        params.rightMargin = mConfig.getNavigationBarWidth();
    }
    mStatusBarView.setLayoutParams(params);
    mStatusBarView.setBackgroundColor(DEFAULT_BAR_COLOR);
    decorViewGroup.addView(mStatusBarView);
}

/**
 * 设置底部Navigation Bar的view
 *
 * @param decorViewGroup
 */
private void setNavigationBarView(ViewGroup decorViewGroup) {
    if (mNavigationBarView == null) {
        mNavigationBarView = new View(mContext);
    }
    FrameLayout.LayoutParams params;
    if (mConfig.isNavigationBarAtBottom()) {
        params = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, mConfig.getNavigationBarHeight());
        params.gravity = Gravity.BOTTOM;
    } else {
        params = new FrameLayout.LayoutParams(mConfig.getNavigationBarWidth(), FrameLayout.LayoutParams.MATCH_PARENT);
        params.gravity = Gravity.RIGHT;
    }
    mNavigationBarView.setLayoutParams(params);
    mNavigationBarView.setBackgroundColor(DEFAULT_BAR_COLOR);
    decorViewGroup.addView(mNavigationBarView);
}

这样就大功告成。

接着介绍一下TintingUtil,这个的主要作用是提取view的颜色,想法很简单,就是把view的bitmap取出,就可以得到每一个像素点的颜色,这样就可以进行类似颜色最多的求取,平均颜色的求取。
1.首先从view中获取bitmap,多做的一个步骤就是在create bitmap的时候,防止内存不足而多次create。

/**
 * 从view中去获取一个bitmap
 * @param view
 * @return
 */
protected Bitmap createBitmapFromView(View view) {
    // ImageView直接获取它的drawable
    if (view instanceof ImageView) {
        Drawable drawable = ((ImageView) view).getDrawable();
        if (drawable != null && drawable instanceof BitmapDrawable) {
            return ((BitmapDrawable) drawable).getBitmap();
        }
    }

    // 有可能为0, 比如在activity正在onCreate时,不建议进行此操作
    int width = view.getWidth();
    int height = view.getHeight();

    Bitmap bitmap = createBitmapSafely(width, height, Bitmap.Config.ARGB_8888, 1);
    if (bitmap != null) {
        Canvas canvas = new Canvas(bitmap);
        view.draw(canvas);
        canvas.setBitmap(null);
        canvas = null;
    }
    return bitmap;
}

/**
 *
 * @param width
 * @param height
 * @param config
 * @param times  create bitmap的次数,决定于当前的内存
 * @return
 */
protected Bitmap createBitmapSafely(int width, int height, Bitmap.Config config, int times) {
    try {
        return Bitmap.createBitmap(width, height, config);
    } catch (OutOfMemoryError e) {
        if (times > 0) {
            System.gc();
            return createBitmapSafely(width, height, config, --times);
        }
    } catch (Exception e) {
        Log.e(TAG, "view width or height shuold not < 0");
    }
    return null;
}

2.获取bitmap像素点颜色,做其他操作。

int color = bitmap.getPixel(i, j);


DyeingBarHelper项目仓库地址


参考网址

android粒子爆炸效果
android systembarTint

沉浸式状态栏(仓库地址)

  这种模式主要是让整一个app能够霸占整个屏幕,能够让用户完全的沉浸于这个app中,有很多的app都有这样的功能,比如阅读类app,下面的展示图就是小米自带的一个阅读app。

原理

主要的原理是隐藏status bar和navigation bar

实现(android官网参考)

在android的官网上有这样的System UI的教程,如果想了解的更细致,建议浏览官网。

操作System UI的做法,由于System UI属于activity基础界面 decor view的其中的一个布局,可以通过设置可见来实现隐藏和展示:

// 获取窗口的decor view
mDecorView = getWindow().getDecorView();

/**
 * 隐藏status bar和navigation bar
 */
private void hideSystemUI() {
    mDecorView.setSystemUiVisibility(
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                    | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                    | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                    | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION // hide nav bar
                    | View.SYSTEM_UI_FLAG_FULLSCREEN // hide status bar
                    | View.SYSTEM_UI_FLAG_IMMERSIVE);
}

/** 
 * 显示status bar和navigation bar
 */
private void showSystemUI() {
  mDecorView.setSystemUiVisibility(
          View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                  | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                  | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}

在隐藏了System UI之后,如何动态的控制它在出现之后又自动隐藏呢,比如阅读app上的效果,点击出现System UI,一阵子后就消失。这个可以通过延时任务进行操作,同时要设置decorView的监听器,监听SystemUi的变化:

mDecorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
    @Override
    public void onSystemUiVisibilityChange(int visibility) {
        if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
            invokeHide();
        }
    }
});

public void invokeHide() {
    new Handler().postDelayed(new Runnable() {
        @Override
        public void run() {
            hideSystemUI();
        }
    }, 1000);
}

效果:
这里写图片描述


项目地址

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值