2023 Android 折叠屏适配详解,是时候点亮新技能了
The following article is from GSYTech Author 恋猫de小郭
⚠️本文超长,可收藏以备不时之需。
一般是 App 锁死旋转方向和采用了不可调整大小。
重启按钮:设备厂商可以为尺寸兼容模式的重启按钮赋予新的外观。(尺寸兼容模式可以让 App 的宽或者高尽可能充满屏幕)
sw >= 600dp 可以简单理解为你的屏幕的绝对宽度大于 600dp
<activity android:name=".MainActivity"
android:maxAspectRatio="2.4" />
<meta-data android:name="android.max_aspect" android:value="2.4" />
PS :如果 resizeableActivity 是true, maxAspectRatio 会不生效。
android:configChanges="screenLayout|smallestScreenSize|screenSize"
<meta-data
android:name="android.supports_size_changes" android:value="true" />
当应用的宽高比与其屏幕比例不兼容,App 锁死旋转方向和大小时会进入 Letterboxing 模式。 resizeableActivity 的效果主要看 TargetSDK 版本, Android 12(API 31)及更高版本上可能还是会进去分屏模式。 maxAspectRatio 的作用主要看 resizeableActivity。 配置 android:configChanges 和 supports_size_changes 防止重启 Activity 保证连续性。
Compose Activity Embedding SlidingPaneLayout
「Compact」: 普通手机设备,宽度 < 600dp 「Medium」:折叠屏或平板的竖屏,600dp < 宽度 < 840dp 「Expanded」:展开屏幕,平板或平板电脑等,宽度 > 840dp
Compose
import androidx.activity.compose.setContent
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
class MyActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// Calculate the window size class for the activity's current window. If the window
// size changes, for example when the device is rotated, the value returned by
// calculateSizeClass will also change.
val windowSizeClass = calculateWindowSizeClass(this)
// Perform logic on the window size class to decide whether to use a nav rail.
val useNavRail = windowSizeClass.widthSizeClass > WindowWidthSizeClass.Compact
// MyScreen knows nothing about window size classes, and performs logic based on a
// Boolean flag.
MyScreen(useNavRail = useNavRail)
}
}
}
更多可见:
https://github.com/google/accompanist/tree/3810fe1182cf52c6660787ae3226dfb7f5ad372a/sample/src/main/java/com/google/accompanist/sample/adaptive
关于 Compose 适配折叠屏 Demo 还可以参考 :
https://github.com/android/compose-samples/tree/main/JetNews
Activity Embedding
无论是 Android 12L(API 32)以上的大屏设备,还是更早期折叠屏平台版本的设备,Jetpack WindowManager 都能帮助构建 Activity Embedding 多窗格布局,这种基于多个 Activity 而非 fragment 或基于视图的布局(如 SlidingPaneLayout)的方式可以「最简单提供大屏幕用户体验而无需重构源代码」。
splitRatio:设置容器比例。该值为开区间 (0.0, 1.0) 内的浮点数。 splitLayoutDirection:指定分割容器相对于彼此的布局方式。值包括: ltr:左到右 rtl:右到左 locale:ltr 或 rtl 由语言环境设置决定
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<property
android:name="android.window.PROPERTY_ACTIVITY_EMBEDDING_SPLITS_ENABLED"
android:value="true" />
</application>
</manifest>
<!-- main_split_config.xml -->
<resources
xmlns:window="http://schemas.android.com/apk/res-auto">
<!-- Define a split for the named activities. -->
<SplitPairRule
window:splitRatio="0.33"
window:splitLayoutDirection="locale"
window:splitMinWidthDp="840"
window:splitMaxAspectRatioInPortrait="alwaysAllow"
window:finishPrimaryWithSecondary="never"
window:finishSecondaryWithPrimary="always"
window:clearTop="false">
<SplitPairFilter
window:primaryActivityName=".ListActivity"
window:secondaryActivityName=".DetailActivity"/>
</SplitPairRule>
<!-- Specify a placeholder for the secondary container when content is
not available. -->
<SplitPlaceholderRule
window:placeholderActivityName=".PlaceholderActivity"
window:splitRatio="0.33"
window:splitLayoutDirection="locale"
window:splitMinWidthDp="840"
window:splitMaxAspectRatioInPortrait="alwaysAllow"
window:stickyPlaceholder="false">
<ActivityFilter
window:activityName=".ListActivity"/>
</SplitPlaceholderRule>
<!-- Define activities that should never be part of a split. Note: Takes
precedence over other split rules for the activity named in the
rule. -->
<ActivityRule
window:alwaysExpand="true">
<ActivityFilter
window:activityName=".ExpandedActivity"/>
</ActivityRule>
</resources>
更多可见:
https://developer.android.com/guide/topics/large-screens/activity-embedding
SlidingPaneLayout
如果测量后发现列表窗格的最小尺寸为 200dp,而详细信息窗格需要 400dp,那么只要可用宽度不小于 600dp,SlidingPaneLayout 就会自动并排显示两个窗格 如果子视图的总宽度超过了 SlidingPaneLayout 中的可用宽度,这些视图就会重叠在一起。
如果视图没有重叠,那么 SlidingPaneLayout 支持对子视图使用布局参数 layout_weight,以指定在测量结束后如何划分剩余的空间。
<!-- two_pane.xml -->
<androidx.slidingpanelayout.widget.SlidingPaneLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/sliding_pane_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- The first child view becomes the left pane. When the combined
desired width (expressed using android:layout_width) would
not fit on-screen at once, the right pane is permitted to
overlap the left. -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list_pane"
android:layout_width="280dp"
android:layout_height="match_parent"
android:layout_gravity="start"/>
<!-- The second child becomes the right (content) pane. In this
example, android:layout_weight is used to expand this detail pane
to consume leftover available space when the
the entire window is wide enough to fit both the left and right pane.-->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/detail_container"
android:layout_width="300dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:background="#ff333333"
android:name="com.example.SelectAnItemFragment" />
</androidx.slidingpanelayout.widget.SlidingPaneLayout>
使用的设备带有遮挡部分屏幕的铰链,它会自动将 App 的内容放置在任一侧。
为了防止用户滑到空窗格,需要点击击列表项才能加载有关该窗格的信息,但允许他们滑回到列表,在有空间并排显示两个视图的可折叠设备或平板电脑上,锁定模式将被忽略。
更多可见:
https://developer.android.com/guide/topics/ui/layout/twopane?hl=zh-cn
识别折叠屏
state:设备的折叠状态,FLAT (完全打开) 或 HALF_OPENED (处于打开和关闭状态之间的中间位置)。 orientation:折叠或铰链的方向,HORIZONTAL 或者 VERTICAL。 occlusionType:折叠或铰链是否隐藏了部分显示屏,NONE (不遮挡)或者 FULL (遮挡)。 isSeparating:折叠或铰链是否创建两个显示区域,true(半开/双屏) 或 false。
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
hingeAngleSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_HINGE_ANGLE)
fun isTableTopMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
fun isBookMode(foldFeature: FoldingFeature) =
foldFeature.isSeparating &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
override fun onStart() {
super.onStart()
initializePlayer()
layoutUpdatesJob = uiScope.launch {
windowInfoRepository.windowLayoutInfo
.collect { newLayoutInfo ->
onLayoutInfoChanged(newLayoutInfo)
}
}
}
override fun onStop() {
super.onStop()
layoutUpdatesJob?.cancel()
releasePlayer()
}
private fun onLayoutInfoChanged(newLayoutInfo: WindowLayoutInfo) {
if (newLayoutInfo.displayFeatures.isEmpty()) {
// The display doesn't have a display feature, we may be on a secondary,
// non foldable-screen, or on the main foldable screen but in a split-view.
centerPlayer()
} else {
newLayoutInfo.displayFeatures.filterIsInstance(FoldingFeature::class.java)
.firstOrNull { feature -> isInTabletopMode(feature) }
?.let { foldingFeature ->
val fold = foldPosition(binding.root, foldingFeature)
foldPlayer(fold)
} ?: run {
centerPlayer()
}
}
}
private fun centerPlayer() {
ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, 0)
binding.playerView.useController = true // use embedded controls
}
private fun foldPlayer(fold: Int) {
ConstraintLayout.getSharedValues().fireNewValue(R.id.fold, fold)
binding.playerView.useController = false // use custom controls
}
窗口大小适配
getMetrics() getSize() getRealMetrics() getRealSize() getRectSize() getWidth() getHeight()
getWindowVisibleDisplayFrame() getLocationOnScreen
Platform: getCurrentWindowMetrics() getMaximumWindowMetrics() Jetpack: WindowMetricsCalculator#computeCurrentWindowMetrics() WindowMetricsCalculator#computeMaximumWindowMetrics()
getCurrentWindowMetrics() :返回系统当前窗口状态对象 WindowMetrics getMaximumWindowMetrics() :返回系统的最大窗口状态 WindowMetrics
val windowMetrics = context.createDisplayContext(display)
.createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION, null)
.getSystemService(WindowManager::class.java)
.maximumWindowMetrics
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val windowMetrics = WindowMetricsCalculator.getOrCreate()
.computeCurrentWindowMetrics(this@MainActivity)
val bounds = windowMetrics.getBounds()
...
}
使用 wrap_content、match_parent 避免硬编码。 使用 ConstraintLayout 做根布局,方便屏幕尺寸变化,视图自动移动和拉伸。 在 App 的 AndroidManifest 里将 application 或 activity 的 android:resizeableActivity 属性设置为 true 来支持大小调整并支持响应式/自适应布局。 res/layout/ 可以通过创建如 layout-w600dp 的等目录来提供自适应的布局。 ·····
多窗口和生命周期
Android 7.0 支持分屏:左右/上下显示两个窗口。 Android 8.0 支持画中画模式,此时处于画中画的 Activity 虽处于前台,但处于 Paused 状态。 Android 9.0 (API 28) 及以下:多窗口下只有获得焦点应用处于 Resumed 状态,其它可见 Activity 仍处于 Paused 状态。 Android 10.0 (API 29) :多窗口模式时,每个 Acttivity 全部处于Resumed状态。
<meta-data
android:name="android.allow_multiple_resumed_activities" android:value="true" />
也就是俗称的 Multi-resume 状态。
override fun onTopResumedActivityChanged(topResumed: Boolean) {
if (topResumed) {
// Top resumed activity
// Can be a signal to re-acquire exclusive resources
} else {
// No longer the top resumed activity
}
}
事实上只要通过回调做好判断,其实这个「焦点」切换体验是无缝的。
在多窗口模式下,Android 可能会禁用或忽略不适用于与其他 Activity 或应用共享设备屏幕的 Activity 的功能。
注意:画中画模式是多窗口模式的特例,如果isInPictureInPictureMode() 返回 true,则 isInMultiWindowMode() 也会返回 true。
如果 Activity 正在进入多窗口模式,则系统向该方法传递一个值 true;如果 Activity 正在离开多窗口模式,则系统向该方法传递一个值 false。
如果 Activity 正在进入画中画模式,则系统向该方法传递一个 true 值;如果 Activity 正在离开画中画模式,则系统向该方法传递一个 false 值。
Fragment 同样提供了类似方式,如 Fragment.onMultiWindowModeChanged() 。
@override
void didChangeMetrics() {
final ui.Display? display = _display;
if (display == null) {
return;
}
if (display.size.width / display.devicePixelRatio < kOrientationLockBreakpoint) {
SystemChrome.setPreferredOrientations(<DeviceOrientation>[
DeviceOrientation.portraitUp,
]);
} else {
SystemChrome.setPreferredOrientations(<DeviceOrientation>[]);
}
}
另外,Flutter 上关于支持多个显示器尺寸的支持还在同步 #125938 、#125939 ,感兴趣的也可以关注一下。
兼容的 Letterboxing 模式表现。 resizeableActivity 等配置的不同行为。 Compose /Activity Embedding /SlidingPaneLayout 的适配方案。 折叠屏的判断、窗口适配和生命周期兼容。 Flutter API。
最后推荐一下我做的网站,玩Android: wanandroid.com ,包含详尽的知识体系、好用的工具,还有本公众号文章合集,欢迎体验和收藏!
推荐阅读:
扫一扫 关注我的公众号
如果你想要跟大家分享你的文章,欢迎投稿~
┏(^0^)┛明天见!