為 TV 開(kāi)發(fā)的 App,你說(shuō)要運(yùn)行在手機(jī)上?
一、前言
Android 智能電視,不知道你接觸過(guò)沒(méi)有?近兩年生產(chǎn)的電視,基本上都屬于智能電視,而因?yàn)?Android 的開(kāi)放性,這些電視很大一部分都是搭載的 Android 系統(tǒng)。
而除了 Android 智能電視之外,還有一些智能盒子,例如:小米盒子、天貓魔盒等,其實(shí)都是屬于 Android 陣營(yíng)的,接上一臺(tái)顯示器,就可以當(dāng)一個(gè)智能電視使用。
在國(guó)內(nèi)的環(huán)境下,開(kāi)發(fā) TV App 其實(shí)并沒(méi)有遵循標(biāo)準(zhǔn)的 Google TV 的開(kāi)發(fā)規(guī)范,而是把它當(dāng)成一個(gè)普通的橫屏 Android App 來(lái)開(kāi)發(fā)。可是在這個(gè)過(guò)程中,是需要額外處理一些手機(jī)和電視的差異的,例如:焦點(diǎn)的控制、選中態(tài)的控制、屏幕的適配等等。
如果這些適配都已經(jīng)做的非常好了的話(huà),是可以在 Android 手機(jī)上,不需要做任何改動(dòng)和配置,就***的運(yùn)行一個(gè)原本為 Android TV 而開(kāi)發(fā)的 App 的。
而在某些場(chǎng)景下,你可能需要對(duì)你原本想為 TV 開(kāi)發(fā)的 App,做一些手機(jī)上的適配,讓它在運(yùn)行在手機(jī)上的時(shí)候,呈現(xiàn)出另外的 UI 效果或者執(zhí)行分支的邏輯。
舉個(gè)比較實(shí)際的例子:簡(jiǎn)單的微信登錄功能,TV App 來(lái)實(shí)現(xiàn)這個(gè)功能,一般是展示一個(gè)登錄二維碼,讓用戶(hù)通過(guò)手機(jī)掃碼登錄,但是如果這個(gè) App 運(yùn)行在手機(jī)上的話(huà),你可能需要的是一個(gè)按鈕,點(diǎn)擊吊起微信去登錄。
你別問(wèn)為什么用戶(hù)要在手機(jī)上安裝一個(gè) TV App?為什么不能讓用戶(hù)截圖然后去微信里掃描截圖登錄?
需求下來(lái)了,就問(wèn)你能不能實(shí)現(xiàn)?
那么,本文就來(lái)討論一下,如何在運(yùn)行時(shí),通過(guò)一些標(biāo)識(shí)來(lái)區(qū)分當(dāng)前 App 是運(yùn)行在手機(jī)上還是 TV 上。
二、如何區(qū)分
既然這是一個(gè)運(yùn)行時(shí)的區(qū)分,肯定是需要獲取一些設(shè)備上的差異值,來(lái)判定當(dāng)前的運(yùn)行環(huán)境。
那么首先提個(gè)問(wèn)題給自己,手機(jī)和 TV 到底存在哪些差異?
手機(jī)和電視的差異性:
- 屏幕物理尺寸不同。
- 布局尺寸不同。
- SIM 卡的支持不同。
- 電源接入的方式不同。
- 系統(tǒng)參數(shù)不同。
差不多就這些差異了,接下來(lái)我們進(jìn)行詳細(xì)分析。
1、屏幕物理尺寸
手機(jī)和電視的屏幕物理尺寸是完全不一樣的,但是我們也不能完全使用買(mǎi)電視的時(shí)候介紹的 Xx寸 來(lái)區(qū)分屏幕物理尺寸。實(shí)際上完全可以將 Android TV 當(dāng)成一個(gè)大號(hào)的平板。
這里以一個(gè)電視英寸數(shù)的計(jì)算公式,計(jì)算屏幕對(duì)角線(xiàn)的長(zhǎng)度,來(lái)做一個(gè)參考的數(shù)值。
- /**
- * 檢查當(dāng)前屏幕的物理尺寸
- * 小于 6.4 人為是手機(jī),否則人為是電視
- *
- * @return true 手機(jī),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;
- }
對(duì)于智能電視而言,我想最小應(yīng)該都在 32 英寸,而這里的 6.4英寸以下,主要是基于手機(jī)的一個(gè)參數(shù)判斷。
不過(guò)手機(jī)的屏幕尺寸越做越大,各大廠商現(xiàn)在也都在上線(xiàn)全面屏的產(chǎn)品,隨手找了小米 Mix2 的參數(shù),尺寸為 5.99 英寸,霸么就這個(gè) 6.4 英寸的判斷條件,在現(xiàn)階段來(lái)看是合理的。
2、布局尺寸
既然屏幕的尺寸有差異,那么從不同的布局中獲取布局文件也是不一樣的,可以通過(guò) screenLayout 參數(shù)來(lái)區(qū)分出當(dāng)前運(yùn)行環(huán)境下***那一套。
規(guī)則如下:
截圖來(lái)自官方文檔,有興趣的可以通篇閱讀一下。
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 支持的模式
對(duì)于電視而言,就現(xiàn)在所了解到的,還沒(méi)有一款智能電視或者智能盒子,是可以插 SIM 卡的,所以判斷 SIM 支持的模式,基本上就可以區(qū)分出電視還是手機(jī)了。
SIM 卡支持的模式可以使用 TelephonyManager 來(lái)獲取當(dāng)前的狀態(tài)。
- /**
- * 檢查 SIM 卡的狀態(tài),如果沒(mé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;
一般而言,識(shí)別不到 SIM 的模式,就可以認(rèn)為是一款不支持 SIM 插卡的設(shè)備了。
4、電源的接入方式
對(duì)于電視的電源,有什么特點(diǎn)?
- 永遠(yuǎn)沒(méi)有耗電的變動(dòng),獲取到的電量永遠(yuǎn)是滿(mǎn)的。
- 電源接入的方式,使用 AC 交流電,而非 USB(充電) 或者電池。
獲取當(dāng)前電源和充電的接入方式,沒(méi)什么好說(shuō)的,基本上依據(jù)這兩個(gè)條件,就可以區(qū)分出當(dāng)前到底是電視還是手機(jī)/平板了。
- /**
- * 檢查當(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)前點(diǎn)亮一定是滿(mǎn)的 兵器是 AC 交流電接入 才認(rèn)為是電視
- return !(isChanging && acCharge);
- }
這種方式去判斷也有缺陷,因?yàn)閷?duì)于智能電視類(lèi)的設(shè)備來(lái)說(shuō),還有一種設(shè)備容易被忽略,那就是投影,對(duì)于投影而言,有一些是會(huì)內(nèi)置電池的。
5、UI Mode
使用 UI Mode 的方式去判斷,就需要用到一個(gè)系統(tǒng)服務(wù) UIModeManager,它和一般的系統(tǒng)服務(wù)一樣,需要我們通過(guò) Context.getSystemService() 方法獲取到。
這是一個(gè)官方給出的判斷方式,但是在國(guó)內(nèi)的環(huán)境下,并不可取。因?yàn)榇蟛糠謴S商的智能電視,只是拿普通的 Android 系統(tǒng)改了改,其實(shí)并沒(méi)有遵循 Google TV 的標(biāo)準(zhǔn),所以這種方式在某些設(shè)備上可能會(huì)判斷出錯(cuò)。
既然文檔介紹了,這里還是簡(jiǎn)單介紹一下。沒(méi)什么好說(shuō)的,直接上代碼就好了。
- /**
- * 檢查當(dāng)前設(shè)備的 UI MODE 來(lái)判定運(yùn)行環(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è)計(jì)原則
這里提供的幾種方法,其實(shí)都是猜測(cè),都是有缺陷的。例如可能出現(xiàn)某些廠商的奇葩設(shè)備,出貨屏幕尺寸就是大的手機(jī),或者有一些奇葩的電視或者盒子,就是可以支持插 SIM 卡,再或者有其實(shí)還有一些智能投影的設(shè)備,其實(shí)是內(nèi)帶電池的,是有電量的消耗的。
所以最穩(wěn)妥的方式,就是組合起來(lái)判斷。
- 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)用市場(chǎng)中,所以這里的判斷條件是對(duì)手機(jī)進(jìn)行嚴(yán)格判斷,其他的都認(rèn)為是 Android TV 。這樣即便是誤判了,影響也不會(huì)太大。
【本文為51CTO專(zhuān)欄作者“張旸”的原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)通過(guò)微信公眾號(hào)聯(lián)系作者獲取授權(quán)】