【Android平板適配】手機/平板二合一應用一站式適配攻略

語言: CN / TW / HK

為啥要適配Android平板

Android平板使用者越來越多

平板領域其實早已變天了,不再是IOS一家獨大。2020年ipad國內市場份額已不足50%,今年更是不足30%。隨著華為在平板上的發力,其它國內廠商也看到了Android平板這塊的利潤,紛紛推出新款平板。

平板專區,流量扶持

華為、榮耀、小米等廠商會有平板專區,如果適配了平板並且稽核通過,將會獲得極大的曝光。(如筆者開發的Elfinbook易飛這款應用,因為適配比較完善,順利進入小米平板專區,且最近兩個月均排在前三位,著實獲取了一把流量)

1

適配方案

各家廠商均給出了適配方案文件,如:

華為平板適配官方文件

小米平板適配官方文件

不過都挺冗長、晦澀難懂且不完全。適配也踩了很多坑,今天這篇文章就是帶著大家把坑填平。

平行視界

最少量開發、快速適配平板的方法。可以橫屏下顯示多Activity。各廠商都有,但叫法不同,如小米就叫平行視窗(magicWindow)。很多應用,如頭條、B站、抖音均使用了平行視界,如圖:

2

可以看到,橫屏下應用可以同時顯示兩個Activity。適配非常簡單,基本寫一個xml配置檔案就可以了。但缺點也很明顯,就是個手機版的雙屏版本,除了顯示內容變多了,沒有其它任何平板顯示互動的優化。最大的問題是,橫屏下單視窗時不能全屏,相比不適配,可顯示區域反而變少了。

如果你時間緊,任務重,可以考慮這種適配方案。但顯然不是一個高質量的適配方案,也達不到上架平板專區的要求。

正確獲取裝置及螢幕引數

工欲善其事,必先利其器。平板裝置各種尺寸都有,且還可以小窗、分屏、旋轉、平行視窗,必須先精確獲取螢幕引數。

判斷是平板裝置還是平板視窗

平板就是平板,為什麼還有平板裝置和平板視窗之分呢?因為平板分屏下,Activity變小,UI展示應該按照手機上來,所以平板也會有手機視窗。後面分屏會講到用法。

```kotlin /* * 判斷是否平板裝置,此值不會改變 / val isTabletDevice: Boolean by lazy { SystemPropertiesProxy.get(context, "ro.build.characteristics")?.contains("tablet") == true }

/* * 動態判斷是否平板視窗 * 在平板裝置上,也可能返回false。如分屏模式下 * 如想判斷物理裝置是不是平板,請使用 isTabletDevice * @return true:平板,false:手機 * @see isTabletDevice / fun isTabletWindow(context: Context): Boolean { return context.resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE } ```

正確獲取螢幕物理尺寸和視窗大小

手機上而言,視窗(或者Activity)大小就等於螢幕物理尺寸,但平板因為可以多視窗顯示,並不總是相等。

獲取螢幕物理尺寸:

```kotlin /* * 獲取螢幕物理尺寸 * * @param context 上下文 * @return 物理尺寸 / private fun getScreenPhysicsSize(context: Context): DisplayMetrics { val display = getDisplay(context) display?.getRealMetrics(mMetrics) return mMetrics }

private fun getDisplay(context: Context): Display? {
    val windowManager = context
        .getSystemService(Context.WINDOW_SERVICE) as WindowManager
    return if (Build.VERSION.SDK_INT >= 30) {
        context.display!!
    } else {
        windowManager.defaultDisplay
    }
}

```

獲取視窗大小:

kotlin fun getScreenSize(context: Context): Point { val point = Point() val displayMetrics = context.resources.displayMetrics point.x = displayMetrics.widthPixels point.y = displayMetrics.heightPixels return point }

判斷是否在平行視界

kotlin /** * 平行視窗模式(華為、小米) */ fun inMagicWindow(context: Context): Boolean { val config: String = context.resources.configuration.toString() return config.contains("hwMultiwindow-magic") || config.contains("miui-magic-windows") || config.contains("hw-magic-windows") }

判斷視窗/裝置處於橫屏

裝置橫屏,視窗不一定是橫屏。如小窗和分屏模式有可能是豎屏。

```kotlin /* * 視窗是橫屏 / fun isWindowLandscape(context: Context): Boolean { val orientation: Int = context.resources.configuration.orientation return orientation == Configuration.ORIENTATION_LANDSCAPE }

/**
 * 裝置是橫屏
 */
fun isDeviceLandscape(context: Context): Boolean {
    val screenPhysicsSize = getScreenPhysicsSize(context)
    return screenPhysicsSize.widthPixels > screenPhysicsSize.heightPixels
}

```

分屏適配

