目录介绍

  • 1.类似酷狗等锁屏页面实现步骤
  • 1.1 什么是锁屏联动媒体播放器
  • 1.2 如何实现锁屏页面
  • 1.3 关于自定义锁屏页面左右滑动的控件
  • 1.4 注意要点分析
  • 1.5 具体完整代码的案例
  • 1.6 效果图展示案例
  • 2.自定义锁屏页的基本原理
  • 2.1 基本原理
  • 2.2 原理图形展示
  • 2.3 讨论一些细节
  • 3.锁屏Activity配置信息说明
  • 3.1 去掉系统锁屏做法
  • 3.2 权限问题
  • 4.屏蔽物理或者手机返回键
  • 4.1 为什么要这样处理
  • 4.2 如何实现,实现逻辑代码
  • 5.滑动屏幕解锁
  • 5.1 滑动解锁原理
  • 5.2 滑动控件自定义
  • 6.透明栏与沉浸模式
  • 6.1 透明栏与沉浸模式的概念
  • 6.2 如何实现,代码展示
  • 7.用户指纹识别,如何锁屏页面失效
  • 8.关于其他
  • 8.1 版本更新情况
  • 8.2 参考案例
  • 8.3 个人博客

0.备注

1.类似酷狗等锁屏页面实现步骤

1.1 什么是锁屏联动媒体播放器

  • 播放器除了播放了音乐之外什么都没做,就可以分别在任务管理、锁屏、负一屏控制播放器。
  • 也可以这样通俗的解释,这个举例子说一个应用场景,我使用混沌大学听音频,然后我关闭了屏幕(屏幕灭了),当我再次打开的时候,屏幕的锁屏页面或者顶层页面便会出现一层音频播放器控制的页面,那么即使我不用解锁屏幕,也照样可以控制音频播放器的基本播放操作。如果你细心观察一下,也会发现有些APP正式这样操作的。目前我发现QQ音乐,酷狗音乐,混沌大学等是这样的
  • 如何实现,逻辑思路
  • 第一步:在服务中注册屏幕熄灭广播
  • 第二步:处理逻辑,发现屏幕熄灭就开启锁屏页面,再次点亮屏幕时就可以看到锁屏页面
  • 第三步:点击锁屏页面上的按钮,比如上一首,下一首,播放暂停可以与主程序同步信息。
  • 第四步:滑动锁屏页面,锁屏页面被销毁,进入程序主界面。

1.2 如何实现锁屏页面

  • 1.2.1 注册一个广播接收者监听屏幕亮了或者灭了
public class AudioBroadcastReceiver extends BroadcastReceiver {
    
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if(action!=null && action.length()>0){
            switch (action){
                //锁屏时处理的逻辑
                case Constant.LOCK_SCREEN:
                    PlayService.startCommand(context,Constant.LOCK_SCREEN);
                    break;
                //当屏幕灭了
                case Intent.ACTION_SCREEN_OFF:
                    PlayService.startCommand(context,Intent.ACTION_SCREEN_OFF);
                    break;
                //当屏幕亮了
                case Intent.ACTION_SCREEN_ON:
                    PlayService.startCommand(context,Intent.ACTION_SCREEN_ON);
                    break;
                default:
                    break;
            }
        }
    }
}
  • 1.2.2 在服务中开启和注销锁屏操作
  • 在oncreate方法中注册广播接收者
final IntentFilter filter = new IntentFilter();
//锁屏
filter.addAction(Constant.LOCK_SCREEN);
//当屏幕灭了
filter.addAction(Intent.ACTION_SCREEN_OFF);
//当屏幕亮了
filter.addAction(Intent.ACTION_SCREEN_ON);
registerReceiver(mAudioReceiver, filter);
  • 在ondestory方法中注销广播接收者
unregisterReceiver(mAudioReceiver);

1.3 关于自定义锁屏页面左右滑动的控件

  • 1.3.1 只有从左向右滑动的自定义控件
public class SlitherFinishLayout extends RelativeLayout implements OnTouchListener {

