LZ-Says:
这些年,身边的“兄弟”越来越多,
真正的兄弟越来越少。。。
前言
今天,我们不讲美女,不讲三国,那么我们一起来聊聊有关Android中沉浸式实现。
关于写这篇文章的目的,有如下几点:
项目中几乎不可避免,没有沉浸式的app第一感觉有点low~
Android的博大精深,让LZ着迷不已,深入了解后,正好做一个总结,厚积薄发~
那么接下来,一起开启有关沉浸式的那些事儿吧~
本文目标
本文阅读大约需要5~10分钟,阅读完本文,你会收获如下内容:
明确官方提出沉浸式与实际开发或者说是我们现在通常说的沉浸式之前区别;
掌握沉浸式开发,做到心中有数,并能自行兼容Android 4.4设备;
基于以上俩点,拓展实现属于自己的沉浸式封装。
沉浸式简述(纠正--->比对--->Start Study Road)
首先,我们先来说下有关纠正的问题。
1. 关于沉浸式纠正
谷歌推崇的沉浸式,乃是让用户完全沉浸在app中,不受任何干扰,当然你可以理解为,当用户使用你我的App的时候,只能看到我们的东西,其他的都是看不到的,包括状态栏~!!!。
接下来,我们来看看什么是谷歌推崇的真正的沉浸式。
首先,阅读类软件想必大家都在用,我们一起来看看,他们的沉浸式是什么效果。很明显,看书全身心投入,状态栏你都看不见。不知道大家有没有下面的情况:
- Y:我就看一会儿,一会儿就睡觉;- Y:过了一会儿,看下时间。。。- Y:卧槽,特么的都几点了~ Fuck。。。
接着,我们继续往下看第二个例子,也就是我们常用的看片软件,嘿嘿,瞅瞅人家的沉浸式~
看视频的过程中,你只能全神贯注的盯着屏幕播放的画面,当然,当你有某种需求的时候,你可以点击下屏幕,其他的功能项才会展示出来~
看完这俩个示例,我们在看看我们实际项目开发过程中的沉浸式:
示例一:
在这里,想必大家已经明确了官方和我们实际开发中的区别了吧?这里再次为大家阐述下:
官方提出的沉浸式,乃是让用户完全投入App中,何为完全投入,就好比阅读小说或者看电影的App,you 只能看我的,系统的 Say GoodBye~
而实际项目开发过程中的沉浸式,好比QQ之类,只是标题栏与系统状态栏颜色一致即可。
下面,让我们着手开始沉浸式思考以及撸码之路吧~
沉浸式实现与兼容
有关沉浸式内容,大家首先要明确以下几点:
在Android Api 5.0 之后,可直接调用官方API进行设置,快速,有效且方便;
而有关Android Api 4.4,则需要单独进行兼容,同时这也是本文重点之一;
关于Android Api 4.4以下,只能抱歉,不兼容。
这时候有的小伙伴说了,为什么我的用的手机版本不在你说的范围内,照样也有这种效果呢?
关于这个,就设计到对不同厂商ROM进行兼容了。下面为大家简单举个小例子:
Android,因为开源而被大厂商进行不同定制化修改,从而造成的系统也是各不相同,比较明显的有小米、华为、魅族等等。每家做的东西毋庸置疑的一定要有着自家的特色,这些不同的特色都是基于Android而产生。 好比调用相机,大家难免会遇到一个很无奈的问题,为什么在这个手机没问题,在别的手机就不行了呢?这也跟厂商修改Android有关。 So,如果想让你的App尽善尽美,那么身为一名Android开发者,你不得不去和各大厂商其下各大品牌斗智斗勇。
伙计说,那么我怎么在Android Api 5.0之后实现沉浸式效果呢?
一、Android 5.0 实现沉浸式
首先,我们创建一个项目,api选择21,也就是Android 5.0系统,然后果断运行一波,看看效果(记得修改最低兼容Android Api版本为19)。
区别很是明显,这里为大家再次阐述下:
1.目前在AS上创建5.0项目,系统会自动为你配置好沉浸式,当然,颜色可能不是你想要的,不过没关系,自行修改即可; 2.为什么同一个项目,同一份代码,不同的手机系统版本上显示不一样?原因上面已经说过了,但是我觉得还是有必要再次申明下。 沉浸式效果,最低兼容Android 4.4,而在Android 5.0及其以上的版本上,官网为我们提供好了相关Api,But,仅适用于Android 5.0以上;
那么,它是如何实现的呢?
1. 基于主题Theme配置Android 5.0上沉浸式效果:
还记得LZ在Material Design第一章的时候,说的配置全局样式吗?Android Study之Material Design初体验(一),直接查看Style中配置文件。
<!-- Base application theme. --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <!-- Customize your theme here. --> <item name="colorPrimary">@color/colorPrimary</item> <item name="colorPrimaryDark">@color/colorPrimaryDark</item> <item name="colorAccent">@color/colorAccent</item> </style>
首先,引入了Theme.AppCompat样式,接着,设置了状态栏、标题栏的颜色值。
其中的关键点在于colorPrimaryDark属性的颜色值设置,例如,我现在将此属性颜色修改为红色,我们一起看看会有什么变化。
那么下面黑乎乎的我该如何修改呢?
很是easy~只需要俩步即可实现,如下:
创建value-v21目录,并新增styles.xml,当然此步可直接拷贝values下的styles文件;
新增样式如下:
<item name="android:navigationBarColor">?attr/colorError</item>
运行即可见~鸡贼的小伙伴说了,既然有第一种方案,那第二种实现方式优势什么呢?
往下看
2. 基于代码实现Android 5.0上沉浸式效果:
我们通过代码调用关于官方提供api实现,如下:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { getWindow().setStatusBarColor(Color.YELLOW); }
因为我们之前说过,官方提供5.0以上api,所以,在此需要对当前系统版本做校验。
下面简单看下LZ怎么知道这api仅支持5.0以上的。鼠标浮上去,自动,嘿嘿嘿,当然这块需要单独对AS进行设置哦~
到此,基于Android 5.0实现沉浸式ok了,下面简单的对比下俩种方案的差异:
使用样式方案设置沉浸式,虽说方便快捷,但是仅适用于界面风格较为统一,当然这里你可以理解为至少标题栏以及状态栏在这个app的界面中都是保持一致;
而使用官方提供api,可直接在BaseActivity中设置全局,效果类似于使用主题,也可以在有差异的Activity中设置其相符的颜色。
至于以上关于Android 5.0该使用哪儿种方案,跟随项目而定~
二、兼容Android 4.4 实现沉浸式
兼容Android 4.4时,我们需要了解如下小知识点:
1.谷歌4.4 推出沉浸式,效果即为半透明; 2.关于实现方案同理如Android 5.0沉浸式一样,可通过Theme主题配置或代码设置。
1. 基于主题兼容Android 4.4上沉浸式效果
创建values-19目录以及styles.xml,新增如下参数:
<item name="android:windowTranslucentStatus">true</item>
这种方式优缺点就好比中西医之间的区别:
西医治标不治本,中医治本养膘。而如同我们使用Theme兼容Android 4.4系统版本一样,同样,如果项目使用主色调统一,使用当前的方案是比较方便的,But,如果不一样,这个的兼容性就大打折扣了。
而相对比,代码方式而更为实用,如下。
2. 基于代码设置兼容Android 4.4上沉浸式效果
// 设置状态栏的透明属性 兼容4.4 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); } setContentView(R.layout.activity_main);
这里需要注明俩点:
兼容范围:Android 4.4 ~Android 5.0 ,区间Android版本只需要设置半透明即可;
必须在setContentView前进行设置。
效果实现了,但是这里有个问题,上图可能看的不是很清楚,下面我们设置下toolBar的子标题以及Logo,
有关兼容问题,我们讲述到这里,下面,开始我们兼容处理。
这个处理包含,当我们实现了沉浸式,也就是状态栏和标题栏颜色一致后,布局或者其他因此而引发的问题。
问题是要解决滴。
解决兼容引发的问题
问题一:状态栏遮挡了部分界面,如何解决?
解决方案一: 在ToolBar设置fitsSystemWindows为true
之后我们运行下效果查看。
这里简单介绍下fitsSystemWindows的属性作用:
当fitsSystemWindows设置为true时,代表通知系统在设置布局时需要考虑当前系统窗口的布局,以适应我们的布局。而false则恰恰相反。
那么这种解决就完美了么?
No,no,no。
在实际开发中,当一个界面陈载太多的子控件时,往往我们将外面的根布局设置为ScrollView,但是,当我们的ScrollView中包含EditText时,它便会引发如下问题,
从上图很清晰的看到,当EditView获取焦点,ToolBar会因为软键盘的原因而导致整个布局的下拉(ToolBar)。
So,基于以上Bug,我们换一种方式。
解决方案二: 在根布局设置fitsSystemWindows为true
设置之后,
而设置之后,会发现,虽然布局正常了,但是我们的状态栏却变成了这个样子。
那么基于以上所谓的小bug,我们能不能设置当前背景跟随系统的呢?曲线救国,能救国就好~
android:background="?attr/colorPrimary"
当然,上面的效果图不是很明显,LZ这里再次设置下我们的ToolBar背景跟随系统,也就是colorPrimary的颜色值。
这时候,大家又发现了一个问题:你特么逗我?怎么我整个布局背景都这样了?
你看你看,小伙子脾气太暴~!!!这里送给大家一句话:
开发的过程,就是写bug,改bug,再写bug,再改,无线循环~ 当然,在某天,当你到达一定高度:撸码间,bug灰飞烟灭~!!!
So,既然兼容Android 4.4 ,我们采用的是曲线救国的方案,那么重点都搞定了,只是一个布局颜色而已,我们只需要再次设置下布局颜色不就好了吗?
瞧着,伙计~肿么样?我就问你肿么样~!!!
可能大家会觉得,好麻烦。由第三方的你为毛不用?
LZ个人见解如下:
别人再好,终究是他人,好的开发,应该学着去看他实现原理,或者说是实现的思路是如何,其实和我们这种差不多。 这里不得不说一句,之前看到鸡排兄的文章,里面依稀记得是关于闹钟,貌似底层也是写了一个死循环,依次去做校验。你觉得麻烦吗? 就好比之前的什么注解方式减少FindViewById,可能也就是在使用的过程中觉得省事,但实际他底层还是去调用了FindViewById,只不过人家进行一定的封装,优化,通过对外API,让我们使用起来更加的方便,快捷而已。 (注:以上属于LZ个人见解,如有口误,欢迎拍砖,及时更正,避免误导~欢迎指导~!!!)
解决方案三: 动态获取ToolBar高度并设置ToolBar高度
现在,我们还原到我们之前的效果,
此时的ToolBar的高度出现了问题。那么官方没有提供相应的API,我们先来看看系统源码中是否设置相关的属性内容。
了解过的小伙伴知道,系统定义了ToolBar的高度为:24dp,而不清楚的,今天LZ教你怎么能看到ToolBar高度值。
首先LZ的Android API版本为26,貌似23可以看到源码,只能曲线救国了。还是建议大家将API更新的及时一些,可以看到最新的更新以及比较6的方式。
如上图,将工程目录切换到Project模式下,直接查看当前Android API下好东西。下面附上地址图:
根据谷歌鉴名其意,我们直接搜索status_bar_height,如下:
<!-- Height of the status bar --> <dimen name="status_bar_height">24dp</dimen> <!-- Height of the bottom navigation / system bar. --> <dimen name="navigation_bar_height">48dp</dimen>
而我们目前实现的思路也是通过拿到这个值,有的小伙伴说了,直接给死24dp不久得了嘛。
这个,只能默默来句,不一样伙计,真的~保不齐某个机型就不是24dp的ToolBar了。。。
系统不提供,我们只能通过反射,去拿到。
/** * 获取状态栏高度 * * @param context * @return */ private int getStatusBarHeight(Context context) { int stateHeight = 0; // 反射R类 try { // 指定目标地址 Class<?> clazz = Class.forName("com.android.internal.R$dimen"); // 实例化 Object object = clazz.newInstance(); // 获取属性值 String heightStr = clazz.getField("status_bar_height").get(object).toString(); // 转换 int height = Integer.parseInt(heightStr); // dp --- px stateHeight = context.getResources().getDimensionPixelOffset(height); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } return stateHeight; }
接下来,动态设置ToolBar高度。
// 设置状态栏的透明属性 兼容4.4 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); } setContentView(R.layout.activity_two); mToolbar = findViewById(R.id.id_toolbar); // 方式一:动态获取toolbar高度 LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) mToolbar.getLayoutParams(); int statusBarHeight = getStatusBarHeight(this); params.height += statusBarHeight; mToolbar.setLayoutParams(params);
为什么会出现这种情况呢?
一句话,还没开始绘制就获取高度,height肯定为0啊。
肿么办?
引用系统值呗,如下:
android:layout_height="?attr/actionBarSize"
再次运行
而关于界面友好性,我们为其设置Padding,原因呢,很简单。
假设我们ToolBar包含TextView,如果不设置Padding,那么造成的结果或者说显示的结果就是,我们的TextView会在左上角,会被状态栏遮挡。
新增如下:
mToolbar.setPadding(mToolbar.getPaddingLeft(), mToolbar.getTop() + getStatusBarHeight(this), mToolbar.getPaddingRight(), mToolbar.getPaddingBottom());
高级进阶 - 让NavigationBar实现沉浸式效果
基于以上套路,我们同样需要考虑版本兼容性:
1.Android API 5.0以上,包含5.0 系统系统api,可通过属性或者代码实现; 2.Android API 4.4以上,5.0以下,不包含5.0需要特殊处理,处理方案与设置状态栏思路基本一致。
下面,我们快速讲解有关如何实现NavigaitonBar沉浸式,小伙子,能hou住吗?
5.0 实现方式
1. 通过属性解决
这里需要注明一点:
由于我们只针对Android API 5.0以上做效果,So,首先需要在res下创建一个values-v21文件。
基于以上,设置属性如下:
<item name="android:navigationBarColor">@color/colorPrimaryDark</item>
2. 通过代码设置
这里需要注明:
由于Android API 5.0提供相关API,但是只限于在5.0上使用,所以,我们需要去判断当前系统版本。
如下:
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP){ getWindow().setNavigationBarColor(Color.YELLOW); }
这次的效果更骚的一比
4.4 兼容
老司机,老套路,同样兼容4.4 状态栏思路
设置NavigationbarColor设置为透明,并设置整体布局背景色
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); }
小伙伴说,我去,这个这么简单吗?
我们先来看看效果
咦,怎么遮挡我布局了呢?
这时候,我们采用一种曲线救国的方案:
布局底部添加一个高度为0.1dp的View,并设置上下比例填充以及背景色;
动态设置底部View的高度为虚拟导航的高度。
至于为什么需要动态获取,这里再次复述一遍:
我们查看系统源码后得知,NavigationBar高度为48dp,由于Android开源性以及各大厂商各自施展自己的才华,难免会对Android内部进行修改,每个厂商ROM不一样,你敢保证所有的都是48dp么?所以,动态获取,最后的选择之一~
布局如下:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:background="?attr/colorPrimary" android:orientation="vertical" tools:context="com.hlq.androidimmersion.MainActivity"> <android.support.v7.widget.Toolbar android:id="@+id/id_toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="@color/colorAccent" app:title="HLQ-Blog" /> <ScrollView android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#FFF"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> 。。。 </LinearLayout> </ScrollView> <View android:id="@+id/id_nav" android:layout_width="match_parent" android:layout_height="0.1dp" android:layout_weight="0" android:background="@color/colorAccent" /></LinearLayout>
随后,通过反射拿到底部导航栏高度,并设置我们定义的View高度即可。
mNavView = findViewById(R.id.id_nav); ViewGroup.LayoutParams navParams = mNavView.getLayoutParams(); navParams.height += getNavigationBarHeight(this); mNavView.setLayoutParams(navParams);
关于通过反射获取高度,与上面获取状态栏一致,只是名字不一样而已,后期可直接上GitHub上查看代码。
接着,运行看一下效果
是不是很Nice?
而基于以上俩部分,我们考虑能否搞一个Base?通过继承即可实现,方便快捷?
封装自己的沉浸式Base
封装目的性,简洁来说就是为了开发更爽。
而封装前,我们需要考虑如下几个问题:
Android SDK 兼容:5.0提供api,4.4自己玩兼容;
特殊机型兼容:有些手机有底部虚拟导航栏有些没有,该如何玩转?
首先,我们还是需要捋捋思路,思路清晰了,写也就不是什么难事了。
重点难点在于什么?
1. 对于不熟悉的伙计,通过反射拿到高度;2. 如何判断是否具有底部虚拟导航栏?
思路在于什么?
1. 判断当前系统版本,针对不同版本做相应的处理; 2. 封装原有反射获取系统高度方法,方便调用; 3. 判断是否当前是否具有虚拟导航栏。此处思路在于,获取当前系统屏幕高度以及内容显示高度,用系统屏幕高度减去内容显示高度结果进行校验,如果>0代表虚拟导航栏存在。同时为了兼容某些手机在左侧右侧,可同时获取宽度,思路如上一致。
代码如下:
package com.hlq.androidimmersion;import android.content.Context;import android.os.Build;import android.os.Bundle;import android.support.annotation.Nullable;import android.support.v7.app.AppCompatActivity;import android.support.v7.widget.Toolbar;import android.util.DisplayMetrics;import android.view.Display;import android.view.View;import android.view.ViewGroup;import android.view.WindowManager;import android.widget.LinearLayout;/** * Created by HLQ on 2017/11/30 */public class BaseActivity extends AppCompatActivity { @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 判断当前系统版本 // 分为俩个区间 1. 大于等于4.4 小于5.0 2.大于等于5.0 // 原因 4.4没有提供api 需要单独设置 而 5.0提供相关api if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT = Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT = Build.VERSION_CODES.LOLLIPOP) { // 5.0 版本以上 包含 5.0 // 设置顶部颜色 toolbar.setBackgroundColor(translucentPrimaryColor); // 通过api设置状态栏颜色 getWindow().setStatusBarColor(translucentPrimaryColor); // 设置底部虚拟导航栏颜色 getWindow().setNavigationBarColor(translucentPrimaryColor); } else { // 小于4.4 不做处理 } } /** * 获取状态栏高度 * * @param context * @return */ private int getStatusBarHeight(Context context) { return getSystemComponentDimen(this, "status_bar_height"); } /** * 获取底部导航栏高度 * * @param context * @return */ private int getNavigationBarHeight(Context context) { return getSystemComponentDimen(this, "navigation_bar_height"); } /** * 反射拿到系统属性值 * * @param context * @param dimenName * @return */ private int getSystemComponentDimen(Context context, String dimenName) { int stateHeight = 0; // 反射R类 try { // 指定目标地址 Class<?> clazz = Class.forName("com.android.internal.R$dimen"); // 实例化 Object object = clazz.newInstance(); // 获取属性值 String heightStr = clazz.getField(dimenName).get(object).toString(); // 转换 int height = Integer.parseInt(heightStr); // dp --- px stateHeight = context.getResources().getDimensionPixelOffset(height); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (InstantiationException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } return stateHeight; } /** * 判断是否存在NavigationBar * * @param windowManager * @return */ private static boolean hasNavigationBarShow(WindowManager windowManager) { Display display = windowManager.getDefaultDisplay(); DisplayMetrics outMetrics = new DisplayMetrics(); display.getRealMetrics(outMetrics); // 获取整个屏幕高度 int heightPixelsAll = outMetrics.heightPixels; // 获取整个屏幕宽度 int widthPixelsAll = outMetrics.widthPixels; // 获取内容展示部分的高度 outMetrics = new DisplayMetrics(); display.getMetrics(outMetrics); // 获取内容显示区域高度 int heightPixelsContent = outMetrics.heightPixels; // 获取内容显示区域宽度 int widthPixelsContent = outMetrics.widthPixels; int width = widthPixelsAll - widthPixelsContent; int height = heightPixelsAll - heightPixelsContent; return width > 0 || height > 0; // 兼容横竖屏 } }
那么继承之后,如何使用呢?很是Easy
public class ThreeActivity extends BaseActivity { private Toolbar mToolBar; private View mNavBar; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_three); mToolBar = findViewById(R.id.id_toolbar); mNavBar = findViewById(R.id.id_nav); setOrChangeTranslucentColor(mToolBar, mNavBar, getResources().getColor(R.color.colorPrimary)); } }
Base封装完成,那么接下来测试下实际效果,看看我们封装的到底行不行。
以上三张图片对应的机型分别为:华为荣耀 6 X 底部导航开启状态、华为荣耀 6 X 底部导航关闭状态以及华为荣耀8显示状态。
上面系统版本均为5.0以上,现在我们来看看在4.4上如何?
让我们拭目以待~
就问你6不6?
当然,谁也不能保证自己的没问题,尤其身处于Android碎片严重的现在,But,能做的就是尽量兼容~
后记
通过以上内容,相信大家已经心中有数,欢迎各位老铁拍砖~