尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Android 13 Launcher3 Hotseat布局方向定制全解析

Android 13 Launcher3 Hotseat布局方向定制全解析 做Launcher定制的同学应该都有同感你只是想改一个桌面细节结果打开源码一看横竖屏、RTL镜像、设备形态、拖拽落点全都搅在一起。最近我们团队在几台Android 13设备上做Hotseat定制化系列第一个课题就是Hotseat布局方向。Hotseat就是桌面底部那条固定应用停靠栏电话、短信、相机这类高频入口默认都摆在里面。“布局方向”这四个字在Launcher3源码里不是一个单一开关它同时涵盖屏幕物理方向、Hotseat内部图标的排列方向、以及系统语言带来的文本方向布局。这篇文章把我实际动手改这一块时涉及的代码链路、修改方案和踩过的坑完整拆一遍给同样在做Android 13桌面定制、Launcher二次开发的同学一个可以照着走的参考。1. 为什么先从Hotseat的“布局方向”开始写1.1 Hotseat在Android 13里到底管着什么在Android 13的Launcher3/Trebuchet体系里Hotseat是一个独立的ViewGroup在布局树上和Workspace平级。Workspace是用户左右滑动的那些桌面页图标、文件夹、小组件都挂在它下面Hotseat则固定在屏幕底部是一排不跟随页面滚动的常驻图标区。两者最直观的区别就是切换桌面页面时Workspace里的图标跟着滚动Hotseat里的图标纹丝不动。这个设计承担的任务很明确高频入口永不丢失。用户不管翻到第几屏都能在同一个位置点到电话、短信、浏览器。所以Hotseat在启动器里的优先级非常高它不能出问题否则整个桌面的可用性会瞬间崩掉。另外多说一句Android 12L之后的平板上还有一个Taskbar容易和Hotseat搞混。Taskbar是平板上的临时任务栏属于系统交互层的一部分Hotseat才是Launcher自身布局里的固定组件。两套逻辑落在完全不同的代码路径上定制之前先分清楚不然改错地方会很尴尬。1.2 “布局方向”四个字在源码里其实对应三层含义刚开始接触Hotseat时很容易把“布局方向”简单理解成横屏还是竖屏。实际上在Launcher3源码里这个问题至少可以拆成三层方向维度源码对应变化触发点屏幕物理方向Configuration.orientation横竖屏切换图标排列方向Hotseat.getCellX / getCellYDeviceProfile重建文本方向布局ViewGroup.layoutDirection系统语言切换第一层最好理解手机转一下Hotseat的宽高、padding、图标大小全部跟着变。第二层被很多人忽略Hotseat里面能排几个图标以及图标是从左往右排还是从上往下排实际由CellLayout的cellX/cellY坐标决定。第三层是RTL阿拉伯语、希伯来语环境下整个Launcher像照镜子一样从左到右变成从右到左Hotseat的图标排序也会被镜像。这三层经常同时发生。比如一台支持RTL的平板横屏状态下把系统语言切成阿拉伯语Hotseat既要做横竖屏适配又要做竖排侧边栏迁移还要做镜像翻转。任何一个环节没照顾到桌面上就会出现图标重叠、顺序错乱、落点偏移这类问题。1.3 为什么这是整个定制系列的第001篇这个系列后续会写到Hotseat的图标间距控制、文件夹上移逻辑、badge角标重绘、拖拽动画、壁纸联动。这些改动有一个共同前提Hotseat的摆放方向和排列顺序必须是稳定的、可预期的。方向不定后面全是返工。比如你做图标间距横屏和竖屏的密度策略可能不一样做文件夹样式竖排Hotseat里文件夹的展开方向和横排Hotseat完全不同。这些工作如果建立在一个“方向逻辑随时会打架”的基础上改完这头那头又炸。所以先把布局方向的地基打牢后面的定制才有地方落脚。2. 追源码Hotseat的方向逻辑都藏在哪几个文件里2.1 起点是launcher.xml里的hotseat节点打开Android 13的Launcher3源码找到res/layout/launcher.xmlHotseat节点就在整个布局树的底部。我贴一个裁剪过的关键结构FrameLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:idid/launcher com.android.launcher3.Workspace android:idid/workspace / com.android.launcher3.Hotseat android:idid/hotseat android:layout_widthmatch_parent android:layout_heightwrap_content android:layout_gravitybottom / /FrameLayout只看这个XML会觉得Hotseat的方向是由宽高推导出来的match_parent宽度加上底部位置自然就是横排。但这只是结果不是原因。真正决定Hotseat横着排还是竖着排的是后面要讲的DeviceProfile和Hotseat自身的测量、布局逻辑。实际源码里launcher.xml要复杂得多里面还有搜索框、页面指示器、ScrimView这些节点。但看方向的切入路径从hotseat节点进是没有问题的。2.2 DeviceProfile方向开关的真正持有者DeviceProfile可以理解成一份“当前设备形态下的Launcher参数快照”。屏幕旋转、分辨率变化、密度变化、平板手机切换都会触发Launcher重建这份快照。所有UI组件在测量和布局时都从这份快照里读参数。和Hotseat方向强相关的关键字段大致有下面这些isTablet是否走平板布局分支isTwoPanels是否双屏工作区平板横屏场景常见numHotseatIconsHotseat最多能放几个图标hotseatTopPadding/hotseatBottomPadding/hotseatLeftRightPadding各个方向的padding值。早期Launcher3版本里DeviceProfile还有一个isVerticalBarLayout()方法字面意思就是判断Hotseat是否作为一条竖直栏靠在屏幕侧边。Android 13代码经过多次重构方法名可能不完全一样但判断逻辑的核心还是落在上面这些字段的组合上。在实际定制时最忌讳的做法是在Activity/Fragment里到处写if (isTablet isLandscape)这类散装判断。方向状态应该收敛到DeviceProfile上让所有组件统一从profile里读取后面维护起来会轻松很多。2.3 Hotseat.java里三个决定图标位置的“命门”方法Hotseat这个类本身不大但从方向定制角度看有三个方法必须吃透。public int getCellX(int orderInHotseat) { return orderInHotseat; } public int getCellY(int orderInHotseat) { return 0; } public int getOrderInHotseat(int screenId) { return screenId; }默认实现下图标横向排列第几个图标就放在第几列行号固定是0。getOrderInHotseat负责把Workspace里的屏幕ID映射成Hotseat格子的序号。想让Hotseat变成纵向排布思路很直接把getCellY从0改成orderInHotseatgetCellX固定成0。但千万别只改这两个方法就以为大功告成因为Hotseat这套坐标还会被图标拖拽落点、文件夹位置计算、RTL镜像逻辑引用改动必须成体系不能顾头不顾尾。2.4 配置项里的两个关键维度除了Java代码还有两个配置文件会影响方向逻辑。第一个是res/values/config.xml里的config_deviceProfiles字符串数组。InvariantDeviceProfile会从这个数组解析出profileId、行数、列数、Hotseat图标个数、文件夹布局ID等参数。这个数组决定了Hotseat最多放几个图标也间接影响后续做方向切换时到底要预留多少个格子。第二个是res/values/dimens.xml里的Hotseat相关尺寸比如图标大小、各方向padding、间距。竖排侧边栏场景下top/bottom padding不能还按横排那套来否则第一颗图标会顶到状态栏或者最后一颗图标会被手势条挡住。实际项目里这两个文件经常要根据设备形态调好几轮。我的建议是先把config_deviceProfiles里的hotseat图标数定死再回来调dimens顺序别反。否则图标数一变dimens那边全部要重新适配。3. 实战改造横屏和平板场景下把Hotseat变成左侧纵向停靠栏3.1 这次定制的目标拆解这次项目的需求比较明确手机竖屏时保持Android 13默认的底部横排Hotseat手机横屏和平板形态下Hotseat变成屏幕左侧一条竖直停靠栏图标从上往下排。为什么选左侧而不是右侧一是符合大多数用户从左往右的阅读视觉顺序二是不跟Workspace里的文件夹页面指示器抢位置那个指示器通常贴在屏幕右侧Hotseat再挤过去会很难受。整个改造拆成三个子任务让Hotseat在目标场景下切换排列方向从横向排变成纵向排切换后Hotseat自身宽高、padding要正确不能被系统手势条或者状态栏遮挡Workspace的可用区域要同步变化否则桌面上最后一列图标会被Hotseat挡住。这三个子任务缺一不可很多定制工程就是死在第三个子任务上。3.2 在DeviceProfile中增加hotseatLayoutDirection字段方向状态最合理的归属是DeviceProfile而不是Activity里的临时变量。原因前面说过Launcher在配置变化时会new新的DeviceProfile所有UI逻辑从这个profile读参数。把方向归纳成profile上的一个字段后横竖屏切换、多窗口变化、RTL切换都只需要刷新这个字段不用改动散落各处的判断。我在DeviceProfile里加了两行常量和一个字段public static final int HOTSEAT_LAYOUT_HORIZONTAL 0; public static final int HOTSEAT_LAYOUT_VERTICAL 1; public int hotseatLayoutDirection HOTSEAT_LAYOUT_HORIZONTAL;然后在构造DeviceProfile的地方根据当前orientation和isTablet赋值boolean isLandscape resources.getConfiguration().orientation Configuration.ORIENTATION_LANDSCAPE; if (isTablet || isLandscape) { hotseatLayoutDirection HOTSEAT_LAYOUT_VERTICAL; } else { hotseatLayoutDirection HOTSEAT_LAYOUT_HORIZONTAL; }这只是我们项目的第一版策略如果你的产品要求平板竖屏也竖排或者只在特定宽高比下切换改这里的判断条件就行。关键是所有组件都从dp.hotseatLayoutDirection读值后面调整策略的成本非常低。3.3 改造Hotseat让getCellX/getCellY真正把图标“竖”起来在Hotseat类里维护一个mIsVerticalHotseat布尔值在onDeviceProfileChange回调里同步DeviceProfile的方向字段然后重写getCellX和getCellY。Override public int getCellX(int orderInHotseat) { if (mIsVerticalHotseat) { return 0; } return orderInHotseat; } Override public int getCellY(int orderInHotseat) { if (mIsVerticalHotseat) { return orderInHotseat; } return 0; }竖排模式下CellLayout仍然是一个二维网格只不过我们用到的区域窄成了一列。这里有一个容易踩的坑CellLayout默认行数可能只有1行纵向排布必须把行数扩出来否则getCellY超出网格范围会被判定成非法位置图标直接不显示。具体做法是在Hotseat的CellLayout初始化完成后调用网格设置接口把列数设为1、行数设为numHotseatIcons。不同Android版本里接口名称有差异但思路一致。这一步不做前面的getCellY写得再对也没用。3.4 配套处理宽高与Workspace偏移方向切了Hotseat自己的尺寸也要跟着切。竖排时它应该是一条窄栏而不是一根顶天立地的胖柱子。LayoutParams lp getLayoutParams(); if (mIsVerticalHotseat) { lp.width dp.hotseatBarWidth; lp.height ViewGroup.LayoutParams.MATCH_PARENT; } else { lp.width ViewGroup.LayoutParams.MATCH_PARENT; lp.height dp.hotseatBarHeight; } setLayoutParams(lp);这里的hotseatBarWidth和hotseatBarHeight是我在DeviceProfile里新增的尺寸参数从dimens读取。千万不要直接写死成一个dp值平板和手机的宽度差异很大写死必然出事。Workspace那边也要联动。横屏时左侧多出一条栏工作区的可用绘制区域要跟着缩窄。我们第一版实现里直接给Workspace的LayoutParams设置了marginStart等于hotseatBarWidth。更精细的做法是调整DeviceProfile里的可用网格列数让Workspace自身少画一列但那个改动波及范围大第一版先用margin方案简单有效。3.5 如何验证改造结果编译烧机之后第一件事不是拿起来点图标而是先把横竖屏来回切个十几遍观察几个关键点竖屏横排、横屏竖排切换能不能稳定触发图标在两种模式之间切换时有没有乱序第1个图标是不是始终在最上方或者最左侧从Workspace拖一个图标到Hotseat松手后的落点是不是正好落在最后一个空位把文件夹拖进竖排Hotseat时文件夹图标有没有变形、展开方向是否正常。还有一个很容易被忽略的点用adb shell wm size修改分辨率之后要重新测一遍。不同分辨率下DeviceProfile可能走不同分支hotseatLayoutDirection的赋值逻辑必须覆盖所有分支否则就会出现“改小分辨率后方向又变回去了”的灵异问题。4. 文本方向布局RTL语言下Hotseat为什么“反过来”了4.1 现象切到阿拉伯语图标顺序整个镜像项目联调阶段测试同学把系统语言切成阿拉伯语回头就提了一个bugHotseat图标顺序反了电话图标从最右跑到了最左。这不是我们改出来的bug是AOSP Launcher本身在RTL语言下的正常表现——整个UI镜像。但问题在于我们的定制目标里竖排Hotseat在RTL环境下也要保持合理的阅读顺序而不是简单粗暴地整体颠倒。这个现象如果不处理发布到中东市场就是事故。用户打开桌面一看图标顺序和认知习惯完全不一样会直接影响使用。4.2 根因layoutDirection的传递链路Android从4.2开始支持RTL布局核心机制是layoutDirection。系统语言切换后Configuration.getLayoutDirection()会返回LAYOUT_DIRECTION_RTL。Launcher作为一个普通Android应用它的DecorView会按这个方向做镜像绘制。对于ViewGroupisLayoutRtl()返回true之后XML里的paddingStart/End、gravity start/End、以及自定义onLayout里的start/End判断都会反转。Hotseat的getCellX默认实现没有主动做RTL处理所以它的镜像行为主要靠父容器把整个区域翻转而不是逐个图标重新排列。单独把Hotseat拿出来看图标顺序其实没变但屏幕上看就是从右往左了。理解这一层后面处理起来才有方向。4.3 三种可选适配策略与我的选择遇到RTL问题第一反应往往是强制Launcher始终LTR把android:supportsRtl关掉。这个方案我试过风险很大系统控件、状态栏、通知栏不会跟着你关桌面和系统UI方向不一致视觉上会非常撕裂用户会觉得这个系统是坏的。第二种方案只对Hotseat内部强制LTR比如给Hotseat的cellContainer单独设置setLayoutDirection(LTR)。这样图标顺序是稳住了但Hotseat左右两侧的箭头、文件夹展开动画、拖拽方向提示这些细节全会乱属于按下葫芦浮起瓢。第三种方案是顺应RTL在我们自己的布局逻辑里按isLayoutRtl()做镜像。我最终选的是第三种。虽然代码量稍微多一点但不会跟系统其他部分打架长期维护最稳。4.4 让自定义方向代码兼容RTL具体实现上竖排模式对RTL其实天然免疫因为只有一列没有左右之分。但横排模式下必须处理镜像反转。Override public int getCellX(int orderInHotseat) { if (mIsVerticalHotseat) { return 0; } if (isLayoutRtl()) { return getHotseatIconCount() - 1 - orderInHotseat; } return orderInHotseat; }注意这里用的是isLayoutRtl()而不是Configuration.getLayoutDirection()。因为View的layoutDirection会受父容器影响用View自身的方法判断才跟实际绘制方向一致。文本方向布局还影响一个不起眼但很重要的地方Hotseat左右两侧的箭头drawable在很多Launcher版本里声明了android:autoMirroredtrueRTL下会自动左右翻转。如果你们不用默认drawable、自己画了箭头一定要记得处理镜像或者沿用autoMirrored机制。不要以为图标顺序对了就算结束细节翻车往往就在这种地方。5. 真机验证时踩过的三个方向坑5.1 横竖屏切换后hotseat图标全部“飞”回默认位置第一次编译烧到设备上竖屏正常切横屏也正常但再从横屏切回竖屏的时候Hotseat图标全部横排恢复竖排状态丢了。排查后发现原因在生命周期Launcher在configuration变化时会重新创建DeviceProfileHotseat的onDeviceProfileChange拿到了新的方向字段但我在更新mIsVerticalHotseat之后没有调用requestLayout()导致View没有立即重新布局。状态字段变了UI没刷新看起来就像状态丢了。加上requestLayout()之后问题消失。这类问题建议一上来就在DeviceProfile变化回调里无脑请求layout先保证功能正确再谈优化别把优化放前面。5.2 从Workspace拖拽到Hotseat时落点永远差一格方向切换正常之后测试又发现一个新问题从Workspace拖一个图标到Hotseat松手后图标总是落在目标位置的右边一格。调查后发现Workspace的拖拽流程里计算Hotseat落点时走的是DropTarget接口。而Hotseat内部很多校验逻辑没有用我们重写过的getCellX/getCellY而是直接把orderInHotseat当成cellX来用。我们改完坐标映射后拖拽校验那段代码用的还是旧假设两边对不上自然错位。解决方式是把所有把orderInHotseat直接映射成横坐标的地方都找出来统一改成通过getCellX/getCellY取坐标。这种问题在代码审查阶段很难发现因为编译不会报错运行时只有拖拽才暴露。建议定制Hotseat坐标相关改动时重点review所有调用getCellX/getCellY的地方确认它们是走新逻辑还是老逻辑。5.3 锁屏唤醒后方向恢复异常一条排查链路这个坑最折腾现象是设备横屏使用锁屏再解锁Launcher回到竖屏布局但系统屏幕明明还是横屏。刚开始我怀疑是Android 13锁屏流程把Launcher的方向配置吃掉了。后来排查发现不是。具体排查链路是这样的先用adb shell dumpsys activity top查看Launcher顶部Activity的Configurationorientation显示横屏说明Launcher进程拿到的config没有问题在Launcher的onStart和onResume里打Log发现锁屏回来后Launcher没有走onConfigurationChanged但确实走了onResume继续查锁屏流程Android 13的锁屏由SystemUI的Keyguard处理Launcher进程只是被stop正常情况下不应该丢方向最后回到自己的代码里找发现是我们自己挖的坑AndroidManifest里给LauncherActivity写了android:screenOrientationportrait。这个属性和传感器方向在某些锁屏时序下会打架导致Launcher恢复时认为自己是竖屏。去掉这个属性、完全交给系统Configuration管理之后问题消失。这一趟下来我的教训是Launcher这类系统桌面应用尽量不要主动锁定方向方向变更最好跟着系统Configuration走。锁屏流程本身没有错是我们给它加了多余约束。5.4 调试方向问题的两个趁手工具方向类问题调试起来很烦因为真机旋转有传感器延迟变量太多。我常用的两个命令# 关闭自动旋转锁定当前传感器方向排除干扰 adb shell settings put system accelerometer_rotation 0 # 强制横屏user_rotation取值0/1/2/3对应四个方向 adb shell settings put system user_rotation 1先把自动旋转关掉方向逻辑稳定之后再开自动旋转回归。另一个习惯是在Hotseat的onLayout里打坐标日志把每个orderInHotseat对应的cellX/cellY打印出来肉眼对比横竖屏、RTL前后是否一致。定位问题非常快确认后记得删掉日志就行。方向这一层在项目里虽然只是001但后续一堆样式和交互改造都要踩在它上面。下一篇我准备写Hotseat图标间距和分组定制的实现到时候今天加的hotseatLayoutDirection字段还会接着用。真正做ROM定制久了就会发现这种基础字段和基础逻辑一旦设计好后面所有功能都是往这个框架里塞东西省心很多。
返回列表