前言
第一次拿着开发板,想研究一下Launcher3源码,第一次接触Launcher3,一步一步看看Launcher3 定制的话有哪些内容需要掌握的。
遇到的困难点
- 拿到源码从何看起,从何研究起。
- 源码无论在线的谷歌源码还是各大半导体厂商提供的Launcher3源码,存在一定的差别。
需要针对性看,特别是针对自己的开发板上面看尤其重要,不然对不上。 - 部分代码看不明白,布局和UI如何对上的。
- 源码到底在哪里,配置文件、布局文件到底是哪一个,对不上,琢磨实验好久。
针对性解决困难
- 多从广义角度、全局角度来看Launcher3,不要一下子钻进了牛角尖 出不来,毫无收获
- 多看一看网上在线的第三方博客、别人的总结、比人的分析,从中自己体会,理解
- 多打印日志,调试;在源码里面搜索关键字、关键文件路径
- 不管你是开发板或者公司产品,务必先针对一款源码熟悉、了解、分析,尽量不要好多平台源码一起看,源码部分架构不一样的,代码也不一样的。
- 多啃、多吃啃源码还是有必要的
需求
remind:初识Launcher首页布局是怎么加载的
首页页面是如何加载的,如何配置,如下图界面我想更改一下每个图标的位置,我想自定义这个界面如何实现?
比如如下看网上别人定制的桌面,蛮好看的,如下图:
相关资料
Launcher3 相关资料参考
菜鸟成长之路-源码分析专栏
Android Launcher3 简介
Launcher3 高端定制
Launcher3 开发
Launcher3 Android Code Search在线源码查看
Launcher3 xref 在线源码查看
Launcher3 RK 源码查看
Launcher3 解析
Launcher3 AndroidP AS版本
谷歌Launcher3 Android13源码修改
Launcher3 和 Launcher3QuickStep 区别
Android14 不分Launcher3修改
Launcher3 LoaderTask 的数据加载
Android14 浅析Launcher
Android O Launcher3-Workspace加载
Launcher3 源码 目录简单介绍
当拿到Launcher3 源码时候,对源码还是一脸懵逼, 用了这么久的手机,源码不熟悉 也正常,先有个大概了解
allapps 目录:主要存放主菜单界面相关代码。
anim目录:存放动画相关代码,主要是动画基类代码。
compat目录:主要存放解决兼容性相关的代码。
config目录:主要配置Launcher相关功能的宏开关,目前Launcher原生新增的功能宏开关都在这个目录。
dragndrop目录:主要存放拖拽相关操作的代码
graphics目录:主要存放处理图标大小、颜色、自适应等相关的代码
model目录:存放Launcher加载流程相关模块化的代码
notification目录:存放通知相关的代码
pageindicators目录:存放桌面页面指示器相关的代码
popu目录:存放长按图标显示弹出框相关的代码
provider目录:存放Launcher数据库相关的代码
qsb目录:存放搜索功能相关的代码
shortcuts目录:存放桌面所属应用某些功能的快捷图标相关的代码。
Launcher3 简介及页面布局分析
回归到需求,我们需要了解的是布局相关,那还是从整体架构来看看 Launcher3
UI整体架构
UI 架构是我们比较熟悉的内容,用了这么多年的手机,手机桌面部分不就是这些内容的嘛,从研发的角度讲我们可以和对应的名称 关联起来。
数据加载
数据加载是Launcher3中一个比较核心的内容,后续需要深入了解,下面给一下加载流程图,后续再继续分析,在看源码过程中提供一个源码查看方向,针对本文就此打住,需要了解 非深入研究部分
布局加载
本身我们通过Launcher3 找到主Activity,主Activity里面再找对应的layout 布局,launcher.xml,这些和上面的UI架构对应 对应的是模块,非本文核心问题。我们的核心问题是找桌面的那些Icon 文件夹 快捷 搜索栏 UI和布局及数据如何展现的,这些其实是配置或者在源码里面硬编码更改。
布局加载核心思想
我自己开发过程中,看 device_profiles.xml 时候,一脸懵,不清楚每个字段 比如 grid-option 、display-option、numRows、numColumns、numFolderRows、numFolderColumns、numHotseatIcons、minWidthDps、minHeightDps、iconImageSize、iconTextSize… 到底啥玩意 这么多配置,后来想一想见名知意。 就是显示的属性和配置呀,都是见名知意。
launcher:defaultLayoutId="@xml/default_workspace_5x5"
launcher:defaultLayoutId="@xml/default_workspace_4x4"
launcher:defaultLayoutId="@xml/default_workspace_3x3"
布局里面好多 grid-option 、display-option,到底用哪一个对应的 default_workspace_MxN
核心思想:
- 动态选择,更具屏幕大小分辨率动态适配加载选择
- 更具横竖屏动态选择
device_profiles.xml 加载
我们先给一个简单的流程图
device_profiles.xml 文件用于定义不同设备配置的布局,该文件是启动器根据设备的特性(如屏幕尺寸、分辨率、密度等)来适配布局和图标大小等元素的重要配置文件。
该文件的主要功能有定义网格布局、设置图标大小、配置热区、定义所有应用列表的布局、屏幕和设备类型特定配置、壁纸和背景设置、提供默认布局等。
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
<grid-option
launcher:name="3_by_3"
launcher:numRows="3"
launcher:numColumns="3"
launcher:numFolderRows="2"
launcher:numFolderColumns="3"
launcher:numHotseatIcons="3"
launcher:dbFile="launcher_3_by_3.db"
launcher:defaultLayoutId="@xml/default_workspace_3x3" >
<display-option
launcher:name="Super Short Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="300"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
<display-option
launcher:name="Shorter Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="400"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
</grid-option>
<grid-option
launcher:name="4_by_4"
launcher:numRows="4"
launcher:numColumns="4"
launcher:numFolderRows="3"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="4"
launcher:dbFile="launcher_4_by_4.db"
launcher:defaultLayoutId="@xml/default_workspace_4x4" >
<display-option
launcher:name="Short Stubby"
launcher:minWidthDps="275"
launcher:minHeightDps="420"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
<display-option
launcher:name="Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="450"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
<display-option
launcher:name="Nexus S"
launcher:minWidthDps="296"
launcher:minHeightDps="491.33"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
<display-option
launcher:name="Nexus 4"
launcher:minWidthDps="359"
launcher:minHeightDps="567"
launcher:iconImageSize="54"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
<display-option
launcher:name="Nexus 5"
launcher:minWidthDps="335"
launcher:minHeightDps="567"
launcher:iconImageSize="54"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
</grid-option>
<grid-option
launcher:name="5_by_5"
launcher:numRows="5"
launcher:numColumns="5"
launcher:numFolderRows="4"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="5"
launcher:dbFile="launcher.db"
launcher:defaultLayoutId="@xml/default_workspace_5x5" >
<display-option
launcher:name="Large Phone"
launcher:minWidthDps="406"
launcher:minHeightDps="694"
launcher:iconImageSize="56"
launcher:iconTextSize="14.4"
launcher:canBeDefault="true" />
<display-option
launcher:name="Large Phone Split Display"
launcher:minWidthDps="406"
launcher:minHeightDps="694"
launcher:iconImageSize="56"
launcher:iconTextSize="14.4"
launcher:canBeDefault="split_display" />
<display-option
launcher:name="Shorter Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="400"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:canBeDefault="true" />
</grid-option>
InvariantDeviceProfile
初始化地方
Launcher.java
InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
LauncherAppState.java
public InvariantDeviceProfile getInvariantDeviceProfile() {
return mInvariantDeviceProfile;
}
LauncherAppState 构造方法
mInvariantDeviceProfile = InvariantDeviceProfile.INSTANCE.get(context);
InvariantDeviceProfile.java 构造方法
@TargetApi(23)
private InvariantDeviceProfile(Context context) {
String gridName = getCurrentGridName(context);
String newGridName = initGrid(context, gridName);
....
}
initGrid(context, gridName)
大家这么想,为啥是Grid? init Grid 是做什么的。 其实桌面中WorkSpack 中的CellLayout, 不就是由网格组成的嘛, 然后给对应的坐标,告诉放到哪一个位置不就可以了嘛。 所以这个名字 Grid 是很有意义的。
//初始化网格
private String initGrid(Context context, String gridName) {
......
// getPredefinedDeviceProfiles 获取预定义的文件配置列表
ArrayList<DisplayOption> allOptions =
getPredefinedDeviceProfiles(context, gridName, isSplitDisplay);
DisplayOption displayOption =
invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay);
Log.d(TAG," initGrid gridName:"+displayOption.grid.name);
initGrid(context, displayInfo, displayOption, isSplitDisplay);
return displayOption.grid.name;
}
getPredefinedDeviceProfiles
getPredefinedDeviceProfiles 获取预定义的文件配置列表
private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(
Context context, String gridName, boolean isSplitDisplay) {
ArrayList<DisplayOption> profiles = new ArrayList<>();
Log.d(TAG,"getPredefinedDeviceProfiles huoqu yudingyi device list gridName:"+gridName);
try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles)) {
final int depth = parser.getDepth();
int type;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
Log.d(TAG,"getPredefinedDeviceProfiles GridOption.TAG_NAME:"+GridOption.TAG_NAME+" parser.getName:"+parser.getName());
if ((type == XmlPullParser.START_TAG)
&& GridOption.TAG_NAME.equals(parser.getName())) {
GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser));
final int displayDepth = parser.getDepth();
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > displayDepth)
&& type != XmlPullParser.END_DOCUMENT) {
if ((type == XmlPullParser.START_TAG) && "display-option".equals(
parser.getName())) {
Log.d(TAG,"getPredefinedDeviceProfiles display-option parser.getName:"+parser.getName());
profiles.add(new DisplayOption(gridOption, context,
Xml.asAttributeSet(parser),
isSplitDisplay ? DEFAULT_SPLIT_DISPLAY : DEFAULT_TRUE));
}
}
}
}
} catch (IOException|XmlPullParserException e) {
throw new RuntimeException(e);
}
ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
if (!TextUtils.isEmpty(gridName)) {
for (DisplayOption option : profiles) {
if (gridName.equals(option.grid.name)) {
filteredProfiles.add(option);
}
}
}
Log.d(TAG,"getPredefinedDeviceProfiles 000 profiles:"+profiles.size()+" filteredProfiles:"+filteredProfiles.size() );
if (filteredProfiles.isEmpty()) {
// No grid found, use the default options
for (DisplayOption option : profiles) {
if (option.canBeDefault) {
filteredProfiles.add(option);
}
}
}
Log.d(TAG,"getPredefinedDeviceProfiles 111 profiles:"+profiles.size()+" filteredProfiles:"+filteredProfiles.size() );
if (filteredProfiles.isEmpty()) {
throw new RuntimeException("No display option with canBeDefault=true");
}
return filteredProfiles;
}
这个方法比较核心,关注三点:
- 加载R.xml.device_profiles 文件,并解析
- DisplayOption类和device_profiles 里面的display-option
属性,不就对上了嘛,得到一个displayOption 配置列表 - 解析device_profiles,将grid-option 节点想的一级属性信息,封装在了DisplayOption,这样实现了
通过displayOption 能够获取 配置文件中上一层的grid 信息。 比如获取gridname,
也就是接下来要讲的MxN.xml
invDistWeightedInterpolate
接着上面的 getPredefinedDeviceProfiles 方法讲,这个方法返回的是List 集合
ArrayList<DisplayOption> getPredefinedDeviceProfiles
那么为什么会返回一个集合? 我们通过 上面分析 device_profiles.xml 配置文件中,grid-option 节点下对应的是多个display-option的。 比如如下日志,可以说明问题:
返回了DisplayOption 集合后,如何选择其一适合自己的呢? invDistWeightedInterpolate 就派上用场了
private static DisplayOption invDistWeightedInterpolate(
Info displayInfo, ArrayList<DisplayOption> points, boolean isSplitDisplay) {
int minWidthPx = Integer.MAX_VALUE;
int minHeightPx = Integer.MAX_VALUE;
for (WindowBounds bounds : displayInfo.supportedBounds) {
boolean isTablet = displayInfo.isTablet(bounds);
if (isTablet && isSplitDisplay) {
// For split displays, take half width per page
minWidthPx = Math.min(minWidthPx, bounds.availableSize.x / 2);
minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
} else if (!isTablet && bounds.isLandscape()) {
// We will use transposed layout in this case
minWidthPx = Math.min(minWidthPx, bounds.availableSize.y);
minHeightPx = Math.min(minHeightPx, bounds.availableSize.x);
} else {
minWidthPx = Math.min(minWidthPx, bounds.availableSize.x);
minHeightPx = Math.min(minHeightPx, bounds.availableSize.y);
}
}
float width = dpiFromPx(minWidthPx, displayInfo.densityDpi);
float height = dpiFromPx(minHeightPx, displayInfo.densityDpi);
// Sort the profiles based on the closeness to the device size
Collections.sort(points, (a, b) ->
Float.compare(dist(width, height, a.minWidthDps, a.minHeightDps),
dist(width, height, b.minWidthDps, b.minHeightDps)));
GridOption closestOption = points.get(0).grid;
float weights = 0;
DisplayOption p = points.get(0);
if (dist(width, height, p.minWidthDps, p.minHeightDps) == 0) {
return p;
}
DisplayOption out = new DisplayOption(closestOption);
for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
p = points.get(i);
float w = weight(width, height, p.minWidthDps, p.minHeightDps, WEIGHT_POWER);
weights += w;
out.add(new DisplayOption().add(p).multiply(w));
}
return out.multiply(1.0f / weights);
}
如则经过一系列的计算,来得到与当前屏幕可用宽高最为合适的各参数大小。具体算法这边就不细究了。
initGrid(Context context, Info displayInfo, DisplayOption displayOption,boolean isSplitDisplay)
得到了gridName,DisplayOption 不就可以获取得到 快捷方式、文件夹、图标等一些列的参数了嘛,且看 源码。
private void initGrid(
Context context, Info displayInfo, DisplayOption displayOption,
boolean isSplitDisplay) {
Log.d(TAG,"initGrid...");
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
GridOption closestProfile = displayOption.grid;
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
dbFile = closestProfile.dbFile;
defaultLayoutId = closestProfile.defaultLayoutId;
demoModeLayoutId = closestProfile.demoModeLayoutId;
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
isScalable = closestProfile.isScalable;
devicePaddingId = closestProfile.devicePaddingId;
mExtraAttrs = closestProfile.extraAttrs;
iconSize = displayOption.iconSize;
landscapeIconSize = displayOption.landscapeIconSize;
iconBitmapSize = ResourceUtils.pxFromDp(iconSize, metrics);
iconTextSize = displayOption.iconTextSize;
landscapeIconTextSize = displayOption.landscapeIconTextSize;
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
minCellHeight = displayOption.minCellHeight;
minCellWidth = displayOption.minCellWidth;
borderSpacing = displayOption.borderSpacing;
Log.d(TAG,"initGrid displayOption iconSize:"+iconSize+" landscapeIconSize:"+landscapeIconSize+" iconTextSize:"+iconTextSize+" landscapeIconTextSize:"+landscapeIconTextSize);
Log.d(TAG,"initGrid displayOption minCellHeight:"+minCellHeight+" minCellWidth:"+minCellWidth+" borderSpacing:"+borderSpacing+" ");
numShownHotseatIcons = closestProfile.numHotseatIcons;
numDatabaseHotseatIcons = isSplitDisplay
? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons;
numAllAppsColumns = closestProfile.numAllAppsColumns;
numDatabaseAllAppsColumns = isSplitDisplay
? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
Log.d(TAG,"initGrid closestProfile numRows:"+numRows+" numColumns:"+numColumns+" dbFile:"+dbFile+" defaultLayoutId:"+defaultLayoutId+" demoModeLayoutId:"+demoModeLayoutId);
Log.d(TAG,"initGrid closestProfile numFolderRows:"+numFolderRows+" numFolderColumns:"+numFolderColumns+" isScalable:"+isScalable+" devicePaddingId:"+devicePaddingId);
Log.d(TAG,"initGrid closestProfile numShownHotseatIcons:"+numShownHotseatIcons+" numDatabaseHotseatIcons:"+numDatabaseHotseatIcons);
if (Utilities.isGridOptionsEnabled(context)) {
allAppsIconSize = displayOption.allAppsIconSize;
allAppsIconTextSize = displayOption.allAppsIconTextSize;
} else {
allAppsIconSize = iconSize;
allAppsIconTextSize = iconTextSize;
}
if (devicePaddingId != 0) {
devicePaddings = new DevicePaddings(context, devicePaddingId);
}
// If the partner customization apk contains any grid overrides, apply them
// Supported overrides: numRows, numColumns, iconSize
applyPartnerDeviceProfileOverrides(context, metrics);
final List<DeviceProfile> localSupportedProfiles = new ArrayList<>();
defaultWallpaperSize = new Point(displayInfo.currentSize);
for (WindowBounds bounds : displayInfo.supportedBounds) {
localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
.setUseTwoPanels(isSplitDisplay)
.setWindowBounds(bounds).build());
// Wallpaper size should be the maximum of the all possible sizes Launcher expects
int displayWidth = bounds.bounds.width();
int displayHeight = bounds.bounds.height();
defaultWallpaperSize.y = Math.max(defaultWallpaperSize.y, displayHeight);
// We need to ensure that there is enough extra space in the wallpaper
// for the intended parallax effects
float parallaxFactor =
dpiFromPx(Math.min(displayWidth, displayHeight), displayInfo.densityDpi) < 720
? 2
: wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight);
defaultWallpaperSize.x =
Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth));
}
supportedProfiles = Collections.unmodifiableList(localSupportedProfiles);
ComponentName cn = new ComponentName(context.getPackageName(), getClass().getName());
defaultWidgetPadding = AppWidgetHostView.getDefaultPaddingForWidget(context, cn, null);
}
部分日志如下:
initGrid displayOption iconSize:55.69532 landscapeIconSize:55.69532 iconTextSize:14.34668 landscapeIconTextSize:14.34668
initGrid displayOption minCellHeight:0.0 minCellWidth:0.0 borderSpacing:0.0
initGrid closestProfile numRows:5 numColumns:5 dbFile:launcher.db defaultLayoutId:2131951619 demoModeLayoutId:2131951619
initGrid closestProfile numFolderRows:4 numFolderColumns:4 isScalable:false devicePaddingId:0
initGrid closestProfile numShownHotseatIcons:5 numDatabaseHotseatIcons:5
InvariantDeviceProfile gridName:5_by_5 newGridName:5_by_5
default_workspace_MxN.xml
经过上面的分析,其实已经找到了GridName,如当前 调试是5_by_5。获取后保存一份,下次获取。每次也要获取一份新的,在上面分析中 筛选DisPlayOption 里面
String newGridName = initGrid(context, gridName);
private String initGrid(Context context, String gridName) {
...
DisplayOption displayOption =
invDistWeightedInterpolate(displayInfo, allOptions, isSplitDisplay);
Log.d(TAG," initGrid gridName:"+displayOption.grid.name);
initGrid(context, displayInfo, displayOption, isSplitDisplay);
return displayOption.grid.name;
}
解析文件要解析道德其实是 launcher:defaultLayoutId="@xml/default_workspace_5x5"
defaultLayoutId 属性, 在initGrid 中解析到defaultLayoutId ,然后找到对应的布局
<favorites xmlns:launcher="http://schemas.android.com/apk/res-auto/com.android.launcher3">
<!-- Hotseat (We use the screen as the position of the item in the hotseat) -->
<!-- Dialer Messaging [All Apps] Contacts Camera -->
<favorite container="-101" screen="0" x="0" y="0" packageName="com.google.android.dialer" className="com.google.android.dialer.extensions.GoogleDialtactsActivity"/>
<favorite container="-101" screen="1" x="1" y="0" packageName="com.google.android.apps.messaging" className="com.google.android.apps.messaging.ui.ConversationListActivity"/>
<favorite container="-101" screen="2" x="2" y="0" packageName="com.google.android.calendar" className="com.android.calendar.event.LaunchInfoActivity"/>
<favorite container="-101" screen="3" x="3" y="0" packageName="com.google.android.contacts" className="com.android.contacts.activities.PeopleActivity"/>
<favorite container="-101" screen="4" x="4" y="0" packageName="com.mediatek.camera" className="com.mediatek.camera.CameraLauncher"/>
<!-- In Launcher3, workspaces extend infinitely to the right, incrementing from zero -->
<!-- Google folder -->
<!-- Google, Chrome, Gmail, Maps, YouTube, (Drive), (Music), (Movies), Hangouts, Photos -->
<folder title="@string/google_folder_title" screen="0" x="0" y="4">
<favorite packageName="com.google.android.googlequicksearchbox" className="com.google.android.googlequicksearchbox.SearchActivity"/>
<favorite packageName="com.android.chrome" className="com.google.android.apps.chrome.Main"/>
<favorite packageName="com.google.android.gm" className="com.google.android.gm.ConversationListActivityGmail"/>
<favorite packageName="com.google.android.apps.maps" className="com.google.android.maps.MapsActivity"/>
<favorite packageName="com.google.android.youtube" className="com.google.android.youtube.app.honeycomb.Shell$HomeActivity"/>
<favorite packageName="com.google.android.apps.docs" className="com.google.android.apps.docs.app.NewMainProxyActivity"/>
<favorite packageName="com.google.android.apps.youtube.music" className="com.google.android.apps.youtube.music.activities.MusicActivity"/>
<favorite packageName="com.google.android.videos" className="com.google.android.videos.GoogleTvEntryPoint"/>
<favorite packageName="com.google.android.apps.tachyon" className="com.google.android.apps.tachyon.MainActivity"/>
<favorite packageName="com.google.android.apps.photos" className="com.google.android.apps.photos.home.HomeActivity"/>
<favorite packageName="com.google.android.apps.adm" className="com.google.android.apps.adm.activities.MainActivity"/>
</folder>
<favorite screen="0" x="2" y="4" packageName="com.google.android.apps.googleassistant" className="com.google.android.apps.googleassistant.AssistantActivity"/>
<favorite screen="0" x="4" y="4" packageName="com.android.vending" className="com.android.vending.AssetBrowserActivity"/>
</favorites>
查找资源文件 default_workspace_MxN.xml 位置
找到 default_workspace_5x5.xml 在哪里呢? 我用的是mtk 平台,搜索文件名如下:
你会发现好多个呀,下面给出具体位置:
MTK 平台GMS版本
\vendor\google\overlay\gms_overlay\vendor\google\apps\SearchLauncher\res\xml\default_workspace_5x5.xml
MTK Launcher3 源码位置
我在MTK 平台上面测试验证,GMS 版本下:
Launcher3 源码存在两份,分别位于 package/app/和vendor/mediatek/proprietary/packages/apps/下,当前调试源码位置:
packages\apps\Launcher3
需求实现
上面我们已经找到了 default_workspace_5x5.xml ,首页的配置就在上面 ,我们更改配置即可,具体更改 每个 字段含有,见名知意的。 可以自己实验。
比如,我自己更改如下,实际效果如下
<favorite screen="0" x="2" y="4" packageName="com.google.android.apps.googleassistant" className="com.google.android.apps.googleassistant.AssistantActivity"/>