Android QQ音樂/酷狗音樂鎖屏控制實現原理,酷狗鎖屏

yangxi_001發表於2016-01-04

我實現的效果

類似效果

混亂的鎖屏控制

Android自4.0版本, 也就是API level 14開始, 加入了鎖屏控制的功能, 相關的類是RemoteControlClient, 這個類在API level 21中被標記為deprecated, 被新的類MediaSession所替代. 我們的音樂App中最開始使用的是原生鎖屏控制API, 說實話這個API不好用, 遇到了一些小坑, 最要命的是不同品牌的手機, 鎖屏介面長的還不一樣, 就連我自己都沒見過原生4.0的鎖屏控制介面是什麼樣的. 國內的手機廠商都自以為自己的審美很強, 設計了千奇百怪的鎖屏控制介面, MIUI更奇怪, MIUI 6是在原生4.4.4的基礎上改的, 竟然有一段時間都沒有鎖屏控制介面, 後來更新才有. 而原生Android在5.0時, 將鎖屏和通知欄控制合併, 整個邏輯非常混亂. 我們還是決定像QQ音樂/酷狗音樂那樣, 自己做一個鎖屏控制頁面

題外話: 給RemoteControlClient設定封面時, 引數是一個Bitmap, 這個引數傳入後, 千萬不要在其他地方使用這個Bitmap, 也不要持有它的引用, 更不要自作聰明呼叫它的recycle方法.

實現思路

鎖屏應用: 大炮打蚊子

首先想到的因該是做一個鎖屏, 也就是使用Android的API, 做一個鎖屏應用, 和輸入法等應用一樣, 但這個方法成本很高. 國內的那些鎖屏應用, 首先要做的就是引導使用者設定鎖屏應用, 步驟相當繁雜, 只是為了一個播放控制就用一個鎖屏應用, 沒有哪個使用者會這麼有耐心.

懸浮窗: 黑魔法

得益於我國程式設計師的腦洞, 我們有了第二種思路: 懸浮窗. 
懸浮窗的一個比較嚴謹的名字叫系統警告視窗, 國內外的一些流氓廠商, 經常用懸浮窗彈一些廣告, 這個懸浮窗是浮在正常的app的上面的, 所以如果它不消失, 很可能你連正常使用手機都有問題. 
這是一個比較打擾使用者的東西, 而且也有一定的安全風險. MIUI的許可權管理預設是將懸浮窗關閉的, 而有道詞典的複製查詞功能, 就是用懸浮窗做的, 如果你沒給有道詞典開啟這個許可權, 複製查詞這個功能就廢了.

普通Activity偽造鎖屏

文章開頭的GIF圖片展示的效果, 就是用一個普通Activity做的. 
國內的app們, 最終都選擇了這條道路, 不知道他們是誰抄的誰, 第一個想到使用普通Activity偽造一個鎖屏的開發者, 我只能說非常有創造力.

監聽鎖屏事件

準確來說我們監聽的是螢幕熄滅事件, 關屏事件的Intent是Intent.ACTION_SCREEN_OFF, 不需要任何許可權就可以監聽, 但是必須使用程式碼註冊, 也就是說我們必須有一個Service在後臺監聽才行, 對音樂類app來說, 這不是問題, 音樂app本身就是使用Service來控制MediaPlayer的. 只需要在Service中註冊監聽Intent.ACTION_SCREEN_OFF就行. 監聽到這個事件, 我們就啟動一個Activity, 這就是我們的鎖屏Activity.

@Override
public void onReceive(Context context, Intent intent) {
    String action = intent.getAction();
    if (action.equals(Intent.ACTION_SCREEN_OFF)) {
        Intent lockscreen = new Intent(PlaybackService.this, LockScreenActivity.class);
        lockscreen.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        startActivity(lockscreen);
    }
}

注意我們在Service中啟動一個Activity, 需要加上Intent.FLAG_ACTIVITY_NEW_TASK這個flag才行.

透明背景

要想做到鎖屏那樣滑動解鎖, 比如像圖中的樣子, 我們除了要根據手勢移動View以外, 還要讓Activity的背景透明, 比如將theme設定成下面這樣.

<style name="LockScreenBase" parent="AppBaseTheme">
    <item name="android:windowIsTranslucent">true</item>
    <item name="android:windowBackground">@android:color/transparent</item>
    <item name="android:colorBackgroundCacheHint">@null</item>
    <item name="android:windowNoTitle">true</item>
    <item name="android:backgroundDimEnabled">false</item>
    <item name="android:windowAnimationStyle">@null</item>
    <item name="android:windowContentOverlay">@null</item>
</style>

這個style其實做了很多東西, 大家根據自己的需要可以刪減一部分, 比如狀態列透明, 不使用TitleBar之類的.

解鎖螢幕與顯示在鎖屏之上

在顯示我們的假鎖屏的時候, 我們應當幫使用者解鎖, 這樣我們才能冒充鎖屏, 而不會出現使用者”解鎖”兩次的情況, 但我們只能要求系統解鎖沒有密碼的鎖屏, 有密碼的情況下, 我們是不能解鎖螢幕的, 這時我們應該覆蓋在鎖屏介面上, 幸好, 在API level 5中就引入了兩個Flag,FLAG_DISMISS_KEYGUARDFLAG_SHOW_WHEN_LOCKED 
在鎖屏Activity的onCreate方法中給Activity加上兩個Flag

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

一定要兩個一起用, 否則效果不大好, 當時測試了好久, 後來看了一下QQ音樂的實現, 才發現兩個一起用效果才好, 否則會有一些奇怪的問題.

隱藏蹤跡與獨立的Task

作為一個鎖屏介面, 應當是獨立的, 也就是說, 我們這個Activity應當獨立於我們的App存在, 至少看起來是這樣. 從Android的角度來看, 我們app的主介面裡的所有Activity, 應當在一個Task裡, 而鎖屏Activity, 應當在一個獨立的Task裡, 因此我們需要給鎖屏Activity一個獨立的Task, 而且無論何時, 都只有一個鎖屏Activity例項存在. 
另外, Android有檢視近期任務的功能, 我們不希望鎖屏介面這個獨立的Task顯示在裡面, 所以鎖屏Activity不能顯示在近期任務中. 
說了這麼多, 要做很簡單, 只需要在Manifest裡面宣告Activity時加入幾個屬性即可

<activity android:excludeFromRecents="true"
    android:exported="false"
    android:launchMode="singleInstance"
    android:name=".view.lockscreen.LockScreenActivity"
    android:screenOrientation="portrait"
    android:taskAffinity="com.package.name.lockscreen"
    android:theme="@style/LockScreenTheme">
</activity>

上面的屬性中android:excludeFromRecents="true"讓鎖屏Activity不顯示在近期任務中,android:launchMode="singleInstance"android:taskAffinity="com.package.name.lockscreen"保證鎖屏Activity有一個單獨的Task, 且這個Task裡永遠只有它一個例項.

不響應Back按鍵

鎖屏介面當然不響應Back按鍵, 只需要重寫Activity的onBackPressed方法即可

@Override
public void onBackPressed() {
    // do nothing
}

對Home按鍵的處理

我們無法監聽Home按鍵, 但是可以改變因Home進入後臺時的處理, 比如在Manifest的activity宣告中加上

android:noHistory="true"

這樣如果使用者通過Home按鍵讓我們的應用進入後臺, 我們會讓這個activity銷燬, 就像我們被滑動關閉一樣. 
如果不加, 最好重寫Activity的onNewIntent來應對因Home進入後臺, 然後Service再次啟動鎖屏Activity的情況.

處理黑色閃屏

我們的鎖屏Activity在滑動”解鎖”之後, 理論上是直接進入下面的介面, 但有時如果下面不是launcher, 而是一個app, 有可能會閃一下黑屏, 這個其實是底下activity的入場動畫導致的, 某些Android版本會對頂部activity透明時處理有些奇怪, 我們不能保證其他的應用不閃黑屏, 但是對自己的的應用還是可以的, 只需要在我們的主體activity的style中加上

<item name="android:windowAnimationStyle">@null</item>

就不會有這種情況發生了, 但是這樣的話入場動畫也沒了, 總之如何取捨看大家了.

相關文章