    /** 
     * SlitherFinishLayout布局的父布局 
     */  
    private ViewGroup mParentView;
    /** 
     * 处理滑动逻辑的View 
     */  
    private View touchView;
    /** 
     * 滑动的最小距离 
     */  
    private int mTouchSlop;  
    /** 
     * 按下点的X坐标 
     */  
    private int downX;  
    /** 
     * 按下点的Y坐标 
     */  
    private int downY;  
    /** 
     * 临时存储X坐标 
     */  
    private int tempX;  
    /** 
     * 滑动类 
     */  
    private Scroller mScroller;
    /** 
     * SlitherFinishLayout的宽度 
     */  
    private int viewWidth;  
    /** 
     * 记录是否正在滑动 
     */  
    private boolean isSlither;  
      
    private OnSlitherFinishListener onSlitherFinishListener;  
    private boolean isFinish;  
      
  
    public SlitherFinishLayout(Context context, AttributeSet attrs) {
        this(context, attrs, 0);  
    }  


    public SlitherFinishLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        mScroller = new Scroller(context);
    }  


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {  
        super.onLayout(changed, l, t, r, b);  
        if (changed) {  
            // 获取SlitherFinishLayout所在布局的父布局  
            mParentView = (ViewGroup) this.getParent();
            viewWidth = this.getWidth();  
        }  
    }  


    /** 
     * 设置OnSlitherFinishListener, 在onSlitherFinish()方法中finish Activity 
     * @param onSlitherFinishListener           listener
     */  
    public void setOnSlitherFinishListener(OnSlitherFinishListener onSlitherFinishListener) {
        this.onSlitherFinishListener = onSlitherFinishListener;  
    }  
  
    /** 
     * 设置Touch的View 
     * @param touchView
     */  
    public void setTouchView(View touchView) {
        this.touchView = touchView;  
        touchView.setOnTouchListener(this);  
    }  


    public View getTouchView() {
        return touchView;  
    }

  
    /** 
     * 滚动出界面 
     */  
    private void scrollRight() {  
        final int delta = (viewWidth + mParentView.getScrollX());  
        // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item  
        mScroller.startScroll(mParentView.getScrollX(), 0, -delta + 1, 0, Math.abs(delta));
        postInvalidate();  
    }  
  
    /** 
     * 滚动到起始位置 
     */  
    private void scrollOrigin() {  
        int delta = mParentView.getScrollX();  
        mScroller.startScroll(mParentView.getScrollX(), 0, -delta, 0, Math.abs(delta));
        postInvalidate();  
    }  
  
    /** 
     * touch的View是否是AbsListView, 例如ListView, GridView等其子类 
     * @return
     */  
    private boolean isTouchOnAbsListView() {
        return touchView instanceof AbsListView ? true : false;
    }  
  
    /** 
     * touch的view是否是ScrollView或者其子类 
     * @return
     */  
    private boolean isTouchOnScrollView() {  
        return touchView instanceof ScrollView ? true : false;
    }  


    @Override
    public boolean onTouch(View v, MotionEvent event) {
        switch (event.getAction()) {  
            case MotionEvent.ACTION_DOWN:
                downX = tempX = (int) event.getRawX();
                downY = (int) event.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int) event.getRawX();
                int deltaX = tempX - moveX;
                tempX = moveX;
                if (Math.abs(moveX - downX) > mTouchSlop && Math.abs((int) event.getRawY() - downY) < mTouchSlop) {
                    isSlither = true;
                    // 若touchView是AbsListView,
                    // 则当手指滑动,取消item的点击事件,不然我们滑动也伴随着item点击事件的发生
                    if (isTouchOnAbsListView()) {
                        MotionEvent cancelEvent = MotionEvent.obtain(event);
                        cancelEvent.setAction(MotionEvent.ACTION_CANCEL
                                        | (event.getActionIndex() << MotionEvent.ACTION_POINTER_INDEX_SHIFT));
                        v.onTouchEvent(cancelEvent);
                    }
                }
                if (moveX - downX >= 0 && isSlither) {
                    mParentView.scrollBy(deltaX, 0);
                    // 屏蔽在滑动过程中ListView ScrollView等自己的滑动事件
                    if (isTouchOnScrollView() || isTouchOnAbsListView()) {
                        return true;
                    }
                }
                break;
            case MotionEvent.ACTION_UP:
                isSlither = false;
                if (mParentView.getScrollX() <= -viewWidth / 2) {
                    isFinish = true;
                    scrollRight();
                } else {
                    scrollOrigin();
                    isFinish = false;
                }
                break;
            default:
                break;
        }
        // 假如touch的view是AbsListView或者ScrollView 我们处理完上面自己的逻辑之后  
        // 再交给AbsListView, ScrollView自己处理其自己的逻辑  
        if (isTouchOnScrollView() || isTouchOnAbsListView()) {  
            return v.onTouchEvent(event);  
        }
        // 其他的情况直接返回true  
        return true;  
    }  


    @Override
    public void computeScroll() {  
        // 调用startScroll的时候scroller.computeScrollOffset()返回true,  
        if (mScroller.computeScrollOffset()) {  
            mParentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());  
            postInvalidate();
            if (mScroller.isFinished()) {
                if (onSlitherFinishListener != null && isFinish) {  
                    onSlitherFinishListener.onSlitherFinish();  
                }  
            }  
        }  
    }  
      
  
    public interface OnSlitherFinishListener {  
        void onSlitherFinish();
    }  
  
}  
  • 1.3.2 支持向左或者向右滑动的控件,灵活处理
