InvariantDeviceProfile
路径 packages/apps/Launcher3/src/com/android/launcher3/InvariantDeviceProfile.java
InvariantDeviceProfile
主要负责布局的加载和对应参数的保存,采用单例模式。
java
public static final MainThreadInitializedObject<InvariantDeviceProfile> INSTANCE =
new MainThreadInitializedObject<>(InvariantDeviceProfile::new);
@TargetApi(23)
private InvariantDeviceProfile(Context context) {
//去取出缓存的 gridName 这里初始化的时候 默认是null
String gridName = getCurrentGridName(context);
//我这里取出来是5_by_5
String newGridName = initGrid(context, gridName);
if (!newGridName.equals(gridName)) {
LauncherPrefs.getPrefs(context).edit().putString(KEY_IDP_GRID_NAME, newGridName)
.apply();
Log.d("b/258560494", "InvariantDeviceProfile - setting newGridName: " + newGridName
+ ", gridName: " + gridName);
}
new DeviceGridState(this).writeToPrefs(context);
//省略
}
打印了下日志
shell
2025-07-13 23:48:16.181 1477-1477 b/258560494 com.android.launcher3 D InvariantDeviceProfile - setting newGridName: 5_by_5, gridName: null
因为我们已经知道gridName
是个null
,我们直接进入initGrid(context, gridName);
java
private String initGrid(Context context, String gridName) {
Info displayInfo = DisplayController.INSTANCE.get(context).getInfo();
@DeviceType int deviceType = getDeviceType(displayInfo);
ArrayList<DisplayOption> allOptions =
getPredefinedDeviceProfiles(context, gridName, deviceType,
RestoreDbTask.isPending(context));
DisplayOption displayOption =
invDistWeightedInterpolate(displayInfo, allOptions, deviceType);
initGrid(context, displayInfo, displayOption, deviceType);
return displayOption.grid.name;
}
getDeviceType
根据当前设备信息 返回对应类型。
java
public static final int TYPE_PHONE = 0;
public static final int TYPE_MULTI_DISPLAY = 1;
public static final int TYPE_TABLET = 2;
public static final String TAG_NAME = "grid-option";
private static @DeviceType int getDeviceType(Info displayInfo) {
int flagPhone = 1 << 0;
int flagTablet = 1 << 1;
int type = displayInfo.supportedBounds.stream()
.mapToInt(bounds -> displayInfo.isTablet(bounds) ? flagTablet : flagPhone)
.reduce(0, (a, b) -> a | b);
if ((type == (flagPhone | flagTablet)) && ENABLE_TWO_PANEL_HOME.get()) {
// device has profiles supporting both phone and table modes
return TYPE_MULTI_DISPLAY;
} else if (type == flagTablet) {
return TYPE_TABLET;
} else {
return TYPE_PHONE;
}
}
- getPredefinedDeviceProfiles 该方法根据传入的类型,去
device_profiles.xml
下,获取 为grid-option
和display-option
和的,并返回,对应参数含义,放在文章最后。
java
private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
ArrayList<DisplayOption> profiles = new ArrayList<>();
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) {
if ((type == XmlPullParser.START_TAG)
&& GridOption.TAG_NAME.equals(parser.getName())) {
GridOption gridOption = new GridOption(context, Xml.asAttributeSet(parser),
deviceType);
if (gridOption.isEnabled || allowDisabledGrid) {
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())) {
profiles.add(new DisplayOption(gridOption, context,
Xml.asAttributeSet(parser)));
}
}
}
}
}
} catch (IOException | XmlPullParserException e) {
throw new RuntimeException(e);
}
ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
if (!TextUtils.isEmpty(gridName)) {
//第一次默认是null 这里就不会进入了
for (DisplayOption option : profiles) {
if (gridName.equals(option.grid.name)
&& (option.grid.isEnabled || allowDisabledGrid)) {
filteredProfiles.add(option);
}
}
}
if (filteredProfiles.isEmpty()) {
if (gridName != null) {
Log.d("b/258560494", "No matching grid from for gridName: " + gridName
+ ", deviceType: " + deviceType);
}
// No grid found, use the default options
for (DisplayOption option : profiles) {
if (option.canBeDefault) {
filteredProfiles.add(option);
}
}
}
if (filteredProfiles.isEmpty()) {
throw new RuntimeException("No display option with canBeDefault=true");
}
return filteredProfiles;
}
device_profiles.xml
xml
<grid-option
launcher:name="5_by_5"
launcher:numRows="5"
launcher:numColumns="6"
launcher:numFolderRows="4"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="5"
launcher:numExtendedHotseatIcons="6"
launcher:dbFile="launcher.db"
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
launcher:defaultLayoutId="@xml/default_workspace_5x5"
launcher:deviceCategory="phone|multi_display" >
<display-option
launcher:name="Large Phone"
launcher:minWidthDps="406"
launcher:minHeightDps="694"
launcher:iconImageSize="56"
launcher:iconTextSize="14.4"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
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:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
launcher:canBeDefault="true" />
<display-option
launcher:name="Shorter Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="400"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
launcher:canBeDefault="true" />
</grid-option>
java
public GridOption(Context context, AttributeSet attrs, @DeviceType int deviceType) {
TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.GridDisplayOption);
name = a.getString(R.styleable.GridDisplayOption_name);
numRows = a.getInt(R.styleable.GridDisplayOption_numRows, 0);
numColumns = a.getInt(R.styleable.GridDisplayOption_numColumns, 0);
numSearchContainerColumns = a.getInt(
R.styleable.GridDisplayOption_numSearchContainerColumns, numColumns);
dbFile = a.getString(R.styleable.GridDisplayOption_dbFile);
//省略
}
static final class DisplayOption {
public final GridOption grid;
private final float minWidthDps;
private final float minHeightDps;
private final boolean canBeDefault;
private final PointF[] minCellSize = new PointF[COUNT_SIZES];
private final PointF[] borderSpaces = new PointF[COUNT_SIZES];
private final float[] horizontalMargin = new float[COUNT_SIZES];
//省略
invDistWeightedInterpolate
可以简单理解为加权计算,调配出一套最合适当前屏幕的各种磁村,比如前面配置里 iconImageSize 为56dp,但是实际可能大点或者小点。
java
private static DisplayOption invDistWeightedInterpolate(
Info displayInfo, ArrayList<DisplayOption> points, @DeviceType int deviceType) {
int minWidthPx = Integer.MAX_VALUE;
int minHeightPx = Integer.MAX_VALUE;
for (WindowBounds bounds : displayInfo.supportedBounds) {
boolean isTablet = displayInfo.isTablet(bounds);
if (isTablet && deviceType == TYPE_MULTI_DISPLAY) {
// 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.getDensityDpi());
float height = dpiFromPx(minHeightPx, displayInfo.getDensityDpi());
// 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)));
DisplayOption closestPoint = points.get(0);
GridOption closestOption = closestPoint.grid;
float weights = 0;
if (dist(width, height, closestPoint.minWidthDps, closestPoint.minHeightDps) == 0) {
return closestPoint;
}
DisplayOption out = new DisplayOption(closestOption);
for (int i = 0; i < points.size() && i < KNEARESTNEIGHBOR; ++i) {
DisplayOption 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));
}
out.multiply(1.0f / weights);
// Since the bitmaps are persisted, ensure that all bitmap sizes are not larger than
// predefined size to avoid cache invalidation
for (int i = INDEX_DEFAULT; i < COUNT_SIZES; i++) {
out.iconSizes[i] = Math.min(out.iconSizes[i], closestPoint.iconSizes[i]);
}
return out;
}
initGrid
该方法主要是给 InvariantDeviceProfile 内部参数进行赋值,方便外部调用。
java
private void initGrid(Context context, Info displayInfo, DisplayOption displayOption,
@DeviceType int deviceType) {
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
GridOption closestProfile = displayOption.grid;
numRows = closestProfile.numRows;
numColumns = closestProfile.numColumns;
numSearchContainerColumns = closestProfile.numSearchContainerColumns;
dbFile = closestProfile.dbFile;
defaultLayoutId = closestProfile.defaultLayoutId;
demoModeLayoutId = closestProfile.demoModeLayoutId;
numFolderRows = closestProfile.numFolderRows;
numFolderColumns = closestProfile.numFolderColumns;
folderStyle = closestProfile.folderStyle;
//省略...
}
常见属性解析
-
launcher:name="5_by_5"
布局方案的名称("5x5"),标识这是一个 "5 行 5 列" 的桌面网格布局方案。 -
launcher:numRows="5"
桌面主页面的行数(垂直方向可显示的图标行数),此处为 5 行。 -
launcher:numColumns="5"
桌面主页面的列数(水平方向可显示的图标列数),此处为 5 列。 (注:numRows
和numColumns
共同决定桌面能容纳的图标总数,5x5 布局相比 3x3 能显示更多图标,适合大屏设备)。 -
launcher:numFolderRows="4"
桌面文件夹内部的行数(打开文件夹后,垂直方向可显示的图标行数)。 -
launcher:numFolderColumns="4"
桌面文件夹内部的列数(打开文件夹后,水平方向可显示的图标列数)。 (文件夹布局通常比桌面主布局更紧凑,4x4 适合在有限空间内显示更多应用)。 -
launcher:numHotseatIcons="5"
底部固定 Dock 栏(热座)可显示的图标数量,此处为 5 个(通常用于放置常用应用)。 -
launcher:numExtendedHotseatIcons="6"
扩展状态下 Dock 栏可显示的图标数量(可能用于大屏设备或横屏模式,允许放置更多常用应用)。 -
launcher:dbFile="launcher.db"
关联的数据库文件路径,用于存储该布局方案下的桌面图标位置、文件夹信息等用户配置(避免布局切换时丢失用户数据)。 -
launcher:defaultLayoutId="@xml/default_workspace_5x5"
默认桌面布局的 XML 资源路径,定义了首次使用该方案时,系统预装应用(如电话、短信)的默认位置。 -
launcher:deviceCategory="phone|multi_display"
该布局方案支持的设备类型:phone
:普通手机multi_display
:多屏设备(如折叠屏、外接显示器的设备)。
-
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
桌面底部导航按钮(如返回、主页键)与 Dock 栏图标的间距,引用了 dimens 资源中定义的具体数值,确保不同屏幕尺寸下的间距一致。