為 TV 開發(fā)的 App,你說要運行在手機上?
一、前言
Android 智能電視,不知道你接觸過沒有?近兩年生產(chǎn)的電視,基本上都屬于智能電視,而因為 Android 的開放性,這些電視很大一部分都是搭載的 Android 系統(tǒng)。
而除了 Android 智能電視之外,還有一些智能盒子,例如:小米盒子、天貓魔盒等,其實都是屬于 Android 陣營的,接上一臺顯示器,就可以當(dāng)一個智能電視使用。
在國內(nèi)的環(huán)境下,開發(fā) TV App 其實并沒有遵循標(biāo)準(zhǔn)的 Google TV 的開發(fā)規(guī)范,而是把它當(dāng)成一個普通的橫屏 Android App 來開發(fā)??墒窃谶@個過程中,是需要額外處理一些手機和電視的差異的,例如:焦點的控制、選中態(tài)的控制、屏幕的適配等等。
如果這些適配都已經(jīng)做的非常好了的話,是可以在 Android 手機上,不需要做任何改動和配置,就***的運行一個原本為 Android TV 而開發(fā)的 App 的。
而在某些場景下,你可能需要對你原本想為 TV 開發(fā)的 App,做一些手機上的適配,讓它在運行在手機上的時候,呈現(xiàn)出另外的 UI 效果或者執(zhí)行分支的邏輯。
舉個比較實際的例子:簡單的微信登錄功能,TV App 來實現(xiàn)這個功能,一般是展示一個登錄二維碼,讓用戶通過手機掃碼登錄,但是如果這個 App 運行在手機上的話,你可能需要的是一個按鈕,點擊吊起微信去登錄。
你別問為什么用戶要在手機上安裝一個 TV App?為什么不能讓用戶截圖然后去微信里掃描截圖登錄?
需求下來了,就問你能不能實現(xiàn)?
那么,本文就來討論一下,如何在運行時,通過一些標(biāo)識來區(qū)分當(dāng)前 App 是運行在手機上還是 TV 上。
二、如何區(qū)分
既然這是一個運行時的區(qū)分,肯定是需要獲取一些設(shè)備上的差異值,來判定當(dāng)前的運行環(huán)境。
那么首先提個問題給自己,手機和 TV 到底存在哪些差異?
手機和電視的差異性:
- 屏幕物理尺寸不同。
- 布局尺寸不同。
- SIM 卡的支持不同。
- 電源接入的方式不同。
- 系統(tǒng)參數(shù)不同。
差不多就這些差異了,接下來我們進行詳細(xì)分析。
1、屏幕物理尺寸
手機和電視的屏幕物理尺寸是完全不一樣的,但是我們也不能完全使用買電視的時候介紹的 Xx寸 來區(qū)分屏幕物理尺寸。實際上完全可以將 Android TV 當(dāng)成一個大號的平板。
這里以一個電視英寸數(shù)的計算公式,計算屏幕對角線的長度,來做一個參考的數(shù)值。
- /**
- * 檢查當(dāng)前屏幕的物理尺寸
- * 小于 6.4 人為是手機,否則人為是電視
- *
- * @return true 手機,false TV
- */
- private static boolean checkScreenIsPhone(Context ctx) {
- WindowManager wm = (WindowManager) ctx.getSystemService(Context.WINDOW_SERVICE);
- Display display = wm.getDefaultDisplay();
- DisplayMetrics dm = new DisplayMetrics();
- display.getMetrics(dm);
- double x = Math.pow(dm.widthPixels / dm.xdpi, 2);
- double y = Math.pow(dm.heightPixels / dm.ydpi, 2);
- // 屏幕尺寸
- double screenInches = Math.sqrt(x + y);
- return screenInches < 6.5;
- }
對于智能電視而言,我想最小應(yīng)該都在 32 英寸,而這里的 6.4英寸以下,主要是基于手機的一個參數(shù)判斷。
不過手機的屏幕尺寸越做越大,各大廠商現(xiàn)在也都在上線全面屏的產(chǎn)品,隨手找了小米 Mix2 的參數(shù),尺寸為 5.99 英寸,霸么就這個 6.4 英寸的判斷條件,在現(xiàn)階段來看是合理的。
2、布局尺寸
既然屏幕的尺寸有差異,那么從不同的布局中獲取布局文件也是不一樣的,可以通過 screenLayout 參數(shù)來區(qū)分出當(dāng)前運行環(huán)境下***那一套。
規(guī)則如下:
截圖來自官方文檔,有興趣的可以通篇閱讀一下。
https://developer.android.com/guide/practices/screens_support.html?hl=zh-cn
而代碼如下:
- /**
- * 檢查當(dāng)前設(shè)備的局部尺寸
- * 如果是 SIZE_LARGE 就人為是大屏幕的
- */
- private static boolean checkScreenLayoutIsPhone(Context ctx) {
- return (ctx.getResources().getConfiguration().screenLayout
- & Configuration.SCREENLAYOUT_LAYOUTDIR_MASK)
- <= Configuration.SCREENLAYOUT_SIZE_LARGE;
- }
3、SIM 支持的模式
對于電視而言,就現(xiàn)在所了解到的,還沒有一款智能電視或者智能盒子,是可以插 SIM 卡的,所以判斷 SIM 支持的模式,基本上就可以區(qū)分出電視還是手機了。
SIM 卡支持的模式可以使用 TelephonyManager 來獲取當(dāng)前的狀態(tài)。
- /**
- * 檢查 SIM 卡的狀態(tài),如果沒有檢查到,認(rèn)為是電視
- *
- * @param ctx
- * @return
- */
- private static boolean checkTelephonyIsPhone(Context ctx) {
- TelephonyManager telecomManager = (TelephonyManager) ctx.getSystemService(Context.TELEPHONY_SERVICE);
- return telecomManager.getPhoneType() != TelephonyManager.PHONE_TYPE_NONE;
- }
可以看到 getPhoneType() 可以獲取當(dāng)前設(shè)備支持的 Radio 的模式。
- /** No phone radio. */
- public static final int PHONE_TYPE_NONE = PhoneConstants.PHONE_TYPE_NONE;
- /** Phone radio is GSM. */
- public static final int PHONE_TYPE_GSM = PhoneConstants.PHONE_TYPE_GSM;
- /** Phone radio is CDMA. */
- public static final int PHONE_TYPE_CDMA = PhoneConstants.PHONE_TYPE_CDMA;
- /** Phone is via SIP. */
- public static final int PHONE_TYPE_SIP = PhoneConstants.PHONE_TYPE_SIP;
一般而言,識別不到 SIM 的模式,就可以認(rèn)為是一款不支持 SIM 插卡的設(shè)備了。
4、電源的接入方式
對于電視的電源,有什么特點?
- 永遠(yuǎn)沒有耗電的變動,獲取到的電量永遠(yuǎn)是滿的。
- 電源接入的方式,使用 AC 交流電,而非 USB(充電) 或者電池。
獲取當(dāng)前電源和充電的接入方式,沒什么好說的,基本上依據(jù)這兩個條件,就可以區(qū)分出當(dāng)前到底是電視還是手機/平板了。
- /**
- * 檢查當(dāng)前電源的接入狀態(tài),電視一定是 AC 交流電
- *
- * @param ctx
- * @return
- */
- private static boolean checkBatteryIsPhone(Context ctx) {
- IntentFilter filter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
- Intent batteryStatus = ctx.registerReceiver(null, filter);
- // 當(dāng)前電池的狀態(tài)
- int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
- boolean isChanging = status == BatteryManager.BATTERY_STATUS_FULL;
- // 當(dāng)前充電的狀態(tài)
- int chargePlug = batteryStatus.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
- boolean acCharge = chargePlug == BatteryManager.BATTERY_PLUGGED_AC;
- // 電視的狀態(tài) 當(dāng)前點亮一定是滿的 兵器是 AC 交流電接入 才認(rèn)為是電視
- return !(isChanging && acCharge);
- }
這種方式去判斷也有缺陷,因為對于智能電視類的設(shè)備來說,還有一種設(shè)備容易被忽略,那就是投影,對于投影而言,有一些是會內(nèi)置電池的。
5、UI Mode
使用 UI Mode 的方式去判斷,就需要用到一個系統(tǒng)服務(wù) UIModeManager,它和一般的系統(tǒng)服務(wù)一樣,需要我們通過 Context.getSystemService() 方法獲取到。
這是一個官方給出的判斷方式,但是在國內(nèi)的環(huán)境下,并不可取。因為大部分廠商的智能電視,只是拿普通的 Android 系統(tǒng)改了改,其實并沒有遵循 Google TV 的標(biāo)準(zhǔn),所以這種方式在某些設(shè)備上可能會判斷出錯。
既然文檔介紹了,這里還是簡單介紹一下。沒什么好說的,直接上代碼就好了。
- /**
- * 檢查當(dāng)前設(shè)備的 UI MODE 來判定運行環(huán)境是 TV 還是 Phone
- */
- private static boolean checkUIModeIsPhone(Context ctx) {
- UiModeManager uiModeManager = (UiModeManager) ctx.getSystemService(Context.UI_MODE_SERVICE);
- return uiModeManager.getCurrentModeType() != Configuration.UI_MODE_TYPE_TELEVISION;
- }
有興趣可以直接閱讀完整的官方文檔中的相關(guān)部分。
https://developer.android.com/training/tv/start/hardware.html#runtime-check
三、設(shè)計原則
這里提供的幾種方法,其實都是猜測,都是有缺陷的。例如可能出現(xiàn)某些廠商的奇葩設(shè)備,出貨屏幕尺寸就是大的手機,或者有一些奇葩的電視或者盒子,就是可以支持插 SIM 卡,再或者有其實還有一些智能投影的設(shè)備,其實是內(nèi)帶電池的,是有電量的消耗的。
所以最穩(wěn)妥的方式,就是組合起來判斷。
- private static boolean sIsChecked = false;
- private static boolean sIsPhoneRunCache = false;
- public static boolean isPhoneRunning(Context ctx) {
- if (!sIsChecked) {
- sIsPhoneRunCache = checkScreenIsPhone(ctx)
- && checkScreenLayoutIsPhone(ctx)
- && checkTelephonyIsPhone(ctx)
- && checkBatteryIsPhone(ctx);
- sIsChecked = true;
- }
- return sIsPhoneRunCache;
- }
這里的判斷,是基于當(dāng)前 App 是主要發(fā)布在 Android 電視的應(yīng)用市場中,所以這里的判斷條件是對手機進行嚴(yán)格判斷,其他的都認(rèn)為是 Android TV 。這樣即便是誤判了,影響也不會太大。
【本文為51CTO專欄作者“張旸”的原創(chuàng)稿件,轉(zhuǎn)載請通過微信公眾號聯(lián)系作者獲取授權(quán)】