16、多屏适配与横竖屏:不同屏幕尺寸的网格计算、横竖屏切换的布局重建、自适应图标(AdaptiveIcon)

做Android桌面开发,绕不开一个现实:屏幕尺寸千奇百怪

我自己刚入行那会儿,就吃过这个亏。当时给一款平板做桌面,在模拟器上跑得好好的,结果真机一测——图标全挤到左上角去了。嗯,说白了就是没做多屏适配。

这一章,我就把多屏适配的核心三板斧讲清楚:网格计算、横竖屏切换、自适应图标。这三样搞定了,你的桌面在什么设备上都能站稳。

核心观点:桌面系统的多屏适配,本质是「网格系统」的弹性伸缩。不是简单缩放,而是根据屏幕参数重新计算行列数、图标大小、间距。

多屏适配核心知识体系 多屏适配 网格计算 • 获取屏幕宽高/密度 • 计算行列数 • 图标大小 = 网格单元 - 间距 • 支持动态调整 横竖屏切换 • 监听配置变更 • 重建布局 • 保存/恢复状态 • 避免闪烁 自适应图标 • 前景层 + 背景层 • 安全区域 72dp • 视觉大小一致 • 形状自适应 三者配合,桌面在任何屏幕上都「刚刚好」

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 的地方之一。

为什么会这样?因为横竖屏不仅仅是屏幕方向变了,宽高比变了,网格行列数也得变

我记得有一次,用户反馈说「手机横过来之后,桌面上有个文件夹打不开了」。查了半天,原因是横屏时网格行列数变了,但文件夹的弹出位置还是按竖屏算的,结果弹到了屏幕外面。

避坑指南:横竖屏切换时,一定要做三件事:

  1. 保存当前状态——图标位置、滚动偏移、打开的文件夹 ID
  2. 重建网格——重新计算行列数、图标大小
  3. 恢复状态——把图标放回原位,滚动到之前的位置

我曾经因为忘了保存滚动偏移,导致每次横竖屏切换后桌面都跳回顶部,用户骂声一片。

代码实现上,我推荐用 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