判斷是否在分屏模式

這裡注意Android7.0之後才支援分屏, isInMultiWindowMode為true時表示在分屏或小窗

kotlin if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { isInMultiWindowMode = activity.isInMultiWindowMode }

使用者進入分屏Acitivity的回撥:

kotlin override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) { super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig) // 當isInMultiWindowMode為true時,表示進入分屏或小窗 }

動態調整列表展示列數

分屏可以是1/3屏、1/2屏、2/3屏。所以我們應該根據分屏後螢幕尺寸來重新調整展示列數,如下圖:

fenping.gif

首先需要重寫Activity的onConfigurationChanged方法,觸發分屏不會銷燬Activity,而是會收到此方法的回撥,在此方法裡呼叫上面getScreenSize方法,通過當前視窗寬度計算出要顯示的列數,再重新設定LayoutManager。示例如下:

kotlin override fun onConfigurationChanged(newConfig: Configuration) { super.onConfigurationChanged(newConfig) val newColumn = min(7, max(3, (getScreenSize(context).x / dip2px(context, 150f).toFloat() + 0.3f).roundToInt()) // 根據視窗寬度動態計算出一個3-7的列數 recyclerView.setLayoutManager(GridLayoutManager(this, newColumn)) }

Dialog適配

手機裝置上,許多彈窗是寬鋪滿,底部彈出的,這在平板上展示會非常醜陋。所以一套彈窗應該有兩套彈出方式,如圖:

popup.jpg

可以看出,手機上Dialog應從底部彈出,平板應居中顯示,且不可以鋪滿。平板分屏狀態下,2/3屏和1/2屏時應該和手機顯示一致。

給不同裝置適配不同Dialog動畫

這就用到上面isTabletWindow()方法了,可以動態判斷是否平板視窗。此方法在平板全屏、2/3屏時返回true,1/3、1/2屏 和手機裝置上返回false。

kotlin dialog.window.setWindowAnimations(if (isTabletWindow(context)) R.style.DialogAnimFadeCenter else R.style.DialogAnimBottomUp)

修改Dialog位置

kotlin dialog.window.attributes.gravity = if (isTabletWindow(context)) Gravity.CENTER else Gravity.Bottom

修改Dialog寬度

dialog.window.attributes.width = if (isTabletWindow(context)) WRAP_CONTENT else MATCH_PARENT

旋轉螢幕適配

手機上大家絕大多數場景都是豎屏使用,大多數Android應用本來就不支援橫屏顯示,但是平板裝置橫屏很常見,使用者可能會經常切換螢幕方向。

旋轉螢幕不重建Activity

通過在Manifest中給Activity增加configChanges屬性,可以旋轉不銷燬重建Activity。如下:

xml <activity android:name=".com.example.DemoActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />

各屬性意義如下:

| screenLayout | 螢幕的顯示發生了變化---不同的顯示被啟用 | | ------------------ | ------------------------------------------------ | | orientation | 螢幕方向改變了---橫豎屏切換 | | screenSize | 螢幕大小改變了 | | smallestScreenSize | 螢幕的物理大小改變了,如:連線到一個外部的螢幕上 |

新增configChanges後,當螢幕方向改變時,Activity會回撥onConfigurationChanged()方法。就像前面分屏一樣,可以在此方法回撥中更新列數,View尺寸等。

禁止手機自動旋轉

很多時候,我們不想手機使用者自動旋轉螢幕,只鎖定豎屏就夠了。平板使用者可以自由旋轉。那一個應用怎麼滿足這兩種需求呢?我們知道可以在AndroidManifest中加android:screenOrientation="portrait",但Manifest中並不能動態判斷手機還是平板裝置。我們還知道可以在Activity中呼叫setRequestedOrientation()方法動態設定螢幕方向,但此時Activity已建立過了,強行改變螢幕方向會重建Activity,出現閃屏。我們當然不希望適配個平板還讓手機閃屏了。但網上搜了很久也沒找到解決方案,後來自己琢磨出來一個辦法:

1.在AndroidManifest中設定Activity為android:screenOrientation="behind"

xml <activity android:name="com.demo.DemoActivity" android:screenOrientation="behind" />

behind的意思是,螢幕方向和上一個Activity保持一致。

2.在Activity的onCreate裡,根據裝置型別,再次修改螢幕方向

kotlin override fun onCreate(savedInstanceState: Bundle?) { requestedOrientation = if (ScreenUtils.isTabletDevice) { ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED } else { ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } super.onCreate(savedInstanceState) }

如果是平板裝置,就再次指定為可自由旋轉,否則指定為豎屏。因為手機本來就是豎屏,所以指定為豎屏不會重建Activity,也就不會閃屏啦。

非全屏視窗

適配了這麼多,平板裝置的優勢還沒太顯現出來,那就是大螢幕,多內容!我們想要平行視窗的多視窗展示,也想要單視窗時撐滿螢幕。既要...也要...,能實現嗎?當然,看效果:

halfScreen.jpg

手機裝置仍然是鋪滿展示,平板半屏展示,既可以展示更多內容,也能避免沒有適配的頁面被橫向拉伸。

Activity定義半屏主題

```xml

```

此主題主要是設定透明背景和Activity滑入/滑出動畫。在Manifest中給需要半屏顯示的Activity設定此theme即可。

定義半屏滑入、滑出動畫

如果是抽屜Activity,那需要有一個側邊欄滑入、滑出的動畫。

```xml

// activity_slide_enter_left.xml

// activity_slide_exit_left.xml

```

但如果是在抽屜Activity之上再展示半屏Activity,就不需要動畫了。另外定義個主題,刪除android:windowAnimationStyle,或換成淡入、淡出動畫即可。

Activity onCreate方法半屏設定

Activity的onCreate方法中設定半屏比例、點選外側關閉和背景變暗等, 可以在BaseActivity中設定。

kotlin override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (inHalfScreenMode()) { // 是否是半屏模式,根據需要設定 val root = findViewById<View>(android.R.id.content) ?: return // 最頂層的View // 設定抽屜所佔比例,橫屏時比例佔40%,豎屏佔75% val width = (getScreenWidth(this) * if (isWindowLandscape(this)) 0.4 else 0.75).toInt() root.layoutParams.width = width (root.parent as View).setOnClickListener { // 抽屜開啟時,點選外側應該關閉該Activity finish() } root.setOnClickListener { } //防止點選穿透 // 設定背景變暗 window.addFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) window.attributes.dimAmount = 0.4f } }

inHalfScreenMode可以是從外部傳入Activity的Intent引數,就可以動態控制此Activity是否要半屏顯示啦。

相機適配

自動旋轉

相機和其它適配不同,為保證使用者體驗,相機在旋轉過程中預覽必須為連續的,所以不能銷燬重建Activity或View,而是要根據使用者旋轉角度實時旋轉介面元素,如圖:

camera.gif

監聽螢幕旋轉角度

OrientationEventListener是系統自帶的螢幕旋轉方向監聽,監聽範圍0-359度。幾個閾值我調整了許多次,基本避免了旋轉動畫跳動、不流暢等問題。

kotlin private var orientationEventListener: OrientationEventListener? = null /** * 開啟螢幕方向改變監聽 */ fun enableOrientationListener() { orientationEventListener = object : OrientationEventListener(requireContext()) { override fun onOrientationChanged(orientation: Int) { var currentOrientation = mCurrentOrientation if (orientation >= 330 || orientation in 0..29) { // 裝置放平會返回ORIENTATION_UNKNOWN(-1),不做處理,否則會抖動 currentOrientation = 0 } else if (orientation in 60..119) { currentOrientation = -90 } else if (orientation in 150..209) { currentOrientation = 180 } else if (orientation in 240..299) { currentOrientation = 90 } if (mCurrentOrientation != currentOrientation) { onScreenOrientationChanged(currentOrientation) //在此方法裡旋轉可見View mCurrentOrientation = currentOrientation } } } orientationEventListener?.enable() //開始監聽,使用完記得禁用 }

按需執行旋轉動畫

kotlin fun onScreenOrientationChanged(degree: Int) { // viewList即要旋轉的View列表 viewList.filter { it?.isVisible == true } //可見的View執行旋轉動畫 .forEach { ObjectAnimator.ofFloat(it, "rotation", previousDegree.toFloat(), degree.toFloat()) .start() } viewList.filter { it?.isVisible == false } //不可見的View直接更改旋轉角度 .forEach { it?.rotation = degree.toFloat() } }

根據寬高比使用不同佈局

因平板寬高比和手機裝置相差較大,大部分應用的相機都採用4:3的拍攝尺寸,全部使用一個佈局可能會導致黑邊過大、顯示不全等問題。所以應該根據寬高比使用不同佈局,如筆者以寬高比0.65625作為臨界值。

kotlin val layoutResId = if (screenWidth / screenHeight.toFloat() > 0.65625f) {// 超過了目標大小就緒要引用平板佈局 R.layout.fragment_camera_tablet } else { R.layout.fragment_camera }

相機其它適配注意點

不僅僅View需要旋轉,Dialog、Toast、PopupWindow等一切螢幕可見元素都需要。所以相機適配也是我花費最大精力的部分,但鑑於業務邏輯並不相同,不再贅述。

還有好多適配細節沒有提到,鑑於篇幅和作者的懶惰,權當拋磚引玉吧。