16、多屏适配与横竖屏:不同屏幕尺寸的网格计算、横竖屏切换的布局重建、自适应图标(AdaptiveIcon)
做Android桌面开发,绕不开一个现实:屏幕尺寸千奇百怪。
我自己刚入行那会儿,就吃过这个亏。当时给一款平板做桌面,在模拟器上跑得好好的,结果真机一测——图标全挤到左上角去了。嗯,说白了就是没做多屏适配。
这一章,我就把多屏适配的核心三板斧讲清楚:网格计算、横竖屏切换、自适应图标。这三样搞定了,你的桌面在什么设备上都能站稳。
核心观点:桌面系统的多屏适配,本质是「网格系统」的弹性伸缩。不是简单缩放,而是根据屏幕参数重新计算行列数、图标大小、间距。
16.1 不同屏幕尺寸的网格计算
桌面的本质是什么?说白了就是一个网格容器。图标、文件夹、小部件,都摆在这个网格里。
网格计算的核心公式很简单:
// 伪代码:网格计算核心逻辑
int screenWidth = getScreenWidthInDp(); // 屏幕宽度(dp)
int screenHeight = getScreenHeightInDp(); // 屏幕高度(dp)
int cellWidth = screenWidth / columnCount; // 每个网格单元的宽度
int cellHeight = cellWidth; // 通常保持正方形
int iconSize = (int)(cellWidth * 0.6f); // 图标占网格的60%
int iconMargin = (cellWidth - iconSize) / 2; // 居中留白
嗯,这里要注意:行列数不是写死的。我见过不少项目直接在代码里写 COLUMNS = 4,结果在 7 寸平板上图标大得像脸盆,在 6.7 寸手机上又挤得不行。
我的经验:行列数应该根据屏幕宽度动态计算。比如宽度在 360dp~480dp 之间用 4 列,480dp~600dp 用 5 列,600dp 以上用 6 列。这个阈值你可以根据产品需求微调。
具体实现时,我习惯在 Launcher.onConfigurationChanged() 里重新计算:
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// 重新获取屏幕尺寸
DisplayMetrics dm = getResources().getDisplayMetrics();
float widthDp = dm.widthPixels / dm.density;
// 根据宽度决定列数
int columns;
if (widthDp < 400) {
columns = 4;
} else if (widthDp < 550) {
columns = 5;
} else {
columns = 6;
}
// 重新布局
mGridLayout.setColumns(columns);
mGridLayout.requestLayout();
}
你想想看,如果桌面是固定行列,用户从手机换到平板,体验会多糟糕。动态计算才是正解。
16.2 横竖屏切换的布局重建
横竖屏切换,是桌面系统里最容易出 bug 的地方之一。
为什么会这样?因为横竖屏不仅仅是屏幕方向变了,宽高比变了,网格行列数也得变。
我记得有一次,用户反馈说「手机横过来之后,桌面上有个文件夹打不开了」。查了半天,原因是横屏时网格行列数变了,但文件夹的弹出位置还是按竖屏算的,结果弹到了屏幕外面。
避坑指南:横竖屏切换时,一定要做三件事:
- 保存当前状态——图标位置、滚动偏移、打开的文件夹 ID
- 重建网格——重新计算行列数、图标大小
- 恢复状态——把图标放回原位,滚动到之前的位置
我曾经因为忘了保存滚动偏移,导致每次横竖屏切换后桌面都跳回顶部,用户骂声一片。
代码实现上,我推荐用 onSaveInstanceState() 和 onRestoreInstanceState() 来保存状态:
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// 保存当前滚动位置
outState.putInt("scroll_x", mWorkspace.getScrollX());
outState.putInt("scroll_y", mWorkspace.getScrollY());
// 保存当前屏幕索引
outState.putInt("current_screen", mPager.getCurrentItem());
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
// 恢复滚动位置
int scrollX = savedInstanceState.getInt("scroll_x");
int scrollY = savedInstanceState.getInt("scroll_y");
mWorkspace.scrollTo(scrollX, scrollY);
// 恢复当前屏幕
mPager.setCurrentItem(savedInstanceState.getInt("current_screen"));
}
另外,横竖屏切换时布局重建会触发 onCreate(),如果你在 onCreate() 里做了大量初始化工作,切换时会卡顿。我的做法是把重量级初始化放到 onStart() 里,或者用 Fragment 来管理,避免整个 Activity 重建。
一个小技巧:如果你不想让 Activity 重建,可以在 Manifest 里给 Activity 加上 android:configChanges="orientation|screenSize",然后自己处理 onConfigurationChanged()。但这样你得手动处理布局变化,工作量不小。我个人建议还是让系统重建,只要做好状态保存就行。
16.3 自适应图标(AdaptiveIcon)
自适应图标是 Android 8.0(API 26)引入的。它的设计初衷是:不同厂商的设备,图标形状可以不一样。比如三星用圆角矩形,小米用圆形,Pixel 用圆角方形。
如果你的桌面不支持自适应图标,那图标在不同设备上就会「穿帮」——有的被裁切,有的留白太多。
自适应图标的原理很简单:两层结构。
- 背景层:一个纯色或渐变的背景,铺满整个图标区域
- 前景层:实际的图标图案,放在安全区域内
安全区域是 72dp 内的 66dp 圆形区域。说白了,就是图标的核心内容要放在中间,四周留出 3dp 的裁剪余量。
<!-- res/mipmap-anydpi-v26/ic_launcher.xml -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>
在桌面系统中,加载自适应图标时要注意:不要直接用 getDrawable(),因为不同设备对自适应图标的处理方式不同。我建议用 IconCompat 或者 AdaptiveIconDrawable 来加载:
// 加载自适应图标
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Drawable icon = getPackageManager().getActivityIcon(activityInfo);
if (icon instanceof AdaptiveIconDrawable) {
AdaptiveIconDrawable adaptiveIcon = (AdaptiveIconDrawable) icon;
// 获取背景和前景
Drawable bg = adaptiveIcon.getBackground();
Drawable fg = adaptiveIcon.getForeground();
// 可以在这里做自定义处理
}
}
我的经验:有些第三方应用没有提供自适应图标,只有传统的方形图标。这种情况下,我建议你给它们套一个默认的背景层,保证视觉上统一。否则桌面上有的图标是圆角,有的是方形,看起来会很乱。
另外,自适应图标的大小也要根据网格动态调整。我一般把图标大小设为网格单元宽度的 55%~65%,这样既不会太大显得拥挤,也不会太小看不清。
// 动态计算图标大小
int cellSize = screenWidthDp / columnCount;
int iconSize = (int)(cellSize * 0.6f);
// 设置图标大小
ImageView iconView = findViewById(R.id.icon);
ViewGroup.LayoutParams params = iconView.getLayoutParams();
params.width = dpToPx(iconSize);
params.height = dpToPx(iconSize);
iconView.setLayoutParams(params);
嗯,到这里,多屏适配的三个核心点就讲完了。网格计算解决「怎么摆」,横竖屏切换解决「怎么变」,自适应图标解决「怎么好看」。三者缺一不可。
我个人觉得,多屏适配是桌面开发里最考验细节的环节。你想想看,用户每天面对的就是这个桌面,如果图标忽大忽小、横竖屏切换闪一下、图标形状不统一——这些细节累积起来,就是「这个桌面不好用」的印象。
所以,别嫌麻烦。把网格算准,把状态存好,把图标适配好。你的桌面,才能在千奇百怪的 Android 设备上,都显得「刚刚好」。
公众号:蓝海资料掘金营,微信deep3321