【Android平板適配】手機/平板二合一應用一站式適配攻略
為啥要適配Android平板
Android平板使用者越來越多
平板領域其實早已變天了,不再是IOS一家獨大。2020年ipad國內市場份額已不足50%,今年更是不足30%。隨著華為在平板上的發力,其它國內廠商也看到了Android平板這塊的利潤,紛紛推出新款平板。
平板專區,流量扶持
華為、榮耀、小米等廠商會有平板專區,如果適配了平板並且稽核通過,將會獲得極大的曝光。(如筆者開發的Elfinbook易飛這款應用,因為適配比較完善,順利進入小米平板專區,且最近兩個月均排在前三位,著實獲取了一把流量)
適配方案
各家廠商均給出了適配方案文件,如:
不過都挺冗長、晦澀難懂且不完全。適配也踩了很多坑,今天這篇文章就是帶著大家把坑填平。
平行視界
最少量開發、快速適配平板的方法。可以橫屏下顯示多Activity。各廠商都有,但叫法不同,如小米就叫平行視窗(magicWindow)。很多應用,如頭條、B站、抖音均使用了平行視界,如圖:
可以看到,橫屏下應用可以同時顯示兩個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屏。所以我們應該根據分屏後螢幕尺寸來重新調整展示列數,如下圖:
首先需要重寫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適配
手機裝置上,許多彈窗是寬鋪滿,底部彈出的,這在平板上展示會非常醜陋。所以一套彈窗應該有兩套彈出方式,如圖:
可以看出,手機上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,也就不會閃屏啦。
非全屏視窗
適配了這麼多,平板裝置的優勢還沒太顯現出來,那就是大螢幕,多內容!我們想要平行視窗的多視窗展示,也想要單視窗時撐滿螢幕。既要...也要...,能實現嗎?當然,看效果:
手機裝置仍然是鋪滿展示,平板半屏展示,既可以展示更多內容,也能避免沒有適配的頁面被橫向拉伸。
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,而是要根據使用者旋轉角度實時旋轉介面元素,如圖:
監聽螢幕旋轉角度
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等一切螢幕可見元素都需要。所以相機適配也是我花費最大精力的部分,但鑑於業務邏輯並不相同,不再贅述。
還有好多適配細節沒有提到,鑑於篇幅和作者的懶惰,權當拋磚引玉吧。