public class SlideFinishLayout extends RelativeLayout {

    private final String TAG = SlideFinishLayout.class.getName();

    /**
     * SlideFinishLayout布局的父布局
     */
    private ViewGroup mParentView;

    /**
     * 滑动的最小距离
     */
    private int mTouchSlop;
    /**
     * 按下点的X坐标
     */
    private int downX;
    /**
     * 按下点的Y坐标
     */
    private int downY;
    /**
     * 临时存储X坐标
     */
    private int tempX;
    /**
     * 滑动类
     */
    private Scroller mScroller;
    /**
     * SlideFinishLayout的宽度
     */
    private int viewWidth;
    /**
     * 记录是否正在滑动
     */
    private boolean isSlide;

    private OnSlideFinishListener onSlideFinishListener;

    /**
     * 是否开启左侧切换事件
     */
    private boolean enableLeftSlideEvent = true;
    /**
     * 是否开启右侧切换事件
     */
    private boolean enableRightSlideEvent = true;
    /**
     * 按下时范围(处于这个范围内就启用切换事件,目的是使当用户从左右边界点击时才响应)
     */
    private int size ;
    /**
     * 是否拦截触摸事件
     */
    private boolean isIntercept = false;
    /**
     * 是否可切换
     */
    private boolean canSwitch;
    /**
     * 左侧切换
     */
    private boolean isSwitchFromLeft = false;
    /**
     * 右侧侧切换
     */
    private boolean isSwitchFromRight = false;


    public SlideFinishLayout(Context context) {
        super(context);
        init(context);
    }
    public SlideFinishLayout(Context context, AttributeSet attrs) {
        super(context, attrs, 0);
        init(context);
    }
    public SlideFinishLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    private void init(Context context) {
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
        Log.i(TAG, "设备的最小滑动距离:" + mTouchSlop);
        mScroller = new Scroller(context);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        if (changed) {
            // 获取SlideFinishLayout所在布局的父布局
            mParentView = (ViewGroup) this.getParent();
            viewWidth = this.getWidth();
            size = viewWidth;
        }
        Log.i(TAG, "viewWidth=" + viewWidth);
    }


    public void setEnableLeftSlideEvent(boolean enableLeftSlideEvent) {
        this.enableLeftSlideEvent = enableLeftSlideEvent;
    }


    public void setEnableRightSlideEvent(boolean enableRightSlideEvent) {
        this.enableRightSlideEvent = enableRightSlideEvent;
    }

    /**
     * 设置OnSlideFinishListener, 在onSlideFinish()方法中finish Activity
     * @param onSlideFinishListener         onSlideFinishListener
     */
    public void setOnSlideFinishListener(OnSlideFinishListener onSlideFinishListener) {
        this.onSlideFinishListener = onSlideFinishListener;
    }

