4、Launcher主界面架构:Activity与Fragment设计、主界面布局(状态栏、导航栏、内容区)、屏幕适配与分辨率处理
好,咱们进入第四章。这一章要聊的是Launcher的骨架——主界面架构。
说实话,车载Launcher和手机Launcher最大的区别在哪?手机你可以随便折腾,大不了重启。车载不行。用户正在开车,你界面卡了、布局乱了、导航栏点不动了,那是要出事的。所以这一章的内容,我建议你认真看。
4.1 Activity与Fragment设计:谁主谁次?
很多新手会问:Launcher主界面到底用Activity还是Fragment?
我的答案是:一个Activity + 多个Fragment。这是车载行业的主流做法,也是我踩过坑之后的选择。
为什么不用多Activity?
车载系统对内存和启动速度要求极高。每多一个Activity,就意味着多一份系统资源开销。而且Activity之间的切换会有明显的闪白或黑屏,这在驾驶过程中非常影响体验。
我当年第一个车载项目,就是用了三个Activity分别做主页、应用列表和设置。结果呢?客户试驾时一划屏,中间闪了一下白屏,当场被骂回来。后来全改成Fragment,问题解决。
4.1.1 主Activity的职责
主Activity其实很轻。它只做三件事:
- 初始化系统窗口(比如全屏、隐藏导航栏)
- 管理Fragment的切换
- 处理系统级事件(如熄屏、唤醒)
说白了,它就是个容器。真正的UI内容都在Fragment里。
class LauncherActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置全屏,隐藏系统状态栏
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
setContentView(R.layout.activity_launcher)
// 加载主Fragment
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.replace(R.id.fragment_container, MainFragment())
.commit()
}
}
}
4.1.2 Fragment的划分策略
我个人习惯把Launcher拆成三个核心Fragment:
| Fragment名称 | 职责 | 生命周期 |
|---|---|---|
| StatusBarFragment | 显示时间、信号、蓝牙连接状态 | 常驻,不销毁 |
| ContentFragment | 主内容区,比如地图、音乐卡片 | 可替换,支持懒加载 |
| NavigationBarFragment | 底部快捷按钮,返回主页、空调等 | 常驻,不销毁 |
你想想看,状态栏和导航栏几乎不会变,如果每次切换页面都重新创建,那得多浪费性能?所以这两个Fragment我设置为setRetainInstance(true),让它们一直活着。
小技巧:Fragment的切换建议用show/hide而不是replace。replace会销毁重建,show/hide只是控制可见性。我在项目中测试过,show/hide的切换速度比replace快30%以上。
4.2 主界面布局:三块区域怎么分?
车载Launcher的布局,说白了就是三个字:上、中、下。
上边是状态栏,显示时间、网络、蓝牙这些信息。中间是内容区,放地图、音乐、天气这些卡片。下边是导航栏,放返回主页、空调控制、应用列表这些快捷入口。
嗯,这里要注意:千万不要用绝对布局。车载屏幕千奇百怪,有方的、有长的、有带鱼屏,绝对布局会让你改到怀疑人生。
4.2.1 布局文件示例
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 状态栏区域,高度固定为48dp -->
<FrameLayout
android:id="@+id/status_bar_container"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="#1A000000" />
<!-- 内容区域,权重为1,自动填充剩余空间 -->
<FrameLayout
android:id="@+id/content_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1" />
<!-- 导航栏区域,高度固定为56dp -->
<FrameLayout
android:id="@+id/nav_bar_container"
android:layout_width="match_parent"
android:layout_height="56dp"
android:background="#FF1A1A1A" />
</LinearLayout>
为什么状态栏和导航栏用固定高度?因为车载屏幕虽然分辨率不同,但物理尺寸差异不大。48dp和56dp在大多数屏幕上看起来都合适。内容区用layout_weight="1",让它自适应填充。
注意:有些车机系统会自带虚拟按键(比如返回键、Home键)。如果你的导航栏和系统虚拟按键重叠了,用户会点错。我曾经在一个项目里没处理这个问题,结果用户想点空调,却按到了系统返回键,直接被投诉。解决方案是在布局底部加上android:fitsSystemWindows="true",或者动态获取系统导航栏高度并做偏移。
4.3 屏幕适配与分辨率处理
这是车载开发里最让人头疼的问题,没有之一。
手机屏幕好歹还有个主流分辨率,车载屏幕呢?1024x600、1280x720、1920x1080、1920x720(带鱼屏)、甚至还有3840x720的超宽屏。你想想看,同一个布局在不同屏幕上可能完全走样。
4.3.1 使用dp和sp,别用px
这个道理大家都懂,但我还是要强调:所有尺寸都用dp,所有字体都用sp。px是绝对像素,在不同密度屏幕上会变形。dp是密度无关像素,系统会自动换算。
举个例子:
- 在160dpi的屏幕上,1dp = 1px
- 在320dpi的屏幕上,1dp = 2px
- 在480dpi的屏幕上,1dp = 3px
所以如果你用px写了一个48px的按钮,在低分辨率屏幕上可能很大,在高分辨率屏幕上可能小到点不到。用dp就没这个问题。
4.3.2 创建多套资源文件夹
我建议至少创建以下资源文件夹:
res/
layout-sw600dp/ // 7英寸以上平板类屏幕
layout-sw720dp/ // 10英寸以上大屏
layout-land/ // 横屏专用布局
values-sw600dp/ // 对应的尺寸和字体资源
values-sw720dp/
drawable-mdpi/ // 低分辨率图标
drawable-hdpi/ // 中分辨率图标
drawable-xhdpi/ // 高分辨率图标
drawable-xxhdpi/ // 超高分辨率图标
为什么要分这么细?因为车载屏幕的宽高比差异太大了。同样是1920宽,有的屏幕高1080,有的高720。你用同一套布局,在1920x1080上可能刚好,在1920x720上内容区就被压扁了。
我的经验:对于超宽屏(比如1920x720),我通常会单独写一套布局。内容区的卡片从横向排列改成网格排列,避免单行太长导致用户视线需要左右移动。这在驾驶过程中非常重要——用户扫一眼就要获取信息,视线移动越少越好。
4.3.3 动态适配代码
有时候光靠资源文件夹不够,还需要在代码里动态适配。比如获取屏幕宽高,然后按比例计算元素大小。
fun getScreenWidthHeight(context: Context): Pair<Int, Int> {
val displayMetrics = context.resources.displayMetrics
return Pair(displayMetrics.widthPixels, displayMetrics.heightPixels)
}
// 按比例计算卡片宽度,比如占屏幕宽度的45%
fun calculateCardWidth(screenWidth: Int): Int {
return (screenWidth * 0.45).toInt()
}
但要注意,不要滥用动态计算。能写在xml里的尽量写在xml里,代码里只处理那些确实需要运行时判断的情况。比如某些车机在行驶中会隐藏状态栏,这时候内容区的高度就需要重新计算。
4.3.4 适配测试清单
我每次做完适配,都会拿这个清单过一遍:
- 所有文字是否完整显示,没有截断?
- 按钮点击区域是否不小于48dp(人机工程学最小尺寸)?
- 横竖屏切换后布局是否正常?
- 系统虚拟按键弹出后,内容区是否被遮挡?
- 不同dpi下图标是否清晰,没有模糊?
嗯,这个清单看起来简单,但每一条我都踩过坑。尤其是第4条,系统虚拟按键的高度在不同车机上不一样,有的24dp,有的48dp。我建议动态获取:
fun getNavigationBarHeight(context: Context): Int {
val resourceId = context.resources
.getIdentifier("navigation_bar_height", "dimen", "android")
return if (resourceId > 0) {
context.resources.getDimensionPixelSize(resourceId)
} else {
0
}
}
好了,这一章的内容就这些。核心就三句话:一个Activity管全局,三个Fragment分区域,dp+多资源文件夹搞定适配。下一章咱们开始真正写代码,把状态栏和导航栏做出来。