Android鎖屏實現與總結(網易云閱讀)
一、自定義鎖屏基本原理
二、重要步驟
1、廣播注冊
2、Activity設置
3、按鍵的屏蔽
4、滑屏解鎖
5、Event bus的使用
三、出現的問題
1、小米和魅族等手機鎖屏權限問題
2、透明欄與沉浸模式
3、手機適配
4、處理黑色閃屏
5、線控耳機
6、Android上的「安全音量」
一、自定義鎖屏基本原理
先上效果圖:
實現鎖屏的方式有多種(鎖屏應用、懸浮窗、普通Activity偽造鎖屏等等)。通過網絡查找資料與反編譯云音樂apk,本項目使用了國內比較主流并且被廣泛應用的Activity偽造鎖屏方式。
Activity實現自定義鎖屏頁的思路很簡單,即在聽書模式開啟時,啟動一個service,在service中監聽系統SCREEN_OFF的廣播。當屏幕熄滅時service監聽到廣播,開啟一個鎖屏頁Activity在屏幕最上層顯示,該Activity創建的同時會去掉系統的鎖屏(如果有密碼是禁不掉的)。示意圖如下:
二、重要步驟
1、廣播注冊
LockScreenService是普通的Service,在應用啟動聽書模式時候startService(ReadBookActivity),與應用同一個進程。
此外,SCREEN_OFF廣播監聽必須是動態注冊的,如果在AndroidManifest.xml中靜態注冊將無法接收到SCREEN_OFF廣播。
標志位FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS,是為了避免在最近使用程序列表出現Service所啟動的Activity。
啟動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棧中。
2、Activity設置
鎖屏的activity內部也要做相應的配置,讓activity在鎖屏時也能夠顯示,同時去掉系統鎖屏。當然如果設置了系統鎖屏密碼,系統鎖屏是沒有辦法去掉的,這里考慮沒有設置密碼的情況。我們在自定義鎖屏Activity的onCreate()方法里設定以下標志位就能完全實現相同的功能:
FLAG_DISMISS_KEYGUARD用于去掉系統鎖屏頁,FLAG_SHOW_WHEN_LOCKED使Activity在鎖屏時仍然能夠顯示。另外需要在Manifest中加入適當的權限:
3、按鍵的屏蔽
當自定義鎖屏頁最終出現在手機上時,我們希望它像系統鎖屏頁那樣屹立不倒,所有的按鍵都不能觸動它,只有通過劃屏或者指紋才能解鎖,因此有必要對按鍵進行一定程度上的屏蔽。針對只有虛擬按鍵的手機,我們可以通過隱藏虛擬按鍵的方式部分解決這個問題。但是當用戶在鎖屏頁底部滑動,隱藏后的虛擬按鍵還是會滑出,而且如果用戶是物理按鍵的話就必須進行屏蔽了。需要重寫Activity的onBackPressed()方法即可。
Home鍵 與Recent鍵的點擊事件是在framework層進行處理的,因此onKeyDown()與dispatchKeyEvent()都捕獲不到點擊事件。關于這兩個按鍵的屏蔽方法,網上相關的資料有很多,有的用到了反射,有的通過改變Window的標志位和Type等,總的來說這些方法只對部分android版本有效,有的則完全無法編譯通過。其實這么做的目的無非是為了實現一個純粹的鎖屏頁,但是這種做法容易造成鎖屏頁的異常崩潰,我們要滿足的是用戶在鎖屏頁的快捷操作,Home鍵和Recent鍵無關痛癢,基本可以不管。
4、滑屏解鎖
做完以上幾步,當屏幕熄滅后,再打開屏幕就能夠看到我們的自定義鎖屏頁了。接下來要實現劃屏解鎖。劃瓶解鎖的基本思路很簡單,當手指在屏幕上滑動時,攔截并處理滑動事件,使鎖屏頁面隨著手指運動,當運動到達一定的閾值時,用戶手指松開手指,鎖屏頁自動滑動到屏幕邊界消失,如果沒有達到運動閥值,就會自動滑動到起始位置,重新覆蓋屏幕。 為了將劃屏邏輯與頁面內容隔離開來,我們在鎖屏頁面布局中添加一個自定義的UnderView,這個UnderView填充整個屏幕,位于鎖屏內容View(將其引用稱之為mMoveView,并傳入到UnderView中)的下方,所有劃屏相關的事件都在這里攔截并處理。
mMoveView是鎖屏頁的顯示內容,除了處理一些簡單的點擊事件,其他非點擊事件序列都由底層的UnderView進行處理。只需要重寫UnderView的onTouchEvent方法就能夠實現
其中,mStartX記錄滑動操作起始的x坐標,handleMoveView方法控制mMoveView隨手指的移動,doTriggerEvent處理手指離開后mMoveView的移動動畫。兩個方法的定義如下:
在handleMoveView()中,首先計算當前觸點x坐標與初始x坐標mStartX的差值movex,然后調用mMoveView的setTranslationX方法移動。此外,我們可以通過getBackground()獲取UnderView的背景,并根據已劃開屏幕占整個屏幕的百分比調用setAlpha方法改變背景的透明度,做出抽屜拉開時的光影變化效果。
當手指離開屏幕,doTraiggerEvent方法會對滑動的距離與閥值進行一個比較,此處的閥值為0.4*屏幕寬度,如果低于閥值,則通過ObjectAnimator在0.25s將mMoveView移動到初始位置,同時在ObjectAnimator的AnimatorUpdateListener的onAnimationUpdate方法中更新背景透明度;如果低于閥值,以同樣的方式將mMoveView移出屏幕右邊界,然后將Activity干掉,具體做法是為animator增加一個AnimatorListenerAdapter的監聽器,在該監聽器的onAnimationEnd方法中使用在Activity中定義的mHandler發送finish消息,完成解鎖。
5、Event bus的使用
鎖屏Activity中的Buttun,可以通過EventBus遠程控制ReadBookActivity中的播放、暫停、下一首等操作。
二、出現的問題
1、小米和魅族等手機鎖屏權限問題
例如小米手機(miui 6.8.18開始)有鎖屏權限的問題,權限未開的情況下鎖屏會在系統鎖屏的下方。目前只能手動打開(個別應用在MIUI是默認打開)。
在設置中:
2、透明欄與沉浸模式
透明欄與沉浸模式總共用到了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設置一遍,否則可能導致無法達到預想的效果。
在Android 5.0之后狀態欄和導航欄也有更多的特點。除了原有的“半透明”模式以外,還有“全透明”以及“變色”模式,一種會完全隱藏背景,另一種可以取色作為背景顏色等。對于Android 4.4以上5.0以下的版本,設置透明狀態欄的方式如下:
對于Android 5.0及以上版本,設置透明狀態欄的方法如下:
除了要清理掉4.4的FLAG_TRANSLUCENT_STATUS外,還要配合SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN和SYSTEM_UI_FLAG_LAYOUT_STABLE,添加標志位FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS,并調用setStatusBarColor設置狀態欄的顏色為透明。
3、手機適配
由于Android手機屏幕高度差異比較大,所以有可能會存在要顯示的控件高度比屏幕還高的問題。所以需要通過代碼對布局中某些控件寬高進行等比例調整。
例如在本項目中通過固定音量鍵以上的高度和最下方滑動解鎖的位置,來動態等比例動態調整中間的專輯封面圖片。mMain.post(new Runnable()。
4、處理黑色閃屏
我們的鎖屏Activity在滑動”解鎖”之后,理論上是直接進入下面的界面,但有時如果下面不是launcher,而是一個app,有可能會閃一下黑屏,這個其實是底下activity的入場動畫導致的,某些Android版本會對頂部activity透明時處理有些奇怪,不能保證其他的應用不閃黑屏,但是對自己的的應用還是可以的,只需要在主體activity的style中加上
5、線控耳機
我們只要定義一個廣播接收者來接收到這個廣播。這個廣播的意圖是android.intent.action.MEDIA_BUTTON。注冊普通的廣播接收器有兩個常見的辦法,一種是在代碼中動態注冊,另一種是在項目Manifest里面注冊。但是這個廣播,要注冊兩遍、兩遍、兩遍,Manifest里一遍(常規辦法),代碼中一遍(借助多媒體服務注冊),否則沒有效果。
因為兩種注冊方式缺一不可,所以解除了一種,它的監聽作用也就失效了。
6、Android上的「安全音量」
當Android設備插上耳機,為了避免音量過高傷害用戶聽力,會觸發其“安全音量”(Safe Media Volume)機制,如果在未經用戶確認允許使用大音量時,且這時設置音量newIndex超過其推薦閾值,則這段代碼執行完你會發現毫無反應,播放的聲音依然不會很大。
解決問題的關鍵在于被忽略的最后一個參數flags。只要在設置音量后,復查一次當前值是否相當,如果比較小,則交由系統來顯示音量提示對話框。而此時因欲設定的值超過推薦值,一般會觸發音量過高警告,提示用戶用戶確認后即可設置成功。