    /**
     * 是否拦截事件,如果不拦截事件,对于有滚动的控件的界面将出现问题(相冲突)
     */
    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        float downX = ev.getRawX();
        Log.i(TAG, "downX =" + downX + ",viewWidth=" + viewWidth);
        if(enableLeftSlideEvent && downX < size){
            Log.e(TAG, "downX 在左侧范围内 ,拦截事件");
            isIntercept = true;
            isSwitchFromLeft = true;
            isSwitchFromRight = false;
            return false;
        }else if(enableRightSlideEvent && downX > (viewWidth - size)){
            Log.i(TAG, "downX 在右侧范围内 ,拦截事件");
            isIntercept = true;
            isSwitchFromRight = true;
            isSwitchFromLeft = false;
            return true;
        }else{
            Log.i(TAG, "downX 不在范围内 ,不拦截事件");
            isIntercept = false;
            isSwitchFromLeft = false;
            isSwitchFromRight = false;
        }
        return super.onInterceptTouchEvent(ev);
    }


    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        //不拦截事件时 不处理
        if(!isIntercept){
            Log.d(TAG,"false------------");
            return false;
        }
        Log.d(TAG,"true-----------");
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                downX = tempX = (int) event.getRawX();
                downY = (int) event.getRawY();
                Log.d(TAG,"downX---"+downX+"downY---"+downY);
                break;
            case MotionEvent.ACTION_MOVE:
                int moveX = (int) event.getRawX();
                int deltaX = tempX - moveX;
                tempX = moveX;
                if (Math.abs(moveX - downX) > mTouchSlop && Math.abs((int) event.getRawY() - downY) < mTouchSlop) {
                    isSlide = true;
                }
                Log.e(TAG, "scroll deltaX=" + deltaX);
                //左侧滑动
                if(enableLeftSlideEvent){
                    if (moveX - downX >= 0 && isSlide) {
                        mParentView.scrollBy(deltaX, 0);
                    }
                }
                //右侧滑动
                if(enableRightSlideEvent){
                    if (moveX - downX <= 0 && isSlide) {
                        mParentView.scrollBy(deltaX, 0);
                    }
                }
                Log.i(TAG + "/onTouchEvent", "mParentView.getScrollX()=" + mParentView.getScrollX());
                break;
            case MotionEvent.ACTION_UP:
                isSlide = false;
                //mParentView.getScrollX() <= -viewWidth / 2  ==>指左侧滑动
                //mParentView.getScrollX() >= viewWidth / 2   ==>指右侧滑动
                if (mParentView.getScrollX() <= -viewWidth / 2 || mParentView.getScrollX() >= viewWidth / 2) {
                    canSwitch = true;
                    if(isSwitchFromLeft){
                        scrollToRight();
                    }

                    if(isSwitchFromRight){
                        scrollToLeft();
                    }
                } else {
                    scrollOrigin();
                    canSwitch = false;
                }
                break;
            default:
                break;
        }
        return true;
    }


    /**
     * 滚动出界面至右侧
     */
    private void scrollToRight() {
        final int delta = (viewWidth + mParentView.getScrollX());
        // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item
        mScroller.startScroll(mParentView.getScrollX(), 0, -delta + 1, 0, Math.abs(delta));
        postInvalidate();
    }

    /**
     * 滚动出界面至左侧
     */
    private void scrollToLeft() {
        final int delta = (viewWidth - mParentView.getScrollX());
        // 调用startScroll方法来设置一些滚动的参数,我们在computeScroll()方法中调用scrollTo来滚动item
        //此处就不可用+1,也不卡直接用delta
        mScroller.startScroll(mParentView.getScrollX(), 0, delta - 1, 0, Math.abs(delta));
        postInvalidate();
    }

    /**
     * 滚动到起始位置
     */
    private void scrollOrigin() {
        int delta = mParentView.getScrollX();
        mScroller.startScroll(mParentView.getScrollX(), 0, -delta, 0, Math.abs(delta));
        postInvalidate();
    }

    @Override
    public void computeScroll(){
        // 调用startScroll的时候scroller.computeScrollOffset()返回true,
        if (mScroller.computeScrollOffset()) {
            mParentView.scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
            postInvalidate();

            if (mScroller.isFinished()) {
                if (onSlideFinishListener != null && canSwitch) {
                    //回调,左侧切换事件
                    if(isSwitchFromLeft){
                        onSlideFinishListener.onSlideBack();
                    }
                    //右侧切换事件
                    if(isSwitchFromRight){
                        onSlideFinishListener.onSlideForward();
                    }
                }
            }
        }
    }


    public interface OnSlideFinishListener {
        void onSlideBack();
        void onSlideForward();
    }

}

1.4 注意要点分析

  • 1.4.1 在清单文件需要注册属性
 <activity android:name=".ui.lock.LockTestActivity"
            android:noHistory="false"
            android:excludeFromRecents="true"
            android:screenOrientation="portrait"
            android:exported="false"
            android:launchMode="singleInstance"
            android:theme="@style/LockScreenTheme"/>
  • 1.4.2 程序在前台时,当从锁屏页面finish时,会有闪屏效果
  • 如果加上这句话android:launchMode="singleInstance",那么程序在前台时会有闪屏效果,如果在后台时,则直接展现栈顶页面
  • 如果不加这句话

1.5 具体完整代码的案例

2.自定义锁屏页的基本原理

2.1 基本原理

  • Android系统实现自定义锁屏页的思路很简单,即在App启动时开启一个service,在Service中时刻监听系统SCREEN_OFF的广播,当屏幕熄灭时,Service监听到广播,开启一个锁屏页Activity在屏幕最上层显示,该Activity创建的同时会去掉系统锁屏(当然如果有密码是禁不掉的)。

2.2 原理图形展示

  • image

2.3 讨论一些细节

  • 2.3.1 关于启动Activity时Intent的Flag问题
  • 如果不添加FLAG_ACTIVITY_NEW_TASK的标志位,会出现“Calling startActivity() from outside of an Activity”的运行时异常,毕竟我们是从Service启动的Activity。Activity要存在于activity的栈中,而Service在启动activity时必然不存在一个activity的栈,所以要新起一个栈,并装入启动的activity。使用该标志位时,也需要在AndroidManifest中声明taskAffinity,即新task的名称,否则锁屏Activity实质上还是在建立在原来App的task栈中。
  • 标志位FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,是为了避免在最近使用程序列表出现Service所启动的Activity,但这个标志位不是必须的,其使用依情况而定。
  • 2.3.2 动态注册广播接收者
IntentFilter mScreenOffFilter = new IntentFilter();
mScreenOffFilter.addAction(Intent.ACTION_SCREEN_OFF);
registerReceiver(mScreenOffReceiver, mScreenOffFilter);

3.锁屏Activity配置信息说明

3.1 去掉系统锁屏做法

  • 在自定义锁屏Activity的onCreate()方法里设定以下标志位就能完全实现相同的功能:
//注意需要做一下判断
if (getWindow() != null) {
    Window window = getWindow();
    window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN |
        WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN |
        WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS);
    // 锁屏的activity内部也要做相应的配置,让activity在锁屏时也能够显示,同时去掉系统锁屏。
    // 当然如果设置了系统锁屏密码,系统锁屏是没有办法去掉的
    // FLAG_DISMISS_KEYGUARD用于去掉系统锁屏页
    // FLAG_SHOW_WHEN_LOCKED使Activity在锁屏时仍然能够显示
    window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD |
        WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
    window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_HIDE_NAVIGATION);
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    window.getDecorView().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
            | View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_IMMERSIVE);
    }
}

3.2 权限问题

  • 不要忘记在Manifest中加入适当的权限:
<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>  

4.屏蔽物理或者手机返回键

4.1 为什么要这样处理

  • 当自定义锁屏页最终出现在手机上时,我们总希望它像系统锁屏页那样屹立不倒,所有的按键都不能触动它,只有通过划瓶或者指纹才能解锁,因此有必要对按键进行一定程度上的屏蔽。针对只有虚拟按键的手机,我们可以通过隐藏虚拟按键的方式部分解决这个问题,具体方法在后文会介绍。但是当用户在锁屏页底部滑动,隐藏后的虚拟按键还是会滑出,而且如果用户是物理按键的话就必须进行屏蔽了。

4.2 如何实现,实现逻辑代码

@Override
public void onBackPressed() {
    // 不做任何事,为了屏蔽back键
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
    int key = event.getKeyCode();
    switch (key) {
        case KeyEvent.KEYCODE_BACK: {
            return true;
        }
        case KeyEvent.KEYCODE_MENU:{
            return true;
        }
        default:
            break;
    }
    return super.onKeyDown(keyCode, event);
}

5.滑动屏幕解锁

5.1 滑动解锁原理

  • 当手指在屏幕上滑动时,拦截并处理滑动事件,使锁屏页面随着手指运动,当运动到达一定的阀值时,用户手指松开手指,锁屏页自动滑动到屏幕边界消失,如果没有达到运动阀值,就会自动滑动到起始位置,重新覆盖屏幕。
  • 对滑动的距离与阀值进行一个比较,此处的阀值为0.5*屏幕宽度,如果低于阀值,则移动到初始位置;如果高于阀值,以同样的方式移出屏幕右边界,然后将Activity干掉

