號外!號外!全網第一手Android P劉海屏適配大揭祕,唯一Kotlin演算法

keyu88888發表於2019-03-04

1. 背景

  Apple一直在引領設計的潮流,自從iPhone X釋出之後,劉海屏就一直存在爭議。不過不管你怎樣,Android也要躋入“劉海屏“的行列,Android P預覽版增加了很多亮點新特性,其中最接地氣、最直觀的改變當屬適配了類似於華為P20的頂部凹槽螢幕設計這一項,也從系統級支援頂部凹槽螢幕設計。

  很多廠商也在逐漸推出劉海屏設計的手機,在國內比較常見的就是OPPO R15和華為P20。

各廠家的劉海屏

1.1.介紹

  劉海屏的外觀,我想大家應該都有概念,不過不同廠商劉海屏的實現方式也有所不太,這一點需要先有個概念。
  就現在市場上的情況來說,會區分成兩類,一類是標準的 Android P Api,另外一類就是廠商在 Android P 以下的系統,做的特殊適配。

  例如:華為 P20 就是採用的 Android P 標準 Api 的方式,而 OPPO R15 就不一樣了,它有自己的適配 Api。

1.2.需要適配的情況

  Android P版本提供了統一的劉海屏方案和三方適配劉海屏方案:

  • 對於有狀態列的頁面,不會受到劉海屏特性的影響
  • 全屏顯示的頁面,系統劉海屏方案會對應用介面做下移處理,避開劉海區顯示
  • 已經適配Android P應用的全屏頁面可以通過谷歌提供的適配方案使用劉海區,真正做到全屏顯示。

2. 搭建環境

  在手邊沒有對應系統的裝置的時候,模擬器是一條不錯的路,最近 Google 也釋出了 Android P 的模擬器,還有一個辦法就是找一些支援真機雲測的平臺,例如華為的雲測平臺,也是一個解決方案,不過沒有本地模擬機這麼便捷。

2.1.華為終端開放實驗室

華為終端開放實驗室

2.2.本地模擬器

  選擇Android P的模擬器,有需要自己更新SDK,下載更新就好。

下載Android P的Rom

  劉海的凹槽區域,大部分是為了給攝像頭或者其他感測器留出區域。而在沒有劉海的裝置或者模擬器上,可以通過開發者選項裡的 “Simulate a display with a cutout”,開啟劉海屏的支援。

模擬劉海屏

  Android P模擬器自帶四種劉海屏的模式,分別為:“None”、“Narrow display cutout”、“Tall display cutout”和“Wide display cutout”。如下圖所示:

None
Narrow display cutout
Tall display cutout
Wide display cutout

3. 相容性影響

  上面也講清楚了,劉海屏的切割區域,都存在於狀態列上,所以在有狀態列的頁面上,是無需我們特殊處理的,系統會幫我們處理好。

  而對於全屏的頁面,就需要單獨的處理了。我這裡,簡單做了一個全屏頁面,每個橫條都是等寬的這樣能看到佈局上的差異。

關閉劉海屏
開啟劉海屏但不支援
適配劉海屏

  一個全屏的頁面,當沒有支援劉海屏又碰到了劉海屏,會導致 UI 下沉,如果這不是一個列表的佈局,底部的控制元件就會被遮擋。

  還有一些被劉海遮擋區域的效果,其實主要是依賴 UI 設計師來規避了,不要在可能出現劉海切割的地方,設計可操作的區域,影響使用者操作。

4. 官方劉海屏適配

  說那麼多,最終我們還是需要用技術的方式來適配劉海屏。Android P 的劉海屏,是有標準的Api來進行適配,而對於一些廠商自己的劉海屏裝置,例如:OPPO R15,就需要遵循它的開發文件進行單獨適配。

  Android P 為最新的劉海屏,提供了專門的Api來支援:DisplayCutout

4.1.開啟劉海屏

  在非劉海屏P版本手機可以開啟模擬劉海屏除錯的功能

  • 在開發人員選項螢幕中,向下滾動到繪圖部分,然後點選“模擬具有凹口的螢幕”設定項
  • 選擇劉海尺寸資訊

  如下圖所示:

開啟劉海屏
模擬劉海屏

4.2.適配劉海屏

  在劉海屏除錯開啟之後,瀏覽應用的所有頁面,測試所有遮擋問題,或者是下移導致的問題,對有問題的頁面進行佈局適配。適配方案如下:

  Google 提供的適配方案,可以設定是否在全屏模式下,使用劉海屏的區域。

