天天看點

【騰訊Bugly幹貨分享】淺談Android自定義鎖屏頁的發車姿勢

鎖屏作為一種黑白屏時代就存在的手機功能,至今仍發揮着巨大作用,特别是觸屏時代的到來,鎖屏的功用被發揮到了極緻。多少人曾經在無聊的時候每隔幾分鐘劃開鎖屏再關上,孜孜不倦,其酸爽程度不亞于捏氣泡膜。

本文來自于騰訊bugly開發者社群,非經作者同意,請勿轉載,原文位址:http://dev.qq.com/topic/57875330c9da73584b025873

一、為什麼需要自定義鎖屏頁

鎖屏作為一種黑白屏時代就存在的手機功能,至今仍發揮着巨大作用,特别是觸屏時代的到來,鎖屏的功用被發揮到了極緻。多少人曾經在無聊的時候每隔幾分鐘劃開鎖屏再關上,孜孜不倦,其酸爽程度不亞于捏氣泡膜。确實,一款漂亮的鎖屏能為手機增色不少,但鎖屏存在的核心目的主要是三個:保護自己手機的隐私,防止誤操作,在不關閉系統軟體的情況下節省電量。

當下,各個款式的手機自帶的系統鎖屏完全能夠滿足這些需求,而且美觀程度非凡,那麼開發者為什麼仍然需要建構自定義鎖屏呢?讓我們試想一個場景,一位正在使用音樂播放器聽歌的美女使用者,在沒有播放器自定義鎖屏的情況下,切換一首歌需要幾步(參考自同類文章):

  1. 點亮手機螢幕
  2. 解開系統鎖屏
  3. 打開音樂播放器
  4. 切歌再熄滅螢幕

這時的她估計已經被廣場舞的歌曲騷擾了有10秒,續了10次命,這是我們程式員不願意看到的,是以有必要依靠我們靈活的雙手建構出自定義的音樂鎖屏頁,将切歌過程被壓縮為兩步:點亮螢幕和切歌,順便可以看看歌詞。如果再加個開啟和關閉自定義鎖屏的開關,就能完美解決使用者的痛點。

二、自定義鎖屏頁的基本原理

然而,要實作一個自定義鎖屏是一件繁瑣的事情,因為系統有100種方法讓這個非本地的鎖屏待不下去。但是,人類的智慧是無限的,程式員需要逆流而上。

Android系統實作自定義鎖屏頁的思路很簡單,即在App啟動時開啟一個service,在Service中時刻監聽系統SCREEN_OFF的廣播,當螢幕熄滅時,Service監聽到廣播,開啟一個鎖屏頁Activity在螢幕最上層顯示,該Activity建立的同時會去掉系統鎖屏(當然如果有密碼是禁不掉的)。示意圖如下:

道理很簡單,我們這裡需要讨論的是細節。

1. 廣播注冊

Service是普通的Service,在應用啟動時直接startService,與應用同一個程序即可。此外,SCREEN_OFF廣播監聽必須是動态注冊的,如果在AndroidManifest.xml中靜态注冊将無法接收到SCREEN_OFF廣播,這點在Android官方文檔中有明确說明,即需要通過如下代碼注冊:

IntentFilter mScreenOffFilter = new IntentFilter();
 mScreenOffFilter.addAction(Intent.ACTION_SCREEN_OFF);
 registerReceiver(mScreenOffReceiver, mScreenOffFilter);  
   對應的BroadcastReceiver定義如下:

 private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() {
     @SuppressWarnings("deprecation")
     @Override
     public void onReceive(Context context, Intent intent) {
         if (intent.getAction().equals(NOTIFY_SCREEN_OFF)) {
             Intent mLockIntent = new Intent(context, LockScreenActivity.class);
             mLockIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                     | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
             startActivity(mLockIntent);
         }
     }
 };
           

關于啟動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. Activity設定

鎖屏的activity内部也要做相應的配置,讓activity在鎖屏時也能夠顯示,同時去掉系統鎖屏。當然如果設定了系統鎖屏密碼,系統鎖屏是沒有辦法去掉的,這裡考慮沒有設定密碼的情況。

典型的去掉系統鎖屏頁的方法是使用KeyguardManager,具體代碼如下:

KeyguardManager mKeyguardManager = (KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE);
 KeyguardManager.KeyguardLock mKeyguardLock = mKeyguardManager.newKeyguardLock("CustomLockScreen");
 mKeyguardLock.disableKeyguard();
           

其中,KeyguardManager是鎖屏管理類,我們通過getSystemService()的方式擷取執行個體對象mKeyguardManager,調用該對象的newKeyguardLock()方法擷取KeyguardManager的内部類KeyguardLock的執行個體mKeyguardLock,該方法傳入的字元串參數用于辨別是誰隐藏了系統鎖屏,最後調用mKeyguardLock的disableKeyguard()方法可以取消系統鎖屏。

上述方法已經不推薦使用,可以使用更好的方法來替代。我們在自定義鎖屏Activity的onCreate()方法裡設定以下标志位就能完全實作相同的功能:

getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
 getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED);
           

FLAG_DISMISS_KEYGUARD用于去掉系統鎖屏頁,FLAG_SHOW_WHEN_LOCKED使Activity在鎖屏時仍然能夠顯示。當然,不要忘記在Manifest中加入适當的權限:

<uses-permission android:name="android.permission.DISABLE_KEYGUARD"/>  
           

3. 屏蔽按鍵

當自定義鎖屏頁最終出現在手機上時,我們總希望它像系統鎖屏頁那樣屹立不倒,所有的按鍵都不能觸動它,隻有通過劃瓶或者指紋才能解鎖,是以有必要對按鍵進行一定程度上的屏蔽。針對隻有虛拟按鍵的手機,我們可以通過隐藏虛拟按鍵的方式部分解決這個問題,具體方法在後文會介紹。但是當使用者在鎖屏頁底部滑動,隐藏後的虛拟按鍵還是會滑出,而且如果使用者是實體按鍵的話就必須進行屏蔽了。

Back鍵和Menu鍵可以通過重寫onKeyDown()方法進行屏蔽:

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;
         }
     }
     return super.onKeyDown(keyCode, event);
 }
           

Home鍵與Recent鍵(調出最近打開應用的按鍵)的點選事件是在framework層進行處理的,是以onKeyDown與dispatchKeyEvent都捕獲不到點選事件。關于這兩個按鍵的屏蔽方法,網上相關的資料有很多,有的用到了反射,有的通過改變Window的标志位和Type等,總的來說這些方法隻對部分android版本有效,有的則完全無法編譯通過。其實,這麼做的目的無非是為了實作一個純粹的鎖屏頁,但是這種做法有些畫蛇添足,容易造成鎖屏頁的異常崩潰,我們要滿足的是使用者在鎖屏頁的快捷操作,Home鍵和Recent鍵無關痛癢,完全可以不管,少一些套路,多一點真誠嘛。

4. 劃屏解鎖

做完以上幾步,當螢幕熄滅後,再打開螢幕就能夠看到我們的自定義鎖屏頁了,但是這時候,就算劃破手指也無法解鎖。是以,接下來要實作劃屏解鎖。

劃瓶解鎖的基本思路很簡單,當手指在螢幕上滑動時,攔截并處理滑動事件,使鎖屏頁面随着手指運動,當運動到達一定的閥值時,使用者手指松開手指,鎖屏頁自動滑動到螢幕邊界消失,如果沒有達到運動閥值,就會自動滑動到起始位置,重新覆寫螢幕。

為了将劃屏邏輯與頁面内容隔離開來,我們在鎖屏頁面布局中添加一個自定義的UnderView,這個UnderView填充整個螢幕,位于鎖屏内容View(将其引用稱之為mMoveView,并傳入到UnderView中)的下方,所有劃屏相關的事件都在這裡攔截并處理。

mMoveView是鎖屏頁的顯示内容,除了處理一些簡單的點選事件,其他非點選事件序列都由底層的UnderView進行處理。隻需要重寫UnderView的onTouchEvent方法就能夠實作:

Override
 public boolean onTouchEvent(MotionEvent event) {
     final int action = event.getAction();
     final float nx = event.getX();
     switch (action) {
     case MotionEvent.ACTION_DOWN:
         mStartX = nx;
         onAnimationEnd();
     case MotionEvent.ACTION_MOVE:
         handleMoveView(nx);
         break;
     case MotionEvent.ACTION_UP:
     case MotionEvent.ACTION_CANCEL:
         doTriggerEvent(nx);
         break;
     }
     return true;
 }
           

其中,mStartX記錄滑動操作起始的x坐标,handleMoveView方法控制mMoveView随手指的移動,doTriggerEvent處理手指離開後mMoveView的移動動畫。兩個方法的定義如下:

private void handleMoveView(float x) {
     float movex = x - mStartX;
     if (movex < 0)
         movex = 0;
     mMoveView.setTranslationX(movex);

     float mWidthFloat = (float) mWidth;//螢幕顯示寬度
     if(getBackground()!=null){
         getBackground().setAlpha((int) ((mWidthFloat - mMoveView.getTranslationX()) / mWidthFloat * 200));//初始透明度的值為200
     }
 }
           

在handleMoveView()中,首先計算目前觸點x坐标與初始x坐标mStartX的內插補點movex,然後調用mMoveView的setTranslationX方法移動。值得注意的是,目前setTranslationX方法隻能在Android 3.0以上版本使用,如果采用動畫相容庫nineoldandroid中ViewHelper類提供的setTranslation方法,則沒有這個問題。scrollTo與scrollBy也可以實作移動,但是隻是移動View的内容,并不能移動View本身。另外就是通過修改布局參數LayoutParams實作移動,雖然沒有版本的限制,用起來相對複雜。這裡我們采用setTranslationX,為了簡潔,也是為了能夠與後續使用的屬性動畫相統一。

此外,我們可以通過getBackground()擷取UnderView的背景,并根據已劃開螢幕占整個螢幕的百分比調用setAlpha方法改變背景的透明度,做出抽屜拉開時的光影變化效果。

private void doTriggerEvent(float x) {
     float movex = x - mStartX;
     if (movex > (mWidth * 0.4)) {
         moveMoveView(mWidth-mMoveView.getLeft(),true);//自動移動到螢幕右邊界之外,并finish掉

     } else {
         moveMoveView(-mMoveView.getLeft(),false);//自動移動回初始位置,重新覆寫
     }
 }
 private void moveMoveView(float to,boolean exit){
     ObjectAnimator animator = ObjectAnimator.ofFloat(mMoveView, "translationX", to);
     animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
         @Override
         public void onAnimationUpdate(ValueAnimator animation) {
             if(getBackground()!=null){
                 getBackground().setAlpha((int) (((float) mWidth - mMoveView.getTranslationX()) / (float) mWidth * 200));
             }
         }
     });//随移動動畫更新背景透明度
     animator.setDuration(250).start();

     if(exit){
         animator.addListener(new AnimatorListenerAdapter() {
             @Override
             public void onAnimationEnd(Animator animation) {
                 mainHandler.obtainMessage(LockScreenActivity.MSG_LAUNCH_HOME).sendToTarget();
                 super.onAnimationEnd(animation);
             }
         });
     }//監聽動畫結束,利用Handler通知Activity退出
 }
           

當手指離開螢幕,doTraiggerEvent方法會對滑動的距離與閥值進行一個比較,此處的閥值為0.4*螢幕寬度,如果低于閥值,則通過ObjectAnimator在0.25s将mMoveView移動到初始位置,同時在ObjectAnimator的AnimatorUpdateListener的onAnimationUpdate方法中更新背景透明度;如果低于閥值,以同樣的方式将mMoveView移出螢幕右邊界,然後将Activity幹掉,具體做法是為animator增加一個AnimatorListenerAdapter的監聽器,在該監聽器的onAnimationEnd方法中使用在Activity中定義的mHandler發送finish消息,完成解鎖,效果如下圖:

三、透明欄與沉浸模式

沉浸模式與透明欄是兩個不同的概念,由于某些原因,國内一些開發或産品會把這兩個概念混淆。不過沒關系,在接下來的内容我們會對這兩個概念進行詳細的解釋和區分,并應用這兩種不同的模式進一步完善已經初具模樣的鎖屏頁。

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 之前找不到什麼全屏模式會自動隐藏導航欄的應用。

Android 4.4 之後加入的Immersive Full-Screen Mode 允許使用者在應用全屏的情況下,通過在原有的狀态欄/導航欄區域内做向内滑動的手勢來實作短暫調出狀态欄和導航欄的操作,且不會影響應用的正常全屏,短暫調出的狀态欄和導航欄會呈半透明狀态,并且在一段時間内或者使用者與應用内元素進行互動的情況下自動隐藏,沉浸模式的四種狀态如下圖。(參考http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0616/3047.html)

狀态1代表沒有進入沉浸模式時頁面的狀态,仍然可以看到Status Bar和Navigation Bar;狀态2代表使用者第一次進入沉浸模式時,系統的提示彈窗,告訴使用者如何在沉浸模式下呼出Status Bar和Navigation Bar;狀态3代表沉浸模式,可以看到Status Bar和Navigation Bar都被隐藏;狀态4代表使用者在Sticky沉浸模式下呼出Status Bar和Navigation Bar,可以看到兩個Bar重新出現,但是過一段時間能夠自動隐藏。

一般來說,沉浸模式的标記與其他Full Screen相關的Flag搭配起來才能達到我們想要的效果,即通過沉浸模式标記規定狀态欄status bar和導航欄navigation bar顯示和隐藏的運轉邏輯,通過其他标簽設定狀态欄和導航欄顯示或隐藏,以及顯示或隐藏的樣子。這些常見的Flag及相應功能如下表:

如此多的标簽,看起來非常亂,但用起來卻非常簡單和明确,感興趣的開發者可以自由搭配來測試一下。下面,我們通過一個例子,将這些标簽應用于鎖屏頁,實作對Navigation Bar的自動隐藏,同時保留Status Bar。代碼非常簡單,在Activity的onCreate()方法中使用:

getWindow().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_IMMERSIVE_STICKY
     | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
 );
           

總共用到了5個Flag:SYSTEM_UI_FLAG_LAYOUT_STABLE保持整個View穩定,使View不會因為SystemUI的變化而做layout;SYSTEM_UI_FLAG_IMMERSIVE_STIKY,能夠在隐藏的bar被呼出時(比如從螢幕下邊緣開始向上做滑動手勢),使bar在無相關操作的情況下自動再次隐藏;對于SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION,開發者容易被其中的HIDE_NAVIGATION所迷惑,其實這個Flag沒有隐藏導航欄的功能,隻是控制導航欄浮在螢幕上層,不占據螢幕布局空間;SYSTEM_UI_FLAG_HIDE_NAVIGATION,才是能夠隐藏導航欄的Flag;SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN,由上面可知,也不能隐藏狀态欄,隻是使狀态欄浮在螢幕上層。

需要注意的是,這段代碼除了需要加在Activity的OnCreate()方法中,也要加在重寫的onWindowFocusChanged()方法中,在視窗擷取焦點時再将Flag設定一遍,否則可能導緻無法達到預想的效果。

Override
 public void onWindowFocusChanged(boolean hasFocus) {
     super.onWindowFocusChanged(hasFocus);
     if(hasFocus){
         getWindow().getDecorView().setSystemUiVisibility(
                 View.SYSTEM_UI_FLAG_LAYOUT_STABLE
                 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
                 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
                 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
                 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
         );
     }
 }
           

此外,有個部份要稍微留意一下,如果不希望界面的内容被上拉到狀态欄(Status bar)的話,要記得在界面(Layout)XML檔案中,在最外層Layout中将fitsSystemWindows屬性設定為true。如下:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:tools="http://schemas.android.com/tools"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:fitsSystemWindows="true">
     <!-- Content -->
 </RelativeLayout>
           

設定了前文的5個Flag之後,鎖屏頁效果圖如下:

手指在螢幕底端上劃,Navigation Bar會彈出,懸浮于鎖屏頁底部,随後自動消失。Status Bar也按照我們預期的那樣,懸浮在上方,沒有隐藏。

2. 透明欄

什麼是透明欄?Google 在 Android 4.4 的 API 描述頁面裡提到了“Translucent system UI styling”,即半透明化的系統UI風格。這個“半透明化”包括了狀态欄和通知欄,當開發者讓應用支援這個新特性的時候,狀态欄和導航欄可以單獨/同時變為漸變的半透明樣式,如下圖:

在 Android 5.0 之後引入了 Material Design,狀态欄和導航欄也玩出了更多花樣。現在除了原有的“半透明”模式以外,還有“全透明”以及“變色”模式,一種會完全隐藏背景,另一種可以取色作為背景顔色,多種樣式的透明欄如下圖(上圖為透明狀态欄,下圖為透明導航欄):

是以,透明欄隻是能夠改變狀态欄和導航欄的顔色,并不像沉浸模式那樣隐藏狀态欄和導航欄,兩者是有本質差別的。

對于Android 4.4以上5.0以下的版本,設定透明狀态欄的方式如下:

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT){
     Window window = getWindow();
     window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
 }
           

  對于Android 5.0及以上版本,設定透明狀态欄的方法如下:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
     Window window = getWindow();
     window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
     window.getDecorView()
             .setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
     window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
     window.setStatusBarColor(0);
 }
           

除了要清理掉4.4的FLAG_TRANSLUCENT_STATUS外,還要配合SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_STABLE,添加标志位FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,并調用setStatusBarColor設定狀态欄的顔色為透明。

在綜合運用了沉浸模式和透明欄之後,鎖屏頁效果如下:

四、指紋解鎖

到這裡,我們的鎖屏頁已經基本完工,完全能夠非常優雅地解決使用者的痛點,但是跟當下App自定義鎖屏頁的差別并不明顯。接下來對新型号手機普遍具備的指紋解鎖功能的考慮,則能夠為鎖屏頁增色不少。

1. 指紋識别無法解鎖自定義鎖屏頁的問題

持有指紋解鎖手機的使用者在使用App自定義鎖屏頁時會出現一種困惑,當你點亮螢幕,能夠看到自定義鎖屏頁,在使用指紋解鎖成功之後(部分機型指紋解鎖操作隻能在系統鎖屏頁進行),自定義鎖屏頁依然存在,你還是需要劃開自定義鎖屏頁,才能看到手機主界面。

解決這一問題的方案是一種取巧的方法,那就是在鎖屏頁的service中監聽ACTION_USER_PRESENT廣播。ACTION_USER_PRESENT廣播是系統鎖屏解鎖廣播,當系統鎖屏頁解鎖時就會觸發。如果在接收到這一廣播時,将自定義鎖屏頁finish掉,就能避免在指紋解鎖成功後自定義鎖屏頁仍然顯示的問題。但是細心的讀者會發現這種解法在邏輯上還存在問題,因為在使用者沒有設定鎖屏密碼的情況下,前文自定義鎖屏頁在onCreate()時設定的FLAG_DISMISS_KEYGUARD标志位能夠輕易解鎖系統的鎖屏頁,并觸發ACTION_USER_PRESENT廣播,此時自定義鎖屏頁的Service接收到這一廣播後,發finish廣播給自定義鎖屏頁,導緻自定義鎖屏頁剛create就finish掉了,永遠不可能出現。

  

是以,我們必須對場景進行區分,隻在有鎖屏密碼的情況下,才對接收到的ACTION_USER_PRESENT廣播進行處理,finish自定義鎖屏頁。即在BroadcastReceiver的onReceive()方法中加入如下代碼:

if(intent.getAction().equals(Intent.ACTION_USER_PRESENT)) {
     if (VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
         if (km.isKeyguardSecure()) {
             MLog.d(TAG, "KeyguardSecure!");
             Intent i = new Intent(NOTIFY_USER_PRESENT);
             context.sendBroadcast(i);
         }
     }
 }
           

這裡KeyguardManager對象km的isKeyguardSecure()方法就是用來判斷是否設定了鎖屏密碼。NOTIFY_USER_PRESENT是自定義廣播,用來通知鎖屏頁Activity調用finish方法。

這種做法是合理的,因為如果沒有設定鎖屏密碼,FLAG_DISMISS_KEYGUARD标志位解鎖系統鎖屏之後,到達上述代碼塊,isKeyguardSecure()傳回為false,不會導緻自定義鎖屏頁Activity的finish操作。而如果設定了鎖屏密碼,FLAG_DISMISS_KEYGUARD必然無法解鎖系統鎖屏,到達不了上述代碼塊,也不會finish。這樣就避免了自定義鎖屏頁剛建立出來就将自己finish掉的困境。另一方面,其他非FLAG_DISMISS_KEYGUARD方式觸發的解鎖,比如指紋解鎖,都會使Activity消失,滿足了需求。

2. 自定義鎖屏頁下指紋識别無法使用的問題

此外,有些手機型号,比如小米,在自定義鎖屏頁罩在系統鎖屏頁之上時(設定有鎖屏密碼),指紋解鎖是無效的,也就是必須要劃開自定義鎖屏頁,在系統鎖屏頁上才能進行指紋解鎖。為了改善這種體驗,我們可以在Activity中引入指紋解鎖API,識别指紋并解鎖,具體代碼如下:

private void startFingerPrintListening() {
     if (!isFingerprintAuthAvailable()) {
         return;
     } else {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
             if (checkSelfPermission(Manifest.permission.USE_FINGERPRINT) == PackageManager.PERMISSION_GRANTED) {
                 mFingerprintManager.authenticate(null, mCancellationSignal, 0, new FingerprintManager.AuthenticationCallback() {
                     @Override
                     public void onAuthenticationError(int errorCode, CharSequence errString) {
                         super.onAuthenticationError(errorCode, errString);
                     }

                     @Override
                     public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                         super.onAuthenticationSucceeded(result);
                         finish();
                     }

                     @Override
                     public void onAuthenticationFailed() {
                         super.onAuthenticationFailed();
                     }
                 }, null);
                 return;
             }
         }
     }
 }  
 public boolean isFingerprintAuthAvailable() {
     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
         mKeyguardManager = (KeyguardManager) getSystemService(Activity.KEYGUARD_SERVICE);
         if(!mKeyguardManager.isKeyguardSecure()){
             return false;
         }
         if (checkSelfPermission(Manifest.permission.USE_FINGERPRINT) == PackageManager.PERMISSION_GRANTED) {

             mFingerprintManager = (FingerprintManager) getSystemService(Activity.FINGERPRINT_SERVICE);
             mCancellationSignal = new CancellationSignal();

             return  mFingerprintManager.isHardwareDetected()&&mFingerprintManager.hasEnrolledFingerprints();
         }else{
             return false;
         }
     }else{
         return false;
     }
 }
           

當然,不要忘記在Manifest中加入适當的權限:

<uses-permission android:name="android.permission.USE_FINGERPRINT"/>  
           

在調用指紋識别功能之前,我們需要判斷指紋識别功能是否可用,以及APP是否有相應的權限。這一過程展現在isFingerprintAuthAvailable()中,第一步是擷取KeyguardManager對象,調用isKeyguardSecure()判斷是否設定有鎖屏密碼,如果有,則需進一步判斷。checkSelfPermission用來判斷APP是否有指紋識别的權限(SDK 23要求),如果有則擷取FingerprintManager對象,調用該對象的isHardwareDetected()方法判斷指紋識别硬體是否可用,調用hasEnrolledFingerprints()判斷是否有事先錄入好的指紋,隻有以上條件都滿足,接下來才能調用指紋識别功能。

指紋識别的調用展現在startFingerPrintListening()方法中,主要就是調用FingerprintManager的方法

authenticate(FingerprintManager.CryptoObject crypto, 
                       CancellationSignal cancel, 
                       int flags, 
                       FingerprintManager.AuthenticationCallback callback, 
                       Handler handler)
           

其中,crypto參數代表Android6.0中crypto objects的wrapper class,可以通過該對象使authenticate過程更加安全,也可以不使用,這裡我們将其設為null;cancel用來取消anthenticate(),我們new出一個對象傳入就可以;flags是标志位,設定為0;callback為指紋識别回調,包含指紋識别的核心方法:onAuthenticationError()是指紋比對連續失敗後的回調(幾十秒後才能繼續比對),onAuthenticationSucceeded()是指紋比對成功的回調,onAuthenticationFailed()是指紋比對失敗時的回調。我們在這幾個方法中做相應的處理即可,在onAuthenticationSucceeded()方法中調用finish(),就能夠在指紋識别成功後關閉Activity。

五、總結

通過以上内容的分享,本鵝希望能夠對大家的開發有所幫助,如果内容有問題,也希望大家指點。綜上所述,在Android上實作自定義鎖屏頁并不是一件複雜的事情,關鍵是對一些技術點的把握要比較清楚。Service中啟動Activity的正确方法,廣播靜态注冊與動态注冊的差别,touch事件的分發傳播機制,透明欄與沉浸模式的綜合運用,以及指紋識别新技術的應用,都有很多值得推敲的地方。筆者當初實作自定義鎖屏頁時,沒有太多思考,有時照搬前人的做法,有時各種flag随便添加,有時新舊API混淆,雖然實作了需求,但是代碼不夠簡潔,可讀性也差。是以,在今後的開發過程中,除了要快速實作需求,還要在随後的維護中,多多思考和研究,使代碼能夠達到“少一行不行,多一行難受”的境界。

更多精彩内容歡迎關注bugly的微信公衆賬号:

騰訊 Bugly是一款專為移動開發者打造的品質監控工具,幫助開發者快速,便捷的定位線上應用崩潰的情況以及解決方案。智能合并功能幫助開發同學把每天上報的數千條 Crash 根據根因合并分類,每日日報會列出影響使用者數最多的崩潰,精準定位功能幫助開發同學定位到出問題的代碼行,實時上報可以在釋出後快速的了解應用的品質情況,适配最新的 iOS, Android 官方作業系統,鵝廠的工程師都在使用,快來加入我們吧!