5.2 滑动控件自定义

6.透明栏与沉浸模式

6.1 透明栏与沉浸模式的概念

  • 沉浸模式与透明栏是两个不同的概念,由于某些原因,国内一些开发或产品会把这两个概念混淆。
  • 6.1.1 沉浸模式 什么是沉浸模式?
  • 从4.4开始,Android 为 “setSystemUiVisibility()”方法提供了新的标记 “SYSTEM_UI_FLAG_IMMERSIVE”以及”SYSTEM_UI_FLAG_IMMERSIVE_STIKY”,就是我们所谈的沉浸模式,全称为 “Immersive Full-Screen Mode”,它可以使你的app隐藏状态栏和导航栏,实现真正意义上的全屏体验。
  • 之前 Android 也是有全屏模式的,主要通过”setSystemUiVisibility()”添加两个Flag,即”SYSTEM_UI_FLAG_FULLSCREEN”,”SYSTEM_UI_FLAG_HIDE_NAVIGATION”(仅适用于使用导航栏的设备,即虚拟按键)。
  • 这两个标记都存在一些问题,例如使用第一个标记的时候,除非 App 提供暂时退出全屏模式的功能(例如部分电子书软件中点击一次屏幕中央位置),用户是一直都没法看见状态栏的。这样,如果用户想去看看通知中心有什么通知,那就必须点击一次屏幕,显示状态栏,然后才能调出通知中心。
  • 而第二个标记的问题在于,Google 认为导航栏对于用户来说是十分重要的,所以只会短暂隐藏导航栏。一旦用户做其他操作,例如点击一次屏幕,导航栏就会马上被重新调出。这样的设定对于看图软件,视频软件等等没什么大问题,但是对于游戏之类用户需要经常点击屏幕的 App,那就几乎是悲剧了——这也是为什么你在 Android 4.4 之前找不到什么全屏模式会自动隐藏导航栏的应用。

6.2 如何实现,代码展示

  • 6.2.1 在案例中代码展示
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
    window.getDecorView().setSystemUiVisibility(
            // SYSTEM_UI_FLAG_LAYOUT_STABLE保持整个View稳定,使View不会因为SystemUI的变化而做layout
            View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
            // SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,开发者容易被其中的HIDE_NAVIGATION所迷惑,
            // 其实这个Flag没有隐藏导航栏的功能,只是控制导航栏浮在屏幕上层,不占据屏幕布局空间;
            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
            // SYSTEM_UI_FLAG_HIDE_NAVIGATION,才是能够隐藏导航栏的Flag;
            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
            // SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,由上面可知,也不能隐藏状态栏,只是使状态栏浮在屏幕上层。
            View.SYSTEM_UI_FLAG_FULLSCREEN |
            View.SYSTEM_UI_FLAG_IMMERSIVE);
}
  • 注意的是,这段代码除了需要加在Activity的OnCreate()方法中,也要加在重写的onWindowFocusChanged()方法中,在窗口获取焦点时再将Flag设置一遍,否则在部分手机上可能导致无法达到预想的效果。一般情况下没有问题,最后建议还是加上
@Override
public void onWindowFocusChanged(boolean hasFocus) {
    super.onWindowFocusChanged(hasFocus);
    if(hasFocus && getWindow()!=null){
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            getWindow().getDecorView().setSystemUiVisibility(
                    // SYSTEM_UI_FLAG_LAYOUT_STABLE保持整个View稳定,使View不会因为SystemUI的变化而做layout
                    View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
                            // SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,开发者容易被其中的HIDE_NAVIGATION所迷惑,
                            // 其实这个Flag没有隐藏导航栏的功能,只是控制导航栏浮在屏幕上层,不占据屏幕布局空间;
                            View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
                            View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
                            // SYSTEM_UI_FLAG_HIDE_NAVIGATION,才是能够隐藏导航栏的Flag;
                            View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
                            // SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,由上面可知,也不能隐藏状态栏,只是使状态栏浮在屏幕上层。
                            View.SYSTEM_UI_FLAG_FULLSCREEN |
                            View.SYSTEM_UI_FLAG_IMMERSIVE);
        }
    }
}

8.关于其他

8.1 版本更新情况

8.2 参考案例

8.3 个人博客


杨充
221 声望42 粉丝