// 谷歌官方提供的預設適配劉海屏
val attrib = window.attributes
attrib.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS

複製程式碼

  新的佈局屬性layoutInDisplayCutoutMode包含三種可選的模式,分別為:

// 視窗宣告使用劉海區域
public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS = 1;
// 預設情況下,全屏視窗不會使用到劉海區域,非全屏視窗可正常使用劉海區域
public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_DEFAULT = 0;
// 宣告不使用劉海區域
public static final int LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER = 2;
複製程式碼

4.3.劉海屏的高度

  在全屏模式下,我們的應用頁面背景充滿整個螢幕顯示,控制元件和文字等關鍵資訊佈局在狀態列以外的區域,以保證關鍵資訊不會出現遮擋(谷歌要求:凹槽高度和劉海高度要保持一致)。我們需要有辦法獲取到劉海屏凹槽的高度,才可以做到設計和佈局的時候,留出安全距離。

4.3.1. 獲取劉海尺寸資訊介面

  Android P已經預留出了標準的測量劉海屏凹槽的Api:DisplayCutout

DisplayCutout

  劉海屏的凹槽,就在螢幕的中間,所以只有**getSafeInsetTop()方法返回的結果,是我們需要的,而其他的getSafeInsetXXX()**方法,直接返回的是0。程式碼如下所示:

btn_always.postDelayed(Runnable {

    val displayCutout = btn_always.rootWindowInsets.displayCutout
    if (null == displayCutout) {
        Log.e(TAG, "displayCutout is empty")
        return@Runnable
    }
    Log.i(TAG, "SafeInsetBottom:" + displayCutout.safeInsetBottom);
    Log.i(TAG, "SafeInsetLeft:" + displayCutout.safeInsetLeft);
    Log.i(TAG, "SafeInsetRight:" + displayCutout.safeInsetRight);
    Log.i(TAG, "SafeInsetTop:" + displayCutout.safeInsetTop);


}, 100)

複製程式碼

  輸出結果為:

SafeInsetBottom:0
SafeInsetLeft:0
SafeInsetRight:0
SafeInsetTop:84
複製程式碼

4.3.2. 獲取系統狀態列高度介面

  獲取劉海屏的高度之後,我們還要獲取系統狀態列的高度,程式碼如下:

fun getStatusBarHeight(context: Context): Int {
    
    var result = 0

    val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
    if (0 < resourceId) {
        result = context.resources.getDimensionPixelOffset(resourceId)
    }

    return result
}
複製程式碼

5. 其他廠商劉海屏適配

  像華為、Oppo和Vivo這樣的廠商,實現劉海屏的方式,也並不是按照 Android P的標準做的,它完全是自己修改了劉海屏的實現方式。不過他們是會提供完備的適配文件,這就需要我們直接閱讀他們提供的開發文件來進行適配。各個廠商的劉海屏適配參考如下:

廠商 介紹
華為 https://mini.eastday.com/bdmip/180411011257629.html
Oppo https://open.oppomobile.com/wiki/doc#id=10159
Vivo https://dev.vivo.com.cn/doc/document/info?id=103

5.1.華為

  華為提供了劉海屏Api,可以通過反射的方式呼叫。

5.1.1. 判斷是否劉海屏介面

  程式碼如下:

/**
 * 判斷是否是華為劉海屏
 * @param context 上下文物件
 * @return true:是劉海屏;false:非劉海屏
 */
fun hasNotchInScreen(context: Context): Boolean {
    var ret = false
    try {
        val cl = context.getClassLoader()
        val HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
        val method = HwNotchSizeUtil.getMethod("hasNotchInScreen")
        ret = method.invoke(HwNotchSizeUtil) as Boolean

    } catch (e: ClassNotFoundException) {
        Log.e(TAG, "hasNotchInScreen ClassNotFoundException")
    } catch (e: NoSuchMethodException) {
        Log.e(TAG, "hasNotchInScreen NoSuchMethodException")
    } catch (e: Exception) {
        Log.e(TAG, "hasNotchInScreen Exception")
    } finally {
        return ret
    }
}

複製程式碼

5.1.2. 獲取劉海尺寸資訊介面

  程式碼如下:

/**
 * 獲取華為劉海的高寬
 * @param context 上下文物件
 * @return  [0]值為劉海寬度int;[1]值為劉海高度
 */
fun getNotchSize(context: Context): IntArray {
    var ret = intArrayOf(0, 0)
    try {
        val cl = context.classLoader
        val HwNotchSizeUtil = cl.loadClass("com.huawei.android.util.HwNotchSizeUtil")
        val method = HwNotchSizeUtil.getMethod("getNotchSize")
        ret = method.invoke(HwNotchSizeUtil) as IntArray
    } catch (e: ClassNotFoundException) {
        Log.e(TAG, "getNotchSize ClassNotFoundException")
    } catch (e: NoSuchMethodException) {
        Log.e(TAG, "getNotchSize NoSuchMethodException")
    } catch (e: Exception) {
        Log.e(TAG, "getNotchSize Exception")
    } finally {
        return ret
    }
}

複製程式碼

5.1.3. 應用頁面設定使用劉海區顯示

  給window新增華為新增的FLAG_NOTCH_SUPPORT方式,程式碼如下所示:

/**
 * 設定應用視窗在華為劉海屏手機使用挖孔區
 * @param window 應用頁面window物件
 */
fun setFullScreenWindowLayoutInDisplayCutout(window: Window?) {

    if (null == window) {
        return
    }

    val layoutParams: WindowManager.LayoutParams = window.attributes

    try {
        val layoutParamsExCls = Class.forName("com.huawei.android.view.LayoutParamsEx")
        val con = layoutParamsExCls.getConstructor(WindowManager.LayoutParams::class.java)
        val layoutParamsExObj = con.newInstance(layoutParams)
        val method = layoutParamsExCls.getMethod("addHwFlags", Int::class.javaPrimitiveType)
        method.invoke(layoutParamsExObj, FLAG_NOTCH_SUPPORT)
    } catch (e: ClassNotFoundException) {
        Log.e(TAG, "hw notch screen flag api error")
    } catch (e: NoSuchMethodException) {
        Log.e(TAG, "hw notch screen flag api error")
    } catch (e: IllegalAccessException) {
        Log.e(TAG, "hw notch screen flag api error")
    } catch (e: InstantiationException) {
        Log.e(TAG, "hw notch screen flag api error")
    } catch (e: InvocationTargetException) {
        Log.e(TAG, "hw notch screen flag api error")
    } catch (e: Exception) {
        Log.e(TAG, "other Exception")
    }
}

複製程式碼

5.2.Oppo

  對於Oppo而言,它劉海的高度是固定的,就是80px。

Oppo

  判斷當前裝置是否是劉海屏,也提供了對應的 Api,可以用以下方法獲取。程式碼如下所示:

/**
 * 判斷是否是Oppo劉海屏
 * @param context 上下文物件
 * @return true:是劉海屏;false:非劉海屏
 */
fun hasNotchInScreenAtOppo(context: Context): Boolean {
    return context.packageManager!!.hasSystemFeature("com.oppo.feature.screen.heteromorphism")
}
複製程式碼

  返回 true 為劉海屏,但是這種方法只能識別Oppo品牌所支援的劉海屏。

5.3.Vivo

  在Vivo系統中,增加了一個介面來判斷此裝置是否具有凹槽,我們可以使用發射的方式呼叫。程式碼如下所示:

/**
 * 判斷Voio是否有凹槽
 *
 * @param context 上下文物件
 * @return true表示具備此特徵,false表示沒有此特徵
 */
fun hasNotchInScreenAtVoio(context: Context): Boolean {
    var ret = false
    try {
        val cl = context.classLoader
        val FtFeature = cl.loadClass("android.util.FtFeature")
        val method = FtFeature.getMethod("isFeatureSupport", Int::class.javaPrimitiveType)
        ret = method.invoke(FtFeature, NOTCH_IN_SCREEN_VOIO) as Boolean

    } catch (e: ClassNotFoundException) {
        Log.e(TAG, "hasNotchInScreen ClassNotFoundException")
    } catch (e: NoSuchMethodException) {
        Log.e(TAG, "hasNotchInScreen NoSuchMethodException")
    } catch (e: Exception) {
        Log.e(TAG, "hasNotchInScreen Exception")
    } finally {
        return ret
    }
}
複製程式碼

6. 結語

  看完本篇文章,我想你對Android P的劉海屏也有一定的認識了,現在還不確定不同廠商會不會對其微調,所以你要是碰到什麼問題,歡迎一起研究學習,不妨在留言區留言討論。

相關文章