1. 程式人生 > >鎖屏頁面實現及原理深入分析

鎖屏頁面實現及原理深入分析

目錄介紹

  • 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 個人部落格

好訊息

  • 部落格筆記大彙總【16年3月到至今】,包括Java基礎及深入知識點,Android技術部落格,Python學習筆記等等,還包括平時開發中遇到的bug彙總,當然也在工作之餘收集了大量的面試題,長期更新維護並且修正,持續完善……開源的檔案是markdown格式的!同時也開源了生活部落格,從12年起,積累共計47篇[近20萬字],轉載請註明出處,謝謝!
  • 如果覺得好,可以star一下,謝謝!當然也歡迎提出建議,萬事起於忽微,量變引起質變!

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 滑動控制元件自定義

  • 具體可以看程式碼:專案地址https://github.com/yangchong211/YCAudioPlayer
  • 可以直接看weight——other——SlideFinishLayout/SlitherFinishLayout類

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 個人部落格

相關推薦

頁面實現原理深入分析

目錄介紹 1.類似酷狗等鎖屏頁面實現步驟 1.1 什麼是鎖屏聯動媒體播放器 1.2 如何實現鎖屏頁面 1.3 關於自定義鎖屏頁面左右滑動的控制元件 1.4 注意要點分析 1.5 具體完整程式碼的案例 1.6 效果圖展示案例 2.自定義鎖屏頁的基本原理 2.1

頁面回到頂部的實現原理

轉自:http://www.mojidong.com/html/css/js/2013/03/03/page-top-to-achieve/ 現在很多網頁都有一個回到頂部的功能,當你拖動滾動條往下拉到一定程度就會在右下方顯示一個回到頂部的按鈕,點選則可以立即回到頁面頂部,非常貼心的設計。 今天打算自己做一個

ShiroFilterFactoryBean原始碼阻截原理深入分析

Shiro提供了與Web整合的支援,其通過一個ShiroFilter入口來攔截需要安全控制的URL,然後進行相應的控制,ShiroFilter類似於如Strut2/SpringMVC這種web框架的前端控制器,其是安全控制的入口點,其負責讀取配置(如ini配置檔案),然後判斷URL是否需要登入/許可權等工

ShiroFilterFactoryBean原始碼攔截原理深入分析

本篇文章篇幅比較長,但是細看下去相信對學習Shiro應該會有幫助。好了,閒話不多說,直接進入正題: Shiro提供了與Web整合的支援,其通過一個ShiroFilter入口來攔截需要安全控制的URL,然後進行相應的控制,ShiroFilter類似於如Strut

相環倍頻原理簡要分析

信號 cnblogs clas 振蕩器 特性 轉換 分享 檢測 itl 以前學STM32的時候就知道了倍頻這個概念。開發板上外接8M晶振,但是STM32主頻卻能跑72M,這離不開鎖相環(PLL)的作用。之後在使用FPGA的時候,直接有PLL這個IP核提供給我們使

Fork-Join 原理深入分析(二)

框架 ryu 循環 app bject ber setname 索引 skip ??本文是將 Fork-Join 復雜且較為龐大的框架分成5個小點來分析 Fork-Join 框架的實現原理,一個個點地理解透 Fork-Join 的核心原理。 1. Frok-Join 框架的

Android查缺補漏(線程篇)-- AsyncTask的使用原理詳細分析

catch 返回 rri 理解 ams tee ive lean keyword 本文作者:CodingBlock 文章鏈接:http://www.cnblogs.com/codingblock/p/8515304.html 一、AsyncTask的使用 AsyncT

探究Entity Framework如何在多個倉儲層實例之間實現工作單元的實現原理

事務日誌 方法 tran action opera and 底層 下載 none 前言   1、本文的前提條件:EF上下文是線程唯一,EF版本6.1.3。   2、網上已有相關API的詳細介紹,本文更多的是作為我自己的個人學習研究記錄。 疑問   用反編譯工具翻開D

【Unity Shader】(五) ------ 透明效果之半透明效果的實現原理

pic sele 不同的 %20 分享圖片 渲染 select fall 就是 筆者使用的是 Unity 2018.2.0f2 + VS2017,建議讀者使用與 Unity 2018 相近的版本,避免一些因為版本不一致而出現的問題 【Unity Shader學習筆記

java動態代理實現原理詳細分析(【轉載】By--- Gonjan )

【轉載】By---    Gonjan    關於Java中的動態代理,我們首先需要了解的是一種常用的設計模式--代理模式,而對於代理,根據建立代理類的時間點,又可以分為靜態代理和動態代理。  一、代理模式  

java動態代理實現原理詳細分析(【轉載】By--- Gonjan )

sleep class 實施 div prot stack 註意 san 由於 【轉載】By--- Gonjan 關於Java中的動態代理,我們首先需要了解的是一種常用的設計模式--代理模式,而對於代理,根據創建代理類的時間點,又可以分為靜態代理和動態代理。

京東架構師解析京東購物車的Java架構實現原理

今天來寫一下關於購物車的東西, 這裡首先丟擲四個問題: 1)使用者沒登陸使用者名稱和密碼,新增商品, 關閉瀏覽器再開啟後 不登入使用者名稱和密碼 問:購物車商品還在嗎? 2)使用者登陸了使用者名稱密碼,新增商品,關閉瀏覽器再開啟後 不登入使用者名稱和密碼 問:購物車商

CocosCreator | 2048使用Typescript實現原理

檢視所有程式碼請點選 2048-typescript-cocoscreator 先放上游戲體驗的連結  Saber2pr/2048-typescript-cocoscreator 演算法來自2048 遊戲實現原理 演算法看連結裡就好,裡面提供了最最核心的數學

斐波那契數列的遞迴與迴圈實現複雜度分析

一、斐波那契數列的定義: 二 、遞迴實現: 經典例題(杭電2041):  AC程式碼: #include <iostream> using namespace std; int f[41]; int main() { int num,m;

Vue公共廣播/父子元件廣播 $emit、$on的實現原理解答

vm.$emit( event, […args] ) 引數: {string} event [...args] 觸發當前例項上的事件。附加引數都會傳給監聽器回撥 m.$on( event, callback )

java動態代理實現原理詳細分析

generator result title super java args 設計 需要 edt 關於Java中的動態代理,我們首先需要了解的是一種常用的設計模式--代理模式,而對於代理,根據創建代理類的時間點,又可以分為靜態代理和動態代理。 一、代理模式 代理模式

OpenStack DVR 原理深入分析

一、DVR實驗環境 一套vlan模式的openstack環境,interface_driver使用openvswitch,並在計算節點新增br-ex,用於計算節點虛擬機器出外網。 二、DVR配置 1、控制節點 a、編輯/etc/neutron/neutron.conf[DEFAULT]router_d

Java設計模式——策略模式實現原理

簡介 定義 策略模式:將可變的部分從程式中抽象分離成演算法介面,在該介面下分別封裝一系列演算法實現。並使他們可以相互替換,從而導致客戶端程式獨立於演算法的改變。 策略模式中的設計原則:變化的抽象成介面;面向介面程式設計而不是面向實現程式設計。 原理 將有共性的行為抽象為一個

Dijkstra演算法的實現原理

演算法用處與基本思想 Dijkstra演算法的作用是求指定點到圖中任何一點的最短路徑。 其基本思路與BFS(深度優先搜尋)有些類似,但不同的是BFS用的是一般佇列,也就是嚴格的先進先出。而Dijkstra演算法用到的則是優先佇列。 什麼是優先佇列 優

Java設計模式——代理模式實現原理

簡介 Java程式設計的目標是實現現實不能完成的,優化現實能夠完成的,是一種虛擬技術。生活中的方方面面都可以虛擬到程式碼中。代理模式所講的就是現實生活中的這麼一個概念:中介。 代理模式的定義:給某一個物件提供一個代理,並由代理物件控制對原物件的引用。 代理模式包含如下角色: