鎖屏作為一種黑白屏時代就存在的手機功能,至今仍發揮著巨大作用,特別是觸屏時代的到來,鎖屏的功用被髮揮到了極致。多少人曾經在無聊的時候每隔幾分鐘劃開鎖屏再關上,孜孜不倦,其酸爽程度不亞於捏氣泡膜。 ...
本文來自於騰訊bugly開發者社區,非經作者同意,請勿轉載,原文地址:http://dev.qq.com/topic/57875330c9da73584b025873
一、為什麼需要自定義鎖屏頁
鎖屏作為一種黑白屏時代就存在的手機功能,至今仍發揮著巨大作用,特別是觸屏時代的到來,鎖屏的功用被髮揮到了極致。多少人曾經在無聊的時候每隔幾分鐘劃開鎖屏再關上,孜孜不倦,其酸爽程度不亞於捏氣泡膜。確實,一款漂亮的鎖屏能為手機增色不少,但鎖屏存在的核心目的主要是三個:保護自己手機的隱私,防止誤操作,在不關閉系統軟體的情況下節省電量。
當下,各個款式的手機自帶的系統鎖屏完全能夠滿足這些需求,而且美觀程度非凡,那麼開發者為什麼仍然需要構建自定義鎖屏呢?讓我們試想一個場景,一位正在使用音樂播放器聽歌的美女用戶,在沒有播放器自定義鎖屏的情況下,切換一首歌需要幾步(參考自同類文章):
- 點亮手機屏幕
- 解開系統鎖屏
- 打開音樂播放器
- 切歌再熄滅屏幕
這時的她估計已經被廣場舞的歌曲騷擾了有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 官方操作系統,鵝廠的工程師都在使用,快來加入我們吧!