Andorid14 浅析Launcher(代码+图文并茂版)
一、Launcher简介及页面布局分析
launcher一般将其称之为 Android 系统的桌面,提供主屏幕和应用启动功能的应用程序。
launcher不仅显示了用户安装的应用图标,还常常提供一些快捷方式、小部件(Widget)、以及提供搜索框等额外功能。
launcher允许用户添加各种小部件到主屏幕,这些小部件可以提供快速访问信息或应用功能,例如天气、日历事件或新闻摘要等。
launche通常包括一个应用抽屉,用户可以通过滑动手势或点击特定按钮来打开整个应用列表,也可以通过搜索功能找到应用程序。
launcher支持各种手势操作,例如长按图标可以显示应用信息,拖放图标可以重新排列它们或者将其图标移除或者将软件卸载。
1、整体框架
Launcher.xml
<com.android.launcher3.LauncherRootView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
android:id="@+id/launcher"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.android.launcher3.dragndrop.DragLayer
android:id="@+id/drag_layer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:importantForAccessibility="no">
<com.android.launcher3.views.AccessibilityActionsView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/home_screen"
/>
<!-- The workspace contains 5 screens of cells -->
<!-- DO NOT CHANGE THE ID -->
<com.android.launcher3.Workspace
android:id="@+id/workspace"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:theme="@style/HomeScreenElementTheme"
launcher:pageIndicator="@+id/page_indicator" />
<!-- DO NOT CHANGE THE ID -->
<include
android:id="@+id/hotseat"
layout="@layout/hotseat" />
<!-- Keep these behind the workspace so that they are not visible when
we go into AllApps -->
<com.android.launcher3.pageindicators.WorkspacePageIndicator
android:id="@+id/page_indicator"
android:layout_width="match_parent"
android:layout_height="@dimen/workspace_page_indicator_height"
android:layout_gravity="bottom|center_horizontal"
android:theme="@style/HomeScreenElementTheme" />
<include
android:id="@+id/drop_target_bar"
layout="@layout/drop_target_bar" />
<com.android.launcher3.views.ScrimView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/scrim_view"
android:background="@android:color/transparent" />
<include
android:id="@+id/overview_panel"
layout="@layout/overview_panel" />
<include
android:id="@+id/apps_view"
layout="@layout/all_apps"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.android.launcher3.dragndrop.DragLayer>
</com.android.launcher3.LauncherRootView>
2、device_profiles.xml加载
先用流程图的方式将大致的加载过程进行描述:
device_profiles.xml 文件用于定义不同设备配置的布局,该文件是启动器根据设备的特性(如屏幕尺寸、分辨率、密度等)来适配布局和图标大小等元素的重要配置文件。
该文件的主要功能有定义网格布局、设置图标大小、配置热区、定义所有应用列表的布局、屏幕和设备类型特定配置、壁纸和背景设置、提供默认布局等。
Launcher.java
super.onCreate(savedInstanceState);
LauncherAppState app = LauncherAppState.getInstance(this);
mModel = app.getModel();
mRotationHelper = new RotationHelper(this);
InvariantDeviceProfile idp = app.getInvariantDeviceProfile();
initDeviceProfile(idp);
InvariantDeviceProfile.java
@TargetApi(23)
private InvariantDeviceProfile(Context context) {
String gridName = getCurrentGridName(context);
String newGridName = initGrid(context, gridName);
if (!newGridName.equals(gridName)) {
LauncherPrefs.get(context).put(GRID_NAME, newGridName);
}
LockedUserState.get(context).runOnUserUnlocked(() -> {
new DeviceGridState(this).writeToPrefs(context);
});
DisplayController.INSTANCE.get(context).setPriorityListener(
(displayContext, info, flags) -> {
if ((flags & (CHANGE_DENSITY | CHANGE_SUPPORTED_BOUNDS
| CHANGE_NAVIGATION_MODE)) != 0) {
onConfigChanged(displayContext);
}
});
}
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;
}
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;
cellStyle = closestProfile.cellStyle;
isScalable = closestProfile.isScalable;
devicePaddingId = closestProfile.devicePaddingId;
workspaceSpecsId = closestProfile.mWorkspaceSpecsId;
this.deviceType = deviceType;
inlineNavButtonsEndSpacing = closestProfile.inlineNavButtonsEndSpacing;
iconSize = displayOption.iconSizes;
float maxIconSize = iconSize[0];
for (int i = 1; i < iconSize.length; i++) {
maxIconSize = Math.max(maxIconSize, iconSize[i]);
}
iconBitmapSize = ResourceUtils.pxFromDp(maxIconSize, metrics);
fillResIconDpi = getLauncherIconDensity(iconBitmapSize);
iconTextSize = displayOption.textSizes;
minCellSize = displayOption.minCellSize;
borderSpaces = displayOption.borderSpaces;
horizontalMargin = displayOption.horizontalMargin;
numShownHotseatIcons = closestProfile.numHotseatIcons;
numDatabaseHotseatIcons = deviceType == TYPE_MULTI_DISPLAY
? closestProfile.numDatabaseHotseatIcons : closestProfile.numHotseatIcons;
hotseatColumnSpan = closestProfile.hotseatColumnSpan;
hotseatBarBottomSpace = displayOption.hotseatBarBottomSpace;
hotseatQsbSpace = displayOption.hotseatQsbSpace;
allAppsStyle = closestProfile.allAppsStyle;
numAllAppsColumns = closestProfile.numAllAppsColumns;
numDatabaseAllAppsColumns = deviceType == TYPE_MULTI_DISPLAY
? closestProfile.numDatabaseAllAppsColumns : closestProfile.numAllAppsColumns;
allAppsCellSize = displayOption.allAppsCellSize;
allAppsBorderSpaces = displayOption.allAppsBorderSpaces;
allAppsIconSize = displayOption.allAppsIconSizes;
allAppsIconTextSize = displayOption.allAppsIconTextSizes;
inlineQsb = closestProfile.inlineQsb;
transientTaskbarIconSize = displayOption.transientTaskbarIconSize;
startAlignTaskbar = displayOption.startAlignTaskbar;
// 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);
SparseArray<DotRenderer> dotRendererCache = new SparseArray<>();
for (WindowBounds bounds : displayInfo.supportedBounds) {
localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
.setIsMultiDisplay(deviceType == TYPE_MULTI_DISPLAY)
.setWindowBounds(bounds)
.setDotRendererCache(dotRendererCache)
.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.getDensityDpi())
< 720
? 2
: wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight);
defaultWallpaperSize.x =
Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth));
}
supportedProfiles = Collections.unmodifiableList(localSupportedProfiles);
int numMinShownHotseatIconsForTablet = supportedProfiles
.stream()
.filter(deviceProfile -> deviceProfile.isTablet)
.mapToInt(deviceProfile -> deviceProfile.numShownHotseatIcons)
.min()
.orElse(0);
supportedProfiles
.stream()
.filter(deviceProfile -> deviceProfile.isTablet)
.forEach(deviceProfile -> {
deviceProfile.numShownHotseatIcons = numMinShownHotseatIconsForTablet;
deviceProfile.recalculateHotseatWidthAndBorderSpace();
});
}
上面这个四参数initGrid()方法用来真正的设置网格属性、文件夹属性、图标和文本大小、最小单元格大小和边距。设置热区(hotseat)图标的数量、列跨度、底部空间、Qsb空间。
private static ArrayList<DisplayOption> getPredefinedDeviceProfiles(Context context,
String gridName, @DeviceType int deviceType, boolean allowDisabledGrid) {
ArrayList<DisplayOption> profiles = new ArrayList<>();
//change hotseat for WL non gms
if(!WL){
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));
if (gridOption.isEnabled(deviceType) || 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);
}
}else{
try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles_wl)) {
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));
if (gridOption.isEnabled(deviceType) || 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);
}
}
//change hotseat for WL non gms
ArrayList<DisplayOption> filteredProfiles = new ArrayList<>();
if (!TextUtils.isEmpty(gridName)) {
for (DisplayOption option : profiles) {
if (gridName.equals(option.grid.name)
&& (option.grid.isEnabled(deviceType) || allowDisabledGrid)) {
filteredProfiles.add(option);
}
}
}
if (filteredProfiles.isEmpty()) {
// 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;
}
上面代码中使用try (XmlResourceParser parser = context.getResources().getXml(R.xml.device_profiles))这行代码进行解析XML资源文件
Launcher.java
protected boolean initDeviceProfile(InvariantDeviceProfile idp) {
// Load configuration-specific DeviceProfile
DeviceProfile deviceProfile = idp.getDeviceProfile(this);
if (mDeviceProfile == deviceProfile) {
return false;
}
mDeviceProfile = deviceProfile;
if (isInMultiWindowMode()) {
mDeviceProfile = mDeviceProfile.getMultiWindowProfile(
this, getMultiWindowDisplaySize());
}
onDeviceProfileInitiated();
if (FOLDABLE_SINGLE_PAGE.get() && mDeviceProfile.isTwoPanels) {
mCellPosMapper = new TwoPanelCellPosMapper(mDeviceProfile.inv.numColumns);
} else {
mCellPosMapper = CellPosMapper.DEFAULT;
}
mModelWriter = mModel.getWriter(getDeviceProfile().isVerticalBarLayout(), true,
mCellPosMapper, this);
return true;
}
InvariantDeviceProfile.java
public DeviceProfile getDeviceProfile(Context context) {
Resources res = context.getResources();
Configuration config = context.getResources().getConfiguration();
float screenWidth = config.screenWidthDp * res.getDisplayMetrics().density;
float screenHeight = config.screenHeightDp * res.getDisplayMetrics().density;
int rotation = WindowManagerProxy.INSTANCE.get(context).getRotation(context);
return getBestMatch(screenWidth, screenHeight, rotation);
}
获取与当前设备屏幕配置最匹配的 DeviceProfile 对象
getBestMatch找到与给定屏幕尺寸和旋转角度最匹配的 DeviceProfile 对象
InvariantDeviceProfile.java (扩充:定位在initGrid()四参数方法中)
final List<DeviceProfile> localSupportedProfiles = new ArrayList<>();
defaultWallpaperSize = new Point(displayInfo.currentSize);
SparseArray<DotRenderer> dotRendererCache = new SparseArray<>();
for (WindowBounds bounds : displayInfo.supportedBounds) {
localSupportedProfiles.add(new DeviceProfile.Builder(context, this, displayInfo)
.setIsMultiDisplay(deviceType == TYPE_MULTI_DISPLAY)
.setWindowBounds(bounds)
.setDotRendererCache(dotRendererCache)
.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.getDensityDpi())
< 720
? 2
: wallpaperTravelToScreenWidthRatio(displayWidth, displayHeight);
defaultWallpaperSize.x =
Math.max(defaultWallpaperSize.x, Math.round(parallaxFactor * displayWidth));
}
创建存放DeviceProfile类型的列表,是因为DeviceProfile中有多个不同的配置,一套配置可以视为一个DeviceProfile对象。
InvariantDeviceProfile.java
/**
* Returns the device profile matching the provided screen configuration
*/
public DeviceProfile getBestMatch(float screenWidth, float screenHeight, int rotation) {
DeviceProfile bestMatch = supportedProfiles.get(0);
float minDiff = Float.MAX_VALUE;
for (DeviceProfile profile : supportedProfiles) {
float diff = Math.abs(profile.widthPx - screenWidth)
+ Math.abs(profile.heightPx - screenHeight);
if (diff < minDiff) {
minDiff = diff;
bestMatch = profile;
} else if (diff == minDiff && profile.rotationHint == rotation) {
bestMatch = profile;
}
}
return bestMatch;
}
遍历supportedProfiles列表,通过计算屏幕尺寸差异来找到最接近当前屏幕配置的设备配置文件。
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"
launcher:deviceCategory="phone" >
<display-option
launcher:name="Super Short Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="300"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
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>
<grid-option
launcher:name="4_by_4"
launcher:numRows="4"
launcher:numColumns="4"
launcher:numFolderRows="3"
launcher:numFolderColumns="4"
launcher:numHotseatIcons="4"
launcher:numExtendedHotseatIcons="6"
launcher:dbFile="launcher_4_by_4.db"
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_split"
launcher:defaultLayoutId="@xml/default_workspace_4x4"
launcher:deviceCategory="phone|multi_display" >
<display-option
launcher:name="Short Stubby"
launcher:minWidthDps="275"
launcher:minHeightDps="420"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
launcher:canBeDefault="true" />
<display-option
launcher:name="Stubby"
launcher:minWidthDps="255"
launcher:minHeightDps="450"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
launcher:canBeDefault="true" />
<display-option
launcher:name="Nexus S"
launcher:minWidthDps="296"
launcher:minHeightDps="491.33"
launcher:iconImageSize="48"
launcher:iconTextSize="13.0"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
launcher:canBeDefault="true" />
<display-option
launcher:name="Nexus 4"
launcher:minWidthDps="359"
launcher:minHeightDps="567"
launcher:iconImageSize="54"
launcher:iconTextSize="13.0"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
launcher:canBeDefault="true" />
<display-option
launcher:name="Nexus 5"
launcher:minWidthDps="335"
launcher:minHeightDps="567"
launcher:iconImageSize="54"
launcher:iconTextSize="13.0"
launcher:allAppsBorderSpace="16"
launcher:allAppsCellHeight="104"
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: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>
<grid-option
launcher:name="6_by_5"
launcher:numRows="5"
launcher:numColumns="6"
launcher:numSearchContainerColumns="3"
launcher:numFolderRows="3"
launcher:numFolderColumns="3"
launcher:numHotseatIcons="6"
launcher:hotseatColumnSpanLandscape="4"
launcher:numAllAppsColumns="6"
launcher:isScalable="true"
launcher:inlineNavButtonsEndSpacing="@dimen/taskbar_button_margin_6_5"
launcher:devicePaddingId="@xml/paddings_6x5"
launcher:dbFile="launcher_6_by_5.db"
launcher:defaultLayoutId="@xml/default_workspace_6x5"
launcher:deviceCategory="tablet" >
<display-option
launcher:name="Tablet"
launcher:minWidthDps="900"
launcher:minHeightDps="820"
launcher:minCellHeight="120"
launcher:minCellWidth="102"
launcher:minCellHeightLandscape="104"
launcher:minCellWidthLandscape="120"
launcher:iconImageSize="60"
launcher:iconTextSize="14"
launcher:borderSpaceHorizontal="16"
launcher:borderSpaceVertical="64"
launcher:borderSpaceLandscapeHorizontal="64"
launcher:borderSpaceLandscapeVertical="16"
launcher:horizontalMargin="54"
launcher:horizontalMarginLandscape="120"
launcher:allAppsCellWidth="96"
launcher:allAppsCellHeight="142"
launcher:allAppsCellWidthLandscape="126"
launcher:allAppsCellHeightLandscape="126"
launcher:allAppsIconSize="60"
launcher:allAppsIconTextSize="14"
launcher:allAppsBorderSpaceHorizontal="8"
launcher:allAppsBorderSpaceVertical="16"
launcher:allAppsBorderSpaceLandscape="16"
launcher:hotseatBarBottomSpace="76"
launcher:hotseatBarBottomSpaceLandscape="40"
launcher:canBeDefault="true" />
</grid-option>
</profiles>
<grid-option>定义了一个网格选项:
定义了网格的名称、行数、列数。
定义了文件夹在网格中占用的行数、列数。
指定了Hotseat区域中的图标数量。
指定了与此网格配置相关联的数据库文件名。
指定启动器的默认布局资源ID,指向一个XML布局文件。
指定此网格配置适用于"phone"类别的设备。
<display-option>定义了显示选项,可以有多个,每个都有不同的属性集,用于定义不同屏幕大小或密度的特定显示设置。
定义了显示选项的名称,指定了此显示选项适用的最小屏幕宽度、高度。
指定了图标图像、文本的大小,指定了所有应用界面的边框空间大小。
指定了所有应用界面的单元格高度。指示了此显示选项可以作为默认选项。
3、default_workspace_MxN.xml
以default_workspace_5x5.xml进行举例:
<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, [Maps/Music], Browser, Camera -->
<resolve
launcher:container="-101"
launcher:screen="0"
launcher:x="0"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.DIAL;end" />
<favorite launcher:uri="tel:123" />
<favorite launcher:uri="#Intent;action=android.intent.action.CALL_BUTTON;end" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="1"
launcher:x="1"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MESSAGING;end" />
<favorite launcher:uri="sms:" />
<favorite launcher:uri="smsto:" />
<favorite launcher:uri="mms:" />
<favorite launcher:uri="mmsto:" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="2"
launcher:x="2"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MAPS;end" />
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MUSIC;end" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="3"
launcher:x="3"
launcher:y="0" >
<favorite
launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_BROWSER;end" />
<favorite launcher:uri="http://www.example.com/" />
</resolve>
<resolve
launcher:container="-101"
launcher:screen="4"
launcher:x="4"
launcher:y="0" >
<favorite launcher:uri="#Intent;action=android.media.action.STILL_IMAGE_CAMERA;end" />
<favorite launcher:uri="#Intent;action=android.intent.action.CAMERA_BUTTON;end" />
</resolve>
<!-- Bottom row -->
<resolve
launcher:screen="0"
launcher:x="0"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_EMAIL;end" />
<favorite launcher:uri="mailto:" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="1"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_GALLERY;end" />
<favorite launcher:uri="#Intent;type=images/*;end" />
</resolve>
<resolve
launcher:screen="0"
launcher:x="4"
launcher:y="-1" >
<favorite launcher:uri="#Intent;action=android.intent.action.MAIN;category=android.intent.category.APP_MARKET;end" />
<favorite launcher:uri="market://details?id=com.android.launcher" />
</resolve>
</favorites>
该XML配置文件允许Launcher将特定的应用和快捷方式放置在用户定义的位置上。每个元素都可以启动一个特定的Intent,Intent是Android中用于启动活动、服务或传递操作请求的一种机制。通过这种方式,用户可以快速访问最常用的应用,如电话、短信、浏览器、相机等。
<resolve>支持的子标签如下:
favorite //应用程序快捷方式
widget //桌面控件(小组件)
shortcut //链接,如网址、本地磁盘路径等
search //搜索框(谷歌搜索框)
clock //桌面上的钟表Widget
folder //桌面文件夹(如谷歌应用的文件夹)
同时<resolve>标签或其子标签通过launcher:XXX来设定快捷方式项的位置等信息,
这些属性可以写在<resolve>标签或其子标签中,支持的属性如下:
// resolve标签支持的属性
launcher:title // 图标下面的文字,目前只支持引用,不能直接书写字符串;
launcher:icon // 图标引用(适用于应用快捷方式);
launcher:uri // 链接地址,链接网址用的,使用shortcut标签就可以定义一个超链接,打开某个网址,文件等。
launcher:packageName // 应用程序的包名;
launcher:className // 应用程序的启动类名(要写全路径);
launcher:screen // 图标所在的屏幕编号,0表示第一页
launcher:x // 应用图标所处x位置(从左到右,从0开始),(-1是默认值:第一行或者第一列)
launcher:y // 应用图标所处y位置(从上往下,从0开始)
launcher:container // 定义一个快捷方式(Favorite)或桌面项目应该放置在哪个容器中;-101表示HotSeat、-100表示DeskTop、0表示默认的桌面容器、其他正整数表示App shortcut
launcher:spanX //在x方向上所占格数
launcher:spanY //在y方向上所占格数
二、Launcher数据加载分析
1、开机流程—Launcher的启动
BootLoader:启动加载器是设备启动过程的第一阶段,它初始化硬件并加载操作系统内核。
Linux Kernel:内核是操作系统的核心,负责管理系统资源,如内存、处理器和设备驱动程序。
init:init 是系统启动后运行的第一个用户空间进程(进程ID为1)。它负责启动系统上的其他进程和服务。
Zygote:Zygote是Android中的一个特殊进程,它启动后会加载Java核心库,并为其他应用进程提供服务。它通过fork操作来创建新的应用进程。
SystemServer:系统服务器是Android中一个关键的守护进程,它启动了Android系统的核心服务,如窗口管理器、活动管理器、包管理器等。
ActivityManagerService:活动管理器服务负责管理应用程序的活动(Activity)生命周期,并处理应用程序的启动和切换
大概流程可以总结如下:
设备开机,BootLoader启动。
加载Linux内核。
内核初始化后,启动init进程。
init进程启动Zygote进程。
Zygote进程启动后,会加载核心库并准备创建应用进程。
系统服务器(SystemServer)启动,它将启动一系列系统服务。
随着系统服务的启动,包括安装器、活动管理器、包管理器等在内的服务开始运行。
启动器(Launcher)启动,提供用户界面。
2、数据加载分析
1、首先通过调用getInstance()方法来创建LauncherAppState的实例
2、然后进一步获取LauncherModel对象,处理所有与应用相关的数据。
3、addCallbacksAndLoad()方法首先通过addCallbacks()将启动器自身注册为回调对象,确保在数据加载完成后能够接收更新通知。
4、执行startLoader(),启动后台加载过程,以异步方式查询并加载应用数据。
5、调用clearPendingBinds方法,清空回调列表中所有待处理的绑定操作,确保了在开始新的加载之前,所有旧的、未完成的任务都被清除。
6、终止任何正在进行的加载任务,这一措施保证了在启动新的加载任务前,系统资源得到释放,避免了潜在的资源竞争或状态冲突。
7、通过post方法将mLoaderTask置入任务队列。LoaderTask实现了Runnable接口,因此,一旦执行,将会触发其run方法。
8、紧随其后,AllApps、DeepShortcuts和Widgets也会按顺序进行加载和绑定。
Launcher.java
LauncherAppState app = LauncherAppState.getInstance(this);
mModel = app.getModel();
if (!mModel.addCallbacksAndLoad(this)) {
if (!internalStateHandled) {
// If we are not binding synchronously, pause drawing until initial bind complete,
// so that the system could continue to show the device loading prompt
mOnInitialBindListener = Boolean.FALSE::booleanValue;
}
}
LauncherModel.java
/**
* Adds a callbacks to receive model updates
* @return true if workspace load was performed synchronously
*/
public boolean addCallbacksAndLoad(@NonNull final Callbacks callbacks) {
synchronized (mLock) {
addCallbacks(callbacks);
return startLoader(new Callbacks[] { callbacks });
}
}
/**
* Adds a callbacks to receive model updates
*/
public void addCallbacks(@NonNull final Callbacks callbacks) {
Preconditions.assertUIThread();
synchronized (mCallbacksList) {
mCallbacksList.add(callbacks);
}
}
private boolean startLoader(@NonNull final Callbacks[] newCallbacks) {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
ItemInstallQueue.INSTANCE.get(mApp.getContext())
.pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING);
synchronized (mLock) {
// If there is already one running, tell it to stop.
boolean wasRunning = stopLoader();
boolean bindDirectly = mModelLoaded && !mIsLoaderTaskRunning;
boolean bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.length == 0;
final Callbacks[] callbacksList = bindAllCallbacks ? getCallbacks() : newCallbacks;
if (callbacksList.length > 0) {
// Clear any pending bind-runnables from the synchronized load process.
for (Callbacks cb : callbacksList) {
MAIN_EXECUTOR.execute(cb::clearPendingBinds);
}
LauncherBinder launcherBinder = new LauncherBinder(
mApp, mBgDataModel, mBgAllAppsList, callbacksList);
if (bindDirectly) {
// Divide the set of loaded items into those that we are binding synchronously,
// and everything else that is to be bound normally (asynchronously).
launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true);
// For now, continue posting the binding of AllApps as there are other
// issues that arise from that.
launcherBinder.bindAllApps();
launcherBinder.bindDeepShortcuts();
launcherBinder.bindWidgets();
if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mModelDelegate.bindAllModelExtras(callbacksList);
}
return true;
} else {
stopLoader();
mLoaderTask = new LoaderTask(
mApp, mBgAllAppsList, mBgDataModel, mModelDelegate, launcherBinder);
// Always post the loader task, instead of running directly
// (even on same thread) so that we exit any nested synchronized blocks
MODEL_EXECUTOR.post(mLoaderTask);
}
}
}
return false;
}
该方法是加载程序的启动,在启动过程中并对于数据进行尝试同步绑定,若能够进行绑定则可以返回true,在实现的过程中调用的是工作线程LoaderTask
bindDirectly变量确定是否可以在当前线程直接绑定数据,而不是异步加载。
bindAllCallbacks变量根据当前加载器是否正在运行、是否可以直接绑定以及传入的回调数组长度来决定是否绑定所有回调。
根据bindAllCallbacks的值,决定使用现有的回调列表或传入的newCallbacks。
首次启动时,模型数据还没有被加载。或者已经有一个加载任务在运行。为了避免同时启动多个加载任务,bindDirectly 将被设置为 false
如果执行了直接绑定或成功启动了异步加载任务,方法将返回true。如果没有执行任何操作(例如,回调列表为空),则返回false。
最后的post将加载任务提交到后台执行队列。这一步骤是启动异步加载任务的关键。
LoaderTask.java
public void run() {
synchronized (this) {
// Skip fast if we are already stopped.
if (mStopped) {
return;
}
}
Object traceToken = TraceHelper.INSTANCE.beginSection(TAG);
LoaderMemoryLogger memoryLogger = new LoaderMemoryLogger();
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
loadWorkspace(allShortcuts, "", memoryLogger);
// Sanitize data re-syncs widgets/shortcuts based on the workspace loaded from db.
// sanitizeData should not be invoked if the workspace is loaded from a db different
// from the main db as defined in the invariant device profile.
// (e.g. both grid preview and minimal device mode uses a different db)
if (mApp.getInvariantDeviceProfile().dbFile.equals(mDbName)) {
verifyNotStopped();
sanitizeFolders(mItemsDeleted);
sanitizeWidgetsShortcutsAndPackages();
logASplit("sanitizeData");
}
verifyNotStopped();
mLauncherBinder.bindWorkspace(true /* incrementBindId */, /* isBindSync= */ false);
logASplit("bindWorkspace");
mModelDelegate.workspaceLoadComplete();
// Notify the installer packages of packages with active installs on the first screen.
sendFirstScreenActiveInstallsBroadcast();
logASplit("sendFirstScreenActiveInstallsBroadcast");
// Take a break
waitForIdle();
logASplit("step 1 complete");
verifyNotStopped();
// second step
Trace.beginSection("LoadAllApps");
List<LauncherActivityInfo> allActivityList;
try {
allActivityList = loadAllApps();
} finally {
Trace.endSection();
}
logASplit("loadAllApps");
if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
logASplit("allAppsDelegateItems");
}
verifyNotStopped();
mLauncherBinder.bindAllApps();
logASplit("bindAllApps");
verifyNotStopped();
IconCacheUpdateHandler updateHandler = mIconCache.getUpdateHandler();
setIgnorePackages(updateHandler);
updateHandler.updateIcons(allActivityList,
LauncherActivityCachingLogic.newInstance(mApp.getContext()),
mApp.getModel()::onPackageIconsUpdated);
logASplit("update icon cache");
verifyNotStopped();
logASplit("save shortcuts in icon cache");
updateHandler.updateIcons(allShortcuts, new ShortcutCachingLogic(),
mApp.getModel()::onPackageIconsUpdated);
// Take a break
waitForIdle();
logASplit("step 2 complete");
verifyNotStopped();
// third step
List<ShortcutInfo> allDeepShortcuts = loadDeepShortcuts();
logASplit("loadDeepShortcuts");
verifyNotStopped();
mLauncherBinder.bindDeepShortcuts();
logASplit("bindDeepShortcuts");
verifyNotStopped();
logASplit("save deep shortcuts in icon cache");
updateHandler.updateIcons(allDeepShortcuts,
new ShortcutCachingLogic(), (pkgs, user) -> { });
// Take a break
waitForIdle();
logASplit("step 3 complete");
verifyNotStopped();
// fourth step
List<ComponentWithLabelAndIcon> allWidgetsList =
mBgDataModel.widgetsModel.update(mApp, null);
logASplit("load widgets");
verifyNotStopped();
mLauncherBinder.bindWidgets();
logASplit("bindWidgets");
verifyNotStopped();
if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
logASplit("otherDelegateItems");
verifyNotStopped();
}
updateHandler.updateIcons(allWidgetsList,
new ComponentWithIconCachingLogic(mApp.getContext(), true),
mApp.getModel()::onWidgetLabelsUpdated);
logASplit("save widgets in icon cache");
// fifth step
loadFolderNames();
verifyNotStopped();
updateHandler.finish();
logASplit("finish icon update");
mModelDelegate.modelLoadComplete();
transaction.commit();
memoryLogger.clearLogs();
} catch (CancellationException e) {
// Loader stopped, ignore
logASplit("Cancelled");
} catch (Exception e) {
memoryLogger.printLogs();
throw e;
}
TraceHelper.INSTANCE.endSection(traceToken);
}
loadWorkspace() 和 bindWorkspace(),也就是加载 workspace 的应用并且进行绑定,waitForIdle() 方法主要是等待加载数据结束。sendFirstScreenActiveInstallsBroadcast() 发送首屏广播。loadAllApps() 和 bindAllApps() 加载并绑定所有的 APP 信息,loadDeepShortcuts() 和bindDeepShortcuts ()加载并绑定所有的快捷方式,然后加载并绑定所有的小部件。至此launcher数据加载基本就完成了。
三、workspace加载以及bind分析
1、workspace加载
LoaderTask.java
protected void loadWorkspace(
List<ShortcutInfo> allDeepShortcuts,
String selection,
LoaderMemoryLogger memoryLogger) {
Trace.beginSection("LoadWorkspace");
try {
loadWorkspaceImpl(allDeepShortcuts, selection, memoryLogger);
} finally {
Trace.endSection();
}
logASplit("loadWorkspace");
if (FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
verifyNotStopped();
mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
mModelDelegate.markActive();
logASplit("workspaceDelegateItems");
}
}
最后那个if语句的作用是:检查一个特性标志,这个标志用于控制是否采用新的加载顺序。如果特性标志为真,则调用 mModelDelegate 的方法来加载和绑定工作区项目。
上面的代码是在公共方法中封装具体的实现逻辑
private void loadWorkspaceImpl(
List<ShortcutInfo> allDeepShortcuts,
String selection,
@Nullable LoaderMemoryLogger memoryLogger) {
final Context context = mApp.getContext();
final ContentResolver contentResolver = context.getContentResolver();
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
final boolean isSafeMode = pmHelper.isSafeMode();
final boolean isSdCardReady = Utilities.isBootCompleted();
final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
boolean clearDb = false;
if (!mApp.getModel().getModelDbController().migrateGridIfNeeded()) {
// Migration failed. Clear workspace.
clearDb = true;
}
if (clearDb) {
Log.d(TAG, "loadWorkspace: resetting launcher database");
Settings.call(contentResolver, Settings.METHOD_CREATE_EMPTY_DB);
}
Log.d(TAG, "loadWorkspace: loading default favorites");
Settings.call(contentResolver, Settings.METHOD_LOAD_DEFAULT_FAVORITES);
synchronized (mBgDataModel) {
......
......
}
......
......
}
LauncherSettings.java
public static Bundle call(ContentResolver cr, String method) {
return call(cr, method, null /* arg */);
}
public static Bundle call(ContentResolver cr, String method, String arg) {
return cr.call(CONTENT_URI, method, arg, null);
}
LauncherProvider.java
@Override
public Bundle call(String method, final String arg, final Bundle extras) {
if (Binder.getCallingUid() != Process.myUid()) {
return null;
}
switch (method) {
case LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG: {
getModelDbController().clearEmptyDbFlag();
return null;
}
case LauncherSettings.Settings.METHOD_DELETE_EMPTY_FOLDERS: {
Bundle result = new Bundle();
result.putIntArray(LauncherSettings.Settings.EXTRA_VALUE,
getModelDbController().deleteEmptyFolders().toArray());
return result;
}
case LauncherSettings.Settings.METHOD_NEW_ITEM_ID: {
Bundle result = new Bundle();
result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
getModelDbController().generateNewItemId());
return result;
}
case LauncherSettings.Settings.METHOD_NEW_SCREEN_ID: {
Bundle result = new Bundle();
result.putInt(LauncherSettings.Settings.EXTRA_VALUE,
getModelDbController().getNewScreenId());
return result;
}
case LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB: {
getModelDbController().createEmptyDB();
return null;
}
case LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES: {
getModelDbController().loadDefaultFavoritesIfNecessary();
return null;
}
case LauncherSettings.Settings.METHOD_REMOVE_GHOST_WIDGETS: {
getModelDbController().removeGhostWidgets();
return null;
}
case LauncherSettings.Settings.METHOD_NEW_TRANSACTION: {
Bundle result = new Bundle();
result.putBinder(LauncherSettings.Settings.EXTRA_VALUE,
getModelDbController().newTransaction());
return result;
}
case LauncherSettings.Settings.METHOD_REFRESH_HOTSEAT_RESTORE_TABLE: {
getModelDbController().refreshHotseatRestoreTable();
return null;
}
}
return null;
}
在 loadWorkspace() 的开始实际进行的第一个操作是:判断是否有桌面布局数据库,从而好读取数据。如果没有用户布局数据则采用 loadDefaultFavoritesIfNecessary() 方法。实际上没有用户布局数据的场景就是第一次创建数据库的场景。所以loadDefaultFavoritesIfNecessary() 的含义是读取默认布局,仅在首次开机,恢复出厂设置或清除 Launcher 数据的时候使用。
ModelDbController.java
/**
* Loads the default workspace based on the following priority scheme:
* 1) From the app restrictions
* 2) From a package provided by play store
* 3) From a partner configuration APK, already in the system image
* 4) The default configuration for the particular device
*/
@WorkerThread
public synchronized void loadDefaultFavoritesIfNecessary() {
createDbIfNotExists();
if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()))) {
Log.d(TAG, "loading default workspace");
LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
try {
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder);
if (loader == null) {
loader = AutoInstallsLayout.get(mContext, widgetHolder, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(mContext.getPackageManager());
if (partner != null) {
//change hotseat for WL gms
boolean WL = SystemProperties.get("ro.unc.rf","").equals("WL");
int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT);
if(WL){
workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT_WL);
}
//change hotseat for WL gms
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(mContext, widgetHolder,
mOpenHelper, partner.getResources(), workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser(widgetHolder);
}
// There might be some partially restored DB items, due to buggy restore logic in
// previous versions of launcher.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
// Populate favorites table with initial favorites
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
// Unable to load external layout. Cleanup and load the internal layout.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHolder));
}
clearFlagEmptyDbCreated();
} finally {
widgetHolder.destroy();
}
}
}
private synchronized void createDbIfNotExists() {
if (mOpenHelper == null) {
mOpenHelper = createDatabaseHelper(false /* forMigration */);
RestoreDbTask.restoreIfNeeded(mContext, this);
}
}
protected DatabaseHelper createDatabaseHelper(boolean forMigration) {
boolean isSandbox = mContext instanceof SandboxContext;
String dbName = isSandbox ? null : InvariantDeviceProfile.INSTANCE.get(mContext).dbFile;
// Set the flag for empty DB
Runnable onEmptyDbCreateCallback = forMigration ? () -> { }
: () -> LauncherPrefs.get(mContext).putSync(getEmptyDbCreatedKey(dbName).to(true));
DatabaseHelper databaseHelper = new DatabaseHelper(mContext, dbName,
this::getSerialNumberForUser, onEmptyDbCreateCallback);
// Table creation sometimes fails silently, which leads to a crash loop.
// This way, we will try to create a table every time after crash, so the device
// would eventually be able to recover.
if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) {
Log.e(TAG, "Tables are missing after onCreate has been called. Trying to recreate");
// This operation is a no-op if the table already exists.
addTableToDb(databaseHelper.getWritableDatabase(),
getSerialNumberForUser(Process.myUserHandle()),
true /* optional */);
}
databaseHelper.mHotseatRestoreTableExists = tableExists(
databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE);
databaseHelper.initIds();
return databaseHelper;
}
LauncherSettings.java
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional) {
addTableToDb(db, myProfileId, optional, TABLE_NAME);
}
public static void addTableToDb(SQLiteDatabase db, long myProfileId, boolean optional,
String tableName) {
String ifNotExists = optional ? " IF NOT EXISTS " : "";
db.execSQL("CREATE TABLE " + ifNotExists + tableName + " (" +
"_id INTEGER PRIMARY KEY," +
"title TEXT," +
"intent TEXT," +
"container INTEGER," +
"screen INTEGER," +
"cellX INTEGER," +
"cellY INTEGER," +
"spanX INTEGER," +
"spanY INTEGER," +
"itemType INTEGER," +
"appWidgetId INTEGER NOT NULL DEFAULT -1," +
"icon BLOB," +
"appWidgetProvider TEXT," +
"modified INTEGER NOT NULL DEFAULT 0," +
"restored INTEGER NOT NULL DEFAULT 0," +
"profileId INTEGER DEFAULT " + myProfileId + "," +
"rank INTEGER NOT NULL DEFAULT 0," +
"options INTEGER NOT NULL DEFAULT 0," +
APPWIDGET_SOURCE + " INTEGER NOT NULL DEFAULT " + CONTAINER_UNKNOWN +
");");
}
}
这里解释一些重要数据库的含义:
Container:判断属于当前图标属于哪里:包括文件夹、workspace 和 hotseat。
其中如果图标属于文件夹则,图标的 container 值就是其 id 值。
Intent:点击的时候启动的目标。
cellX 和cellY:图标起始于第几行第几列。
spanX 和spanY:widget占据格子数。
==itemType ==:区分具体类型。类型包括,图标,文件夹,widget等
loadDefaultFavoritesIfNecessary() 方法的作用为:获取 loader (布局),和将读取的布局存入数据库。
获取布局的主要方法是上面那1、2、3、4这四种。
存储布局的主要方法是:loadFavorites()。
ModelDbController.java
/**
* Loads the default workspace based on the following priority scheme:
* 1) From the app restrictions
* 2) From a package provided by play store
* 3) From a partner configuration APK, already in the system image
* 4) The default configuration for the particular device
*/
@WorkerThread
public synchronized void loadDefaultFavoritesIfNecessary() {
createDbIfNotExists();
if (LauncherPrefs.get(mContext).get(getEmptyDbCreatedKey(mOpenHelper.getDatabaseName()))) {
Log.d(TAG, "loading default workspace");
LauncherWidgetHolder widgetHolder = mOpenHelper.newLauncherWidgetHolder();
try {
AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHolder);
if (loader == null) {
loader = AutoInstallsLayout.get(mContext, widgetHolder, mOpenHelper);
}
if (loader == null) {
final Partner partner = Partner.get(mContext.getPackageManager());
if (partner != null) {
//change hotseat for WL gms
boolean WL = SystemProperties.get("ro.unc.rf","").equals("WL");
int workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT);
if(WL){
workspaceResId = partner.getXmlResId(RES_PARTNER_DEFAULT_LAYOUT_WL);
}
//change hotseat for WL gms
if (workspaceResId != 0) {
loader = new DefaultLayoutParser(mContext, widgetHolder,
mOpenHelper, partner.getResources(), workspaceResId);
}
}
}
final boolean usingExternallyProvidedLayout = loader != null;
if (loader == null) {
loader = getDefaultLayoutParser(widgetHolder);
}
// There might be some partially restored DB items, due to buggy restore logic in
// previous versions of launcher.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
// Populate favorites table with initial favorites
if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0)
&& usingExternallyProvidedLayout) {
// Unable to load external layout. Cleanup and load the internal layout.
mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase());
mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(),
getDefaultLayoutParser(widgetHolder));
}
clearFlagEmptyDbCreated();
} finally {
widgetHolder.destroy();
}
}
}
DatabaseHelper.java
public int loadFavorites(SQLiteDatabase db, AutoInstallsLayout loader) {
// TODO: Use multiple loaders with fall-back and transaction.
int count = loader.loadLayout(db, new IntArray());
// Ensure that the max ids are initialized
mMaxItemId = initializeMaxItemId(db);
return count;
}
AutoInstallsLayout.java
/**
* Loads the layout in the db and returns the number of entries added on the desktop.
*/
public int loadLayout(SQLiteDatabase db, IntArray screenIds) {
mDb = db;
try {
return parseLayout(mInitialLayoutSupplier.get(), screenIds);
} catch (Exception e) {
Log.e(TAG, "Error parsing layout: ", e);
return -1;
}
}
/**
* Parses the layout and returns the number of elements added on the homescreen.
*/
protected int parseLayout(XmlPullParser parser, IntArray screenIds)
throws XmlPullParserException, IOException {
beginDocument(parser, mRootTag);
final int depth = parser.getDepth();
int type;
ArrayMap<String, TagParser> tagParserMap = getLayoutElementsMap();
int count = 0;
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
count += parseAndAddNode(parser, tagParserMap, screenIds);
}
return count;
}
/**
* Parses the current node and returns the number of elements added.
*/
protected int parseAndAddNode(
XmlPullParser parser, ArrayMap<String, TagParser> tagParserMap, IntArray screenIds)
throws XmlPullParserException, IOException {
if (TAG_INCLUDE.equals(parser.getName())) {
final int resId = getAttributeResourceValue(parser, ATTR_WORKSPACE, 0);
if (resId != 0) {
// recursively load some more favorites, why not?
return parseLayout(mSourceRes.getXml(resId), screenIds);
} else {
return 0;
}
}
mValues.clear();
parseContainerAndScreen(parser, mTemp);
final int container = mTemp[0];
final int screenId = mTemp[1];
mValues.put(Favorites.CONTAINER, container);
mValues.put(Favorites.SCREEN, screenId);
mValues.put(Favorites.CELLX,
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_X), mColumnCount));
mValues.put(Favorites.CELLY,
convertToDistanceFromEnd(getAttributeValue(parser, ATTR_Y), mRowCount));
TagParser tagParser = tagParserMap.get(parser.getName());
if (tagParser == null) {
if (LOGD) Log.d(TAG, "Ignoring unknown element tag: " + parser.getName());
return 0;
}
int newElementId = tagParser.parseAndAdd(parser);
if (newElementId >= 0) {
// Keep track of the set of screens which need to be added to the db.
if (!screenIds.contains(screenId) &&
container == Favorites.CONTAINER_DESKTOP) {
screenIds.add(screenId);
}
return 1;
}
return 0;
}
回到LoaderTask.java中的loadWorkspaceImpl()方法中继续查看读取数据库的相关操作。
private void loadWorkspaceImpl(
List<ShortcutInfo> allDeepShortcuts,
String selection,
@Nullable LoaderMemoryLogger memoryLogger) {
final Context context = mApp.getContext();
final ContentResolver contentResolver = context.getContentResolver();
final PackageManagerHelper pmHelper = new PackageManagerHelper(context);
final boolean isSafeMode = pmHelper.isSafeMode();
final boolean isSdCardReady = Utilities.isBootCompleted();
final WidgetManagerHelper widgetHelper = new WidgetManagerHelper(context);
boolean clearDb = false;
if (!mApp.getModel().getModelDbController().migrateGridIfNeeded()) {
// Migration failed. Clear workspace.
clearDb = true;
}
if (clearDb) {
Log.d(TAG, "loadWorkspace: resetting launcher database");
Settings.call(contentResolver, Settings.METHOD_CREATE_EMPTY_DB);
}
Log.d(TAG, "loadWorkspace: loading default favorites");
Settings.call(contentResolver, Settings.METHOD_LOAD_DEFAULT_FAVORITES);
synchronized (mBgDataModel) {
mBgDataModel.clear();
mPendingPackages.clear();
final HashMap<PackageUserKey, SessionInfo> installingPkgs =
mSessionHelper.getActiveSessions();
installingPkgs.forEach(mApp.getIconCache()::updateSessionCache);
final PackageUserKey tempPackageKey = new PackageUserKey(null, null);
mFirstScreenBroadcast = new FirstScreenBroadcast(installingPkgs);
mShortcutKeyToPinnedShortcuts = new HashMap<>();
ModelDbController dbController = mApp.getModel().getModelDbController();
final LoaderCursor c = new LoaderCursor(
dbController.query(TABLE_NAME, null, selection, null, null),
mApp, mUserManagerState);
final Bundle extras = c.getExtras();
mDbName = extras == null ? null : extras.getString(Settings.EXTRA_DB_NAME);
try {
final LongSparseArray<Boolean> unlockedUsers = new LongSparseArray<>();
mUserManagerState.init(mUserCache, mUserManager);
for (UserHandle user : mUserCache.getUserProfiles()) {
long serialNo = mUserCache.getSerialNumberForUser(user);
boolean userUnlocked = mUserManager.isUserUnlocked(user);
// We can only query for shortcuts when the user is unlocked.
if (userUnlocked) {
QueryResult pinnedShortcuts = new ShortcutRequest(context, user)
.query(ShortcutRequest.PINNED);
if (pinnedShortcuts.wasSuccess()) {
for (ShortcutInfo shortcut : pinnedShortcuts) {
mShortcutKeyToPinnedShortcuts.put(ShortcutKey.fromInfo(shortcut),
shortcut);
}
} else {
// Shortcut manager can fail due to some race condition when the
// lock state changes too frequently. For the purpose of the loading
// shortcuts, consider the user is still locked.
userUnlocked = false;
}
}
unlockedUsers.put(serialNo, userUnlocked);
}
List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos = new ArrayList<>();
while (!mStopped && c.moveToNext()) {
processWorkspaceItem(c, memoryLogger, installingPkgs, isSdCardReady,
tempPackageKey, widgetHelper, pmHelper,
iconRequestInfos, unlockedUsers, isSafeMode, allDeepShortcuts);
}
tryLoadWorkspaceIconsInBulk(iconRequestInfos);
} finally {
IOUtils.closeSilently(c);
}
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mModelDelegate.loadAndBindWorkspaceItems(mUserManagerState,
mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
mModelDelegate.loadAndBindAllAppsItems(mUserManagerState,
mLauncherBinder.mCallbacksList, mShortcutKeyToPinnedShortcuts);
mModelDelegate.loadAndBindOtherItems(mLauncherBinder.mCallbacksList);
mModelDelegate.markActive();
}
// Break early if we've stopped loading
if (mStopped) {
mBgDataModel.clear();
return;
}
// Remove dead items
mItemsDeleted = c.commitDeleted();
// Sort the folder items, update ranks, and make sure all preview items are high res.
FolderGridOrganizer verifier =
new FolderGridOrganizer(mApp.getInvariantDeviceProfile());
for (FolderInfo folder : mBgDataModel.folders) {
Collections.sort(folder.contents, Folder.ITEM_POS_COMPARATOR);
verifier.setFolderInfo(folder);
int size = folder.contents.size();
// Update ranks here to ensure there are no gaps caused by removed folder items.
// Ranks are the source of truth for folder items, so cellX and cellY can be ignored
// for now. Database will be updated once user manually modifies folder.
for (int rank = 0; rank < size; ++rank) {
WorkspaceItemInfo info = folder.contents.get(rank);
info.rank = rank;
if (info.usingLowResIcon()
&& info.itemType == Favorites.ITEM_TYPE_APPLICATION
&& verifier.isItemInPreview(info.rank)) {
mIconCache.getTitleAndIcon(info, false);
}
}
}
c.commitRestoredItems();
}
}
c.moveToNext(): 将游标移动到下一条记录。如果游标已经到达末尾或者说接收到了停止信号,则停止循环。
调用processWorkspaceItem方法来处理当前游标指向的工作区项目。
c: 当前游标。
memoryLogger: 用于记录内存使用情况的日志器。
installingPkgs: 活跃的安装包会话映射。
isSdCardReady: 表示SD卡是否就绪的布尔值。
tempPackageKey: 临时的包键,用于某些比较或查找操作。
widgetHelper: 用于处理小部件的帮助类。
pmHelper: 与包管理器(PackageManager)交互的帮助类。
iconRequestInfos: 存储图标请求信息的列表。
unlockedUsers: 存储已解锁用户的映射。
isSafeMode: 表示设备是否处于安全模式的布尔值。
allDeepShortcuts: 表示所有深度快捷方式的集合或映射。
private void processWorkspaceItem(LoaderCursor c,
LoaderMemoryLogger memoryLogger,
HashMap<PackageUserKey, SessionInfo> installingPkgs,
boolean isSdCardReady,
PackageUserKey tempPackageKey,
WidgetManagerHelper widgetHelper,
PackageManagerHelper pmHelper,
List<IconRequestInfo<WorkspaceItemInfo>> iconRequestInfos,
LongSparseArray<Boolean> unlockedUsers,
boolean isSafeMode,
List<ShortcutInfo> allDeepShortcuts) {
try {
if (c.user == null) {
// User has been deleted, remove the item.
c.markDeleted("User has been deleted");
return;
}
boolean allowMissingTarget = false;
switch (c.itemType) {
case Favorites.ITEM_TYPE_SHORTCUT:
case Favorites.ITEM_TYPE_APPLICATION:
case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
Intent intent = c.parseIntent();
if (intent == null) {
c.markDeleted("Invalid or null intent");
return;
}
int disabledState = mUserManagerState.isUserQuiet(c.serialNumber)
? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0;
ComponentName cn = intent.getComponent();
String targetPkg = cn == null ? intent.getPackage() : cn.getPackageName();
if (TextUtils.isEmpty(targetPkg)
&& c.itemType != Favorites.ITEM_TYPE_SHORTCUT) {
c.markDeleted("Only legacy shortcuts can have null package");
return;
}
// If there is no target package, it's an implicit intent
// (legacy shortcut) which is always valid
boolean validTarget = TextUtils.isEmpty(targetPkg)
|| mLauncherApps.isPackageEnabled(targetPkg, c.user);
// If it's a deep shortcut, we'll use pinned shortcuts to restore it
if (cn != null && validTarget && c.itemType
!= Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// If the apk is present and the shortcut points to a specific component.
// If the component is already present
if (mLauncherApps.isActivityEnabled(cn, c.user)) {
// no special handling necessary for this item
c.markRestored();
} else {
// Gracefully try to find a fallback activity.
intent = pmHelper.getAppLaunchIntent(targetPkg, c.user);
if (intent != null) {
c.restoreFlag = 0;
c.updater().put(
Favorites.INTENT,
intent.toUri(0)).commit();
cn = intent.getComponent();
} else {
c.markDeleted("Unable to find a launch target");
return;
}
}
}
// else if cn == null => can't infer much, leave it
// else if !validPkg => could be restored icon or missing sd-card
if (!TextUtils.isEmpty(targetPkg) && !validTarget) {
// Points to a valid app (superset of cn != null) but the apk
// is not available.
if (c.restoreFlag != 0) {
// Package is not yet available but might be
// installed later.
FileLog.d(TAG, "package not yet restored: " + targetPkg);
tempPackageKey.update(targetPkg, c.user);
if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) {
// Restore has started once.
} else if (installingPkgs.containsKey(tempPackageKey)) {
// App restore has started. Update the flag
c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED;
c.updater().put(Favorites.RESTORED,
c.restoreFlag).commit();
} else {
c.markDeleted("Unrestored app removed: " + targetPkg);
return;
}
} else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) {
// Package is present but not available.
disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE;
// Add the icon on the workspace anyway.
allowMissingTarget = true;
} else if (!isSdCardReady) {
// SdCard is not ready yet. Package might get available,
// once it is ready.
Log.d(TAG, "Missing pkg, will check later: " + targetPkg);
mPendingPackages.add(new PackageUserKey(targetPkg, c.user));
// Add the icon on the workspace anyway.
allowMissingTarget = true;
} else {
// Do not wait for external media load anymore.
c.markDeleted("Invalid package removed: " + targetPkg);
return;
}
}
if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0) {
validTarget = false;
}
if (validTarget) {
// The shortcut points to a valid target (either no target
// or something which is ready to be used)
c.markRestored();
}
boolean useLowResIcon = !c.isOnWorkspaceOrHotseat();
WorkspaceItemInfo info;
if (c.restoreFlag != 0) {
// Already verified above that user is same as default user
info = c.getRestoredItemInfo(intent);
} else if (c.itemType == Favorites.ITEM_TYPE_APPLICATION) {
info = c.getAppShortcutInfo(
intent, allowMissingTarget, useLowResIcon, false);
} else if (c.itemType == Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
ShortcutKey key = ShortcutKey.fromIntent(intent, c.user);
if (unlockedUsers.get(c.serialNumber)) {
ShortcutInfo pinnedShortcut = mShortcutKeyToPinnedShortcuts.get(key);
if (pinnedShortcut == null) {
// The shortcut is no longer valid.
c.markDeleted("Pinned shortcut not found");
return;
}
info = new WorkspaceItemInfo(pinnedShortcut, mApp.getContext());
// If the pinned deep shortcut is no longer published,
// use the last saved icon instead of the default.
mIconCache.getShortcutIcon(info, pinnedShortcut, c::loadIcon);
if (pmHelper.isAppSuspended(
pinnedShortcut.getPackage(), info.user)) {
info.runtimeStatusFlags |= FLAG_DISABLED_SUSPENDED;
}
intent = info.getIntent();
allDeepShortcuts.add(pinnedShortcut);
} else {
// Create a shortcut info in disabled mode for now.
info = c.loadSimpleWorkspaceItem();
info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER;
}
} else { // item type == ITEM_TYPE_SHORTCUT
info = c.loadSimpleWorkspaceItem();
// Shortcuts are only available on the primary profile
if (!TextUtils.isEmpty(targetPkg)
&& pmHelper.isAppSuspended(targetPkg, c.user)) {
disabledState |= FLAG_DISABLED_SUSPENDED;
}
info.options = c.getOptions();
// App shortcuts that used to be automatically added to Launcher
// didn't always have the correct intent flags set, so do that here
if (intent.getAction() != null
&& intent.getCategories() != null
&& intent.getAction().equals(Intent.ACTION_MAIN)
&& intent.getCategories().contains(Intent.CATEGORY_LAUNCHER)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}
}
if (info != null) {
if (info.itemType != Favorites.ITEM_TYPE_DEEP_SHORTCUT) {
// Skip deep shortcuts; their title and icons have already been
// loaded above.
iconRequestInfos.add(c.createIconRequestInfo(info, useLowResIcon));
}
c.applyCommonProperties(info);
info.intent = intent;
info.rank = c.getRank();
info.spanX = 1;
info.spanY = 1;
info.runtimeStatusFlags |= disabledState;
if (isSafeMode && !isSystemApp(mApp.getContext(), intent)) {
info.runtimeStatusFlags |= FLAG_DISABLED_SAFEMODE;
}
LauncherActivityInfo activityInfo = c.getLauncherActivityInfo();
if (activityInfo != null) {
info.setProgressLevel(
PackageManagerHelper.getLoadingProgress(activityInfo),
PackageInstallInfo.STATUS_INSTALLED_DOWNLOADING);
}
if (c.restoreFlag != 0 && !TextUtils.isEmpty(targetPkg)) {
tempPackageKey.update(targetPkg, c.user);
SessionInfo si = installingPkgs.get(tempPackageKey);
if (si == null) {
info.runtimeStatusFlags
&= ~ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE;
} else if (activityInfo == null) {
int installProgress = (int) (si.getProgress() * 100);
info.setProgressLevel(installProgress,
PackageInstallInfo.STATUS_INSTALLING);
}
}
c.checkAndAddItem(info, mBgDataModel, memoryLogger);
} else {
throw new RuntimeException("Unexpected null WorkspaceItemInfo");
}
break;
case Favorites.ITEM_TYPE_FOLDER:
FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id);
c.applyCommonProperties(folderInfo);
// Do not trim the folder label, as is was set by the user.
folderInfo.title = c.getString(c.mTitleIndex);
folderInfo.spanX = 1;
folderInfo.spanY = 1;
folderInfo.options = c.getOptions();
// no special handling required for restored folders
c.markRestored();
c.checkAndAddItem(folderInfo, mBgDataModel, memoryLogger);
break;
case Favorites.ITEM_TYPE_APPWIDGET:
if (WidgetsModel.GO_DISABLE_WIDGETS) {
c.markDeleted("Only legacy shortcuts can have null package");
return;
}
// Follow through
case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
// Read all Launcher-specific widget details
boolean customWidget = c.itemType
== Favorites.ITEM_TYPE_CUSTOM_APPWIDGET;
int appWidgetId = c.getAppWidgetId();
String savedProvider = c.getAppWidgetProvider();
final ComponentName component;
if ((c.getOptions() & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0) {
component = QsbContainerView.getSearchComponentName(mApp.getContext());
if (component == null) {
c.markDeleted("Discarding SearchWidget without packagename ");
return;
}
} else {
component = ComponentName.unflattenFromString(savedProvider);
}
final boolean isIdValid =
!c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_ID_NOT_VALID);
final boolean wasProviderReady =
!c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY);
ComponentKey providerKey = new ComponentKey(component, c.user);
if (!mWidgetProvidersMap.containsKey(providerKey)) {
mWidgetProvidersMap.put(providerKey,
widgetHelper.findProvider(component, c.user));
}
final AppWidgetProviderInfo provider = mWidgetProvidersMap.get(providerKey);
final boolean isProviderReady = isValidProvider(provider);
if (!isSafeMode && !customWidget && wasProviderReady && !isProviderReady) {
c.markDeleted("Deleting widget that isn't installed anymore: " + provider);
} else {
LauncherAppWidgetInfo appWidgetInfo;
if (isProviderReady) {
appWidgetInfo =
new LauncherAppWidgetInfo(appWidgetId, provider.provider);
// The provider is available. So the widget is either
// available or not available. We do not need to track
// any future restore updates.
int status = c.restoreFlag
& ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED
& ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY;
if (!wasProviderReady) {
// If provider was not previously ready, update status and UI flag.
// Id would be valid only if the widget restore broadcast received.
if (isIdValid) {
status |= LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
}
}
appWidgetInfo.restoreStatus = status;
} else {
Log.v(TAG, "Widget restore pending id=" + c.id
+ " appWidgetId=" + appWidgetId
+ " status =" + c.restoreFlag);
appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId, component);
appWidgetInfo.restoreStatus = c.restoreFlag;
tempPackageKey.update(component.getPackageName(), c.user);
SessionInfo si = installingPkgs.get(tempPackageKey);
Integer installProgress = si == null
? null
: (int) (si.getProgress() * 100);
if (c.hasRestoreFlag(LauncherAppWidgetInfo.FLAG_RESTORE_STARTED)) {
// Restore has started once.
} else if (installProgress != null) {
// App restore has started. Update the flag
appWidgetInfo.restoreStatus
|= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED;
} else if (!isSafeMode) {
c.markDeleted("Unrestored widget removed: " + component);
return;
}
appWidgetInfo.installProgress =
installProgress == null ? 0 : installProgress;
}
if (appWidgetInfo.hasRestoreFlag(
LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) {
appWidgetInfo.bindOptions = c.parseIntent();
}
c.applyCommonProperties(appWidgetInfo);
appWidgetInfo.spanX = c.getSpanX();
appWidgetInfo.spanY = c.getSpanY();
appWidgetInfo.options = c.getOptions();
appWidgetInfo.user = c.user;
appWidgetInfo.sourceContainer = c.getAppWidgetSource();
if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0) {
c.markDeleted("Widget has invalid size: "
+ appWidgetInfo.spanX + "x" + appWidgetInfo.spanY);
return;
}
LauncherAppWidgetProviderInfo widgetProviderInfo =
widgetHelper.getLauncherAppWidgetInfo(appWidgetId);
if (widgetProviderInfo != null
&& (appWidgetInfo.spanX < widgetProviderInfo.minSpanX
|| appWidgetInfo.spanY < widgetProviderInfo.minSpanY)) {
FileLog.d(TAG, "Widget " + widgetProviderInfo.getComponent()
+ " minSizes not meet: span=" + appWidgetInfo.spanX
+ "x" + appWidgetInfo.spanY + " minSpan="
+ widgetProviderInfo.minSpanX + "x"
+ widgetProviderInfo.minSpanY);
logWidgetInfo(mApp.getInvariantDeviceProfile(),
widgetProviderInfo);
}
if (!c.isOnWorkspaceOrHotseat()) {
c.markDeleted("Widget found where container != CONTAINER_DESKTOP"
+ "nor CONTAINER_HOTSEAT - ignoring!");
return;
}
if (!customWidget) {
String providerName = appWidgetInfo.providerName.flattenToString();
if (!providerName.equals(savedProvider)
|| (appWidgetInfo.restoreStatus != c.restoreFlag)) {
c.updater()
.put(Favorites.APPWIDGET_PROVIDER,
providerName)
.put(Favorites.RESTORED,
appWidgetInfo.restoreStatus)
.commit();
}
}
if (appWidgetInfo.restoreStatus
!= LauncherAppWidgetInfo.RESTORE_COMPLETED) {
appWidgetInfo.pendingItemInfo = WidgetsModel.newPendingItemInfo(
mApp.getContext(),
appWidgetInfo.providerName,
appWidgetInfo.user);
mIconCache.getTitleAndIconForApp(
appWidgetInfo.pendingItemInfo, false);
}
c.checkAndAddItem(appWidgetInfo, mBgDataModel);
}
break;
}
} catch (Exception e) {
Log.e(TAG, "Desktop items loading interrupted", e);
}
}
这个方法是启动器加载工作区项目的核心逻辑,确保从数据库加载的项目是最新的,并且根据当前设备状态(如SD卡是否就绪、是否处于安全模式等)进行适当的处理。
2、Workspace bind分析
数据绑定即为将 BgDataModel 中的图标放到桌面上。 放置的时候为了提高用户体验,优先放置当前屏幕的图标和 widget,然后再放其他屏幕的图标和 widget,这样用户能更快的看到图标显示完成。
BaseLauncherBinder.java
/**
* Binds all loaded data to actual views on the main thread.
*/
public void bindWorkspace(boolean incrementBindId, boolean isBindSync) {
if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) {
DisjointWorkspaceBinder workspaceBinder =
initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens());
workspaceBinder.bindCurrentWorkspacePages(isBindSync);
workspaceBinder.bindOtherWorkspacePages();
} else {
bindWorkspaceAllAtOnce(incrementBindId, isBindSync);
}
}
/**
* Initializes the WorkspaceBinder for binding.
*
* @param incrementBindId this is used to stop previously started binding tasks that are
* obsolete but still queued.
* @param workspacePages this allows the Launcher to add the correct workspace screens.
*/
public DisjointWorkspaceBinder initWorkspaceBinder(boolean incrementBindId,
IntArray workspacePages) {
synchronized (mBgDataModel) {
if (incrementBindId) {
mBgDataModel.lastBindId++;
}
mMyBindingId = mBgDataModel.lastBindId;
return new DisjointWorkspaceBinder(workspacePages);
}
}
private class DisjointWorkspaceBinder {
private final IntArray mOrderedScreenIds;
private final IntSet mCurrentScreenIds = new IntSet();
private final Set<Integer> mBoundItemIds = new HashSet<>();
protected DisjointWorkspaceBinder(IntArray orderedScreenIds) {
mOrderedScreenIds = orderedScreenIds;
for (Callbacks cb : mCallbacksList) {
mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds));
}
if (mCurrentScreenIds.size() == 0) {
mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID);
}
}
/**
* Binds the currently loaded items in the Data Model. Also signals to the Callbacks[]
* that these items have been bound and their respective screens are ready to be shown.
*
* If this method is called after all the items on the workspace screen have already been
* loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will
* not bind any items.
*/
protected void bindCurrentWorkspacePages(boolean isBindSync) {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems;
ArrayList<LauncherAppWidgetInfo> appWidgets;
ArrayList<FixedContainerItems> fciList = new ArrayList<>();
final int workspaceItemCount;
synchronized (mBgDataModel) {
workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mBgDataModel.extraItems.forEach(fciList::add);
}
workspaceItemCount = mBgDataModel.itemsIdMap.size();
}
workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
appWidgets.forEach(it -> mBoundItemIds.add(it.id));
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
fciList.forEach(item ->
executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
}
sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
// Tell the workspace that we're about to start binding items
executeCallbacksTask(c -> {
c.clearPendingBinds();
c.startBinding();
}, mUiExecutor);
// Bind workspace screens
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
bindWorkspaceItems(workspaceItems);
bindAppWidgets(appWidgets);
executeCallbacksTask(c -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
c.onInitialBindComplete(
mCurrentScreenIds, new RunnableList(), workspaceItemCount, isBindSync);
}, mUiExecutor);
}
protected void bindOtherWorkspacePages() {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems;
ArrayList<LauncherAppWidgetInfo> appWidgets;
synchronized (mBgDataModel) {
workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
}
workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id));
appWidgets.removeIf(it -> mBoundItemIds.contains(it.id));
sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
bindWorkspaceItems(workspaceItems);
bindAppWidgets(appWidgets);
executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor);
mUiExecutor.execute(() -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
ItemInstallQueue.INSTANCE.get(mApp.getContext())
.resumeModelPush(FLAG_LOADER_RUNNING);
});
for (Callbacks cb : mCallbacksList) {
cb.bindStringCache(mBgDataModel.stringCache.clone());
}
}
private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) {
// Bind the workspace items
int count = workspaceItems.size();
for (int i = 0; i < count; i += ITEMS_CHUNK) {
final int start = i;
final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
executeCallbacksTask(
c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
mUiExecutor);
}
}
private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) {
// Bind the widgets, one at a time
int count = appWidgets.size();
for (int i = 0; i < count; i++) {
final ItemInfo widget = appWidgets.get(i);
executeCallbacksTask(
c -> c.bindItems(Collections.singletonList(widget), false),
mUiExecutor);
}
}
}
这个构造函数的作用是初始化工作区绑定器,并确定哪些工作区屏幕需要进行同步绑定。通过回调机制,它允许外部指定特定的页面ID进行同步绑定,如果没有指定,则默认绑定第一个屏幕。这可能是为了优化启动器的工作区加载过程,确保用户首次使用时能快速看到主屏幕。
protected void bindCurrentWorkspacePages(boolean isBindSync) {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems;
ArrayList<LauncherAppWidgetInfo> appWidgets;
ArrayList<FixedContainerItems> fciList = new ArrayList<>();
final int workspaceItemCount;
synchronized (mBgDataModel) {
workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mBgDataModel.extraItems.forEach(fciList::add);
}
workspaceItemCount = mBgDataModel.itemsIdMap.size();
}
workspaceItems.forEach(it -> mBoundItemIds.add(it.id));
appWidgets.forEach(it -> mBoundItemIds.add(it.id));
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
fciList.forEach(item ->
executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
}
sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
// Tell the workspace that we're about to start binding items
executeCallbacksTask(c -> {
c.clearPendingBinds();
c.startBinding();
}, mUiExecutor);
// Bind workspace screens
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
bindWorkspaceItems(workspaceItems);
bindAppWidgets(appWidgets);
executeCallbacksTask(c -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
c.onInitialBindComplete(
mCurrentScreenIds, new RunnableList(), workspaceItemCount, isBindSync);
}, mUiExecutor);
}
protected void bindOtherWorkspacePages() {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems;
ArrayList<LauncherAppWidgetInfo> appWidgets;
synchronized (mBgDataModel) {
workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems);
appWidgets = new ArrayList<>(mBgDataModel.appWidgets);
}
workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id));
appWidgets.removeIf(it -> mBoundItemIds.contains(it.id));
sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems);
bindWorkspaceItems(workspaceItems);
bindAppWidgets(appWidgets);
executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor);
mUiExecutor.execute(() -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
ItemInstallQueue.INSTANCE.get(mApp.getContext())
.resumeModelPush(FLAG_LOADER_RUNNING);
});
for (Callbacks cb : mCallbacksList) {
cb.bindStringCache(mBgDataModel.stringCache.clone());
}
}
private void bindWorkspaceAllAtOnce(boolean incrementBindId, boolean isBindSync) {
// Save a copy of all the bg-thread collections
ArrayList<ItemInfo> workspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>();
final IntArray orderedScreenIds = new IntArray();
ArrayList<FixedContainerItems> extraItems = new ArrayList<>();
final int workspaceItemCount;
synchronized (mBgDataModel) {
workspaceItems.addAll(mBgDataModel.workspaceItems);
appWidgets.addAll(mBgDataModel.appWidgets);
orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens());
mBgDataModel.extraItems.forEach(extraItems::add);
if (incrementBindId) {
mBgDataModel.lastBindId++;
}
mMyBindingId = mBgDataModel.lastBindId;
workspaceItemCount = mBgDataModel.itemsIdMap.size();
}
for (Callbacks cb : mCallbacksList) {
new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId,
workspaceItems, appWidgets, extraItems, orderedScreenIds)
.bind(isBindSync, workspaceItemCount);
}
}
BaseLauncherBinder.java
private void bind(boolean isBindSync, int workspaceItemCount) {
final IntSet currentScreenIds =
mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds);
Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks);
// Separate the items that are on the current screen, and all the other remaining items
ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
otherWorkspaceItems);
filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets,
otherAppWidgets);
final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile();
sortWorkspaceItemsSpatially(idp, currentWorkspaceItems);
sortWorkspaceItemsSpatially(idp, otherWorkspaceItems);
// Tell the workspace that we're about to start binding items
executeCallbacksTask(c -> {
c.clearPendingBinds();
c.startBinding();
}, mUiExecutor);
// Bind workspace screens
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
// Load items on the current page.
bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
bindAppWidgets(currentAppWidgets, mUiExecutor);
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mExtraItems.forEach(item ->
executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
}
RunnableList pendingTasks = new RunnableList();
Executor pendingExecutor = pendingTasks::add;
bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
bindAppWidgets(otherAppWidgets, pendingExecutor);
executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor);
pendingExecutor.execute(
() -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
ItemInstallQueue.INSTANCE.get(mApp.getContext())
.resumeModelPush(FLAG_LOADER_RUNNING);
});
executeCallbacksTask(
c -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
c.onInitialBindComplete(
currentScreenIds, pendingTasks, workspaceItemCount, isBindSync);
}, mUiExecutor);
mCallbacks.bindStringCache(mBgDataModel.stringCache.clone());
}
filterCurrentWorkspaceItems 函数将遍历 mWorkspaceItems 或 mAppWidgets 列表,检查每个项目或小部件的屏幕ID是否在 currentScreenIds 集合中。如果项目或小部件的屏幕ID是当前屏幕之一,它将被添加到 currentWorkspaceItems 或 currentAppWidgets 列表中。如果不是,它将被添加到 otherWorkspaceItems 或 otherAppWidgets 列表中。这种分离允许代码分别处理当前屏幕的元素和其它屏幕的元素。
protected void executeCallbacksTask(CallbackTask task, Executor executor) {
executor.execute(() -> {
if (mMyBindingId != mBgDataModel.lastBindId) {
Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind");
return;
}
task.execute(mCallbacks);
});
}
}
这个mCallbacks也就是LauncherModel的mCallbacks,初始化是在Launcher onCreate里调用LauncherAppState.setLauncher,在LauncherModel的initialize()里完成赋值。 故Callbacks就是我们的Launcher。
调用Launcher里实现的 startBinding(), 改变workspace的状态,移除一些旧的View和数据。
Launcher.java
/**
* Refreshes the shortcuts shown on the workspace.
* <p>
* Implementation of the method from LauncherModel.Callbacks.
*/
public void startBinding() {
Object traceToken = TraceHelper.INSTANCE.beginSection("startBinding");
// Floating panels (except the full widget sheet) are associated with individual icons. If
// we are starting a fresh bind, close all such panels as all the icons are about
// to go away.
AbstractFloatingView.closeOpenViews(this, true, TYPE_ALL & ~TYPE_REBIND_SAFE);
setWorkspaceLoading(true);
// Clear the workspace because it's going to be rebound
mDragController.cancelDrag();
mWorkspace.clearDropTargets();
mWorkspace.removeAllWorkspaceScreens();
mAppWidgetHolder.clearViews();
if (mHotseat != null) {
mHotseat.resetLayout(getDeviceProfile().isVerticalBarLayout());
}
TraceHelper.INSTANCE.endSection(traceToken);
}
@Override
public void bindScreens(IntArray orderedScreenIds) {
int firstScreenPosition = 0;
if (FeatureFlags.QSB_ON_FIRST_SCREEN &&
orderedScreenIds.indexOf(Workspace.FIRST_SCREEN_ID) != firstScreenPosition) {
orderedScreenIds.removeValue(Workspace.FIRST_SCREEN_ID);
orderedScreenIds.add(firstScreenPosition, Workspace.FIRST_SCREEN_ID);
} else if (!FeatureFlags.QSB_ON_FIRST_SCREEN && orderedScreenIds.isEmpty()) {
// If there are no screens, we need to have an empty screen
mWorkspace.addExtraEmptyScreens();
}
bindAddScreens(orderedScreenIds);
// After we have added all the screens, if the wallpaper was locked to the default state,
// then notify to indicate that it can be released and a proper wallpaper offset can be
// computed before the next layout
mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout();
}
private void bindAddScreens(IntArray orderedScreenIds) {
if (mDeviceProfile.isTwoPanels) {
// Some empty pages might have been removed while the phone was in a single panel
// mode, so we want to add those empty pages back.
IntSet screenIds = IntSet.wrap(orderedScreenIds);
orderedScreenIds.forEach(screenId -> screenIds.add(mWorkspace.getScreenPair(screenId)));
orderedScreenIds = screenIds.getArray();
}
int count = orderedScreenIds.size();
for (int i = 0; i < count; i++) {
int screenId = orderedScreenIds.get(i);
if (FeatureFlags.QSB_ON_FIRST_SCREEN && screenId == Workspace.FIRST_SCREEN_ID) {
// No need to bind the first screen, as its always bound.
continue;
}
mWorkspace.insertNewWorkspaceScreenBeforeEmptyScreen(screenId);
}
}
根据获取到Screen Id集合,调用Workspace的insertNewWorkspaceScreenBeforeEmptyScreen,创建相对应的CellLayout,并添加到我们的Workspace这个容器里。
四、Launcher拖拽分析
拖拽就是长按图标,等到workspace出现图标的相关信息(APP info、Widgets)后,就可以拖动该图标。如果遇到格子已经被占领,则挤走当前格子的图标(或者与其生成文件夹);如果当前格子没有被占领则可以放入这个位置;如果拖动到画面右侧边缘,则自动进入下一页。
明确几个对象:
1、Workspace:主屏幕对应的布局,是直接添加到Launcher.xml中的布局对象。
2、CellLayout:主屏幕中的每一页,其父布局就是Workspace,左右滑动屏幕,就是每一个CellLayout的变化过程,这个类中有很多处理拖拽相关方法。
3、ShortcutAndWidgetContainer:装载图标的容器(布局),其父布局是CellLayout。
4、BubbleTextView:launcher中的图标对象(单击、长按图标的实际载体)
view树结构如下:
其中ShortcutAndWidgetContainer会被划分为m×n的格子区域,具体划分数量依据res/xml/device_profiles.xml中定义。
每个BubbTextView怎么显示在ShortcutAndWidgetContainer对应的格子的?
这就要依据BubbTextView携带的ItemInfo对象了,这个信息类封装了图标的一切信息:行列号、屏幕ID、图标名称、图标Drawable、Intent信息等等,这个信息对象非常重要!!!。ShortcutAndWidgetContainer的onMeasure() 方法会对每个装载BubbTextView的格子指定大小(也是根据配置xml来的),在onLayout()方法中根据BubbTextView的行列号计算其在当前那个格子中。
因此,拖拽就是不断更新BubbTextView的行列号以及屏幕ID的过程,然后实时刷新。
除了上述几个用于显示的View对象,还有和拖拽相关的专用对象:
1、DragLayer:拖拽图层,最顶层的View对象,其主要功能就是处理滑动事件,以及拖拽对象的动画效果。
其子View包含Workspace(主页)、PageIndicatorDots(分页指示器)、AllApp(更多应用界面、上拉弹出的抽屉页)、HotSeat(画面底部常驻图标区)。具体可以查看res/layout/launcher.xml里面的内容,以及DragLayer类方法。
2、DragController:核心拖拽控制器基类,定义很多拖拽相关的公共方法,处理滑动事件等等,其子类重点关注LauncherDragController。
3、DropTarget:拖拽事件接口,在Workspace中有实现这个接口。其包含主要的拖拽事件:onDrop(拖拽结束松手的瞬间触发)、onDragEnter(进入拖拽触发)、onDragOver(拖拽过程中触发)、onDragExit(退出拖拽)。重点需要理解的就是onDragOver以及onDrop。
4、DragView:BubbTextView的平替(他们携带的信息是一样的),因为BubbTextView的父布局是ShortcutAndWidgetContainer,如果拖拽到另一个ShortcutAndWidgetContainer是不允许的。所以创造了一个DragView来代替BubbTextView,这样拖动过程其实是拽着DragView动(原始的BubbTextView会被隐藏)。
5、DraggableView:定义绘制预览、拖拽预览以及相关动画的接口,BubbleTextView中有相关的实现。
6、DragOptions:定义拖拽过程中的一些状态、行为信息(例如:是否正在拖拽,是否是键盘控制等等)。
1、Launcher—拖拽触发的起点分析
Launcher.java
public View createShortcut(ViewGroup parent, WorkspaceItemInfo info) {
BubbleTextView favorite = (BubbleTextView) LayoutInflater.from(parent.getContext())
.inflate(R.layout.app_icon, parent, false);
favorite.applyFromWorkspaceItem(info);
favorite.setOnClickListener(getItemOnClickListener());
favorite.setOnFocusChangeListener(mFocusHandler);
return favorite;
}
上面只是BubbleTextView 创建过程,以及单击事件。那么长按事件在哪儿?
只要跟着Launcher#bindItems()步骤,就能找到WorkspaceLayoutManager#addInScreen()中
workspace.addInScreenFromBind(view, item);
其实应该跳转到Workspace.java中,但该类实现了WorkspaceLayoutManager接口而且Workspace.java没有实现该方法。
WorkspaceLayoutManager.java
/**
* At bind time, we use the rank (screenId) to compute x and y for hotseat items.
* See {@link #addInScreen}.
*/
default void addInScreenFromBind(View child, ItemInfo info) {
CellPos presenterPos = getCellPosMapper().mapModelToPresenter(info);
int x = presenterPos.cellX;
int y = presenterPos.cellY;
if (info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
|| info.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
int screenId = presenterPos.screenId;
x = getHotseat().getCellXFromOrder(screenId);
y = getHotseat().getCellYFromOrder(screenId);
}
addInScreen(child, info.container, presenterPos.screenId, x, y, info.spanX, info.spanY);
}
上面这段代码主要用于在主屏幕上根据给定的信息添加视图元素。如果视图是添加到热座位区域,它会特别处理x和y坐标,以确保它们符合热座位的布局。
/**
* Adds the specified child in the specified screen based on the {@param info}
* See {@link #addInScreen(View, int, int, int, int, int, int)}.
*/
default void addInScreen(View child, ItemInfo info) {
CellPos presenterPos = getCellPosMapper().mapModelToPresenter(info);
addInScreen(child, info.container,
presenterPos.screenId, presenterPos.cellX, presenterPos.cellY,
info.spanX, info.spanY);
}
/**
* Adds the specified child in the specified screen. The position and dimension of
* the child are defined by x, y, spanX and spanY.
*
* @param child The child to add in one of the workspace's screens.
* @param screenId The screen in which to add the child.
* @param x The X position of the child in the screen's grid.
* @param y The Y position of the child in the screen's grid.
* @param spanX The number of cells spanned horizontally by the child.
* @param spanY The number of cells spanned vertically by the child.
*/
default void addInScreen(View child, int container, int screenId, int x, int y,
int spanX, int spanY) {
if (container == LauncherSettings.Favorites.CONTAINER_DESKTOP) {
if (getScreenWithId(screenId) == null) {
Log.e(TAG, "Skipping child, screenId " + screenId + " not found");
// DEBUGGING - Print out the stack trace to see where we are adding from
new Throwable().printStackTrace();
return;
}
}
if (EXTRA_EMPTY_SCREEN_IDS.contains(screenId)) {
// This should never happen
throw new RuntimeException("Screen id should not be extra empty screen: " + screenId);
}
final CellLayout layout;
if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT
|| container == LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION) {
layout = getHotseat();
// Hide folder title in the hotseat
if (child instanceof FolderIcon) {
((FolderIcon) child).setTextVisible(false);
}
} else {
// Show folder title if not in the hotseat
if (child instanceof FolderIcon) {
((FolderIcon) child).setTextVisible(true);
}
layout = getScreenWithId(screenId);
}
ViewGroup.LayoutParams genericLp = child.getLayoutParams();
CellLayoutLayoutParams lp;
if (genericLp == null || !(genericLp instanceof CellLayoutLayoutParams)) {
lp = new CellLayoutLayoutParams(x, y, spanX, spanY);
} else {
lp = (CellLayoutLayoutParams) genericLp;
lp.setCellX(x);
lp.setCellY(y);
lp.cellHSpan = spanX;
lp.cellVSpan = spanY;
}
if (spanX < 0 && spanY < 0) {
lp.isLockedToGrid = false;
}
// Get the canonical child id to uniquely represent this view in this screen
ItemInfo info = (ItemInfo) child.getTag();
int childId = info.getViewId();
boolean markCellsAsOccupied = !(child instanceof Folder);
if (!layout.addViewToCellLayout(child, -1, childId, lp, markCellsAsOccupied)) {
// TODO: This branch occurs when the workspace is adding views
// outside of the defined grid
// maybe we should be deleting these items from the LauncherModel?
Log.e(TAG, "Failed to add to item at (" + lp.getCellX() + "," + lp.getCellY()
+ ") to CellLayout");
}
child.setHapticFeedbackEnabled(false);
child.setOnLongClickListener(getWorkspaceChildOnLongClickListener());
if (child instanceof DropTarget) {
onAddDropTarget((DropTarget) child);
}
}
关键的来了,长按事件回调就写在getWorkspaceChildOnLongClickListener()方法中,接着跟代码,最终找到ItemLongClickListener的onWorkspaceItemLongClick()方法,也就是桌面item的长按事件。
ItemLongClickListener.java
private static boolean onWorkspaceItemLongClick(View v) {
if (v instanceof LauncherAppWidgetHostView) {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "Widgets.onLongClick");
} else {
TestLogging.recordEvent(TestProtocol.SEQUENCE_MAIN, "onWorkspaceItemLongClick");
}
Launcher launcher = Launcher.getLauncher(v.getContext());
if (!canStartDrag(launcher)) return false;
if (!launcher.isInState(NORMAL)
&& !launcher.isInState(OVERVIEW)
&& !launcher.isInState(EDIT_MODE)) {
return false;
}
if (!(v.getTag() instanceof ItemInfo)) return false;
launcher.setWaitingForResult(null);
beginDrag(v, launcher, (ItemInfo) v.getTag(), launcher.getDefaultWorkspaceDragOptions());
return true;
}
public static void beginDrag(View v, Launcher launcher, ItemInfo info,
DragOptions dragOptions) {
if (info.container >= 0) {
Folder folder = Folder.getOpen(launcher);
if (folder != null) {
if (!folder.getIconsInReadingOrder().contains(v)) {
folder.close(true);
} else {
folder.startDrag(v, dragOptions);
return;
}
}
}
CellLayout.CellInfo longClickCellInfo = new CellLayout.CellInfo(v, info,
launcher.getCellPosMapper().mapModelToPresenter(info));
launcher.getWorkspace().startDrag(longClickCellInfo, dragOptions);
}
Workspace.java
public void startDrag(CellLayout.CellInfo cellInfo, DragOptions options) {
View child = cellInfo.cell;
mDragInfo = cellInfo;
child.setVisibility(INVISIBLE);
if (options.isAccessibleDrag) {
mDragController.addDragListener(
new AccessibleDragListenerAdapter(this, WorkspaceAccessibilityHelper::new) {
@Override
protected void enableAccessibleDrag(boolean enable) {
super.enableAccessibleDrag(enable);
setEnableForLayout(mLauncher.getHotseat(), enable);
}
});
}
beginDragShared(child, this, options);
}
2、Launcher—Workspace触发拖拽事件
接着看beginDragShared(),头疼的来了~~
public void beginDragShared(View child, DragSource source, DragOptions options) {
Object dragObject = child.getTag();
if (!(dragObject instanceof ItemInfo)) {
String msg = "Drag started with a view that has no tag set. This "
+ "will cause a crash (issue 11627249) down the line. "
+ "View: " + child + " tag: " + child.getTag();
throw new IllegalStateException(msg);
}
beginDragShared(child, null, source, (ItemInfo) dragObject,
new DragPreviewProvider(child), options);
}
/**
* Core functionality for beginning a drag operation for an item that will be dropped within
* the workspace
*/
public DragView beginDragShared(View child, DraggableView draggableView, DragSource source,
ItemInfo dragObject, DragPreviewProvider previewProvider, DragOptions dragOptions) {
float iconScale = 1f;
if (child instanceof BubbleTextView) {
Drawable icon = ((BubbleTextView) child).getIcon();
if (icon instanceof FastBitmapDrawable) {
iconScale = ((FastBitmapDrawable) icon).getAnimatedScale();
}
}
// Clear the pressed state if necessary
child.clearFocus();
child.setPressed(false);
if (child instanceof BubbleTextView) {
BubbleTextView icon = (BubbleTextView) child;
icon.clearPressedBackground();
}
if (draggableView == null && child instanceof DraggableView) {
draggableView = (DraggableView) child;
}
final View contentView = previewProvider.getContentView();
final float scale;
// The draggable drawable follows the touch point around on the screen
final Drawable drawable;
if (contentView == null) {
drawable = previewProvider.createDrawable();
scale = previewProvider.getScaleAndPosition(drawable, mTempXY);
} else {
drawable = null;
scale = previewProvider.getScaleAndPosition(contentView, mTempXY);
}
int dragLayerX = mTempXY[0];
int dragLayerY = mTempXY[1];
Rect dragRect = new Rect();
if (draggableView != null) {
draggableView.getSourceVisualDragBounds(dragRect);
dragLayerY += dragRect.top;
}
if (child.getParent() instanceof ShortcutAndWidgetContainer) {
mDragSourceInternal = (ShortcutAndWidgetContainer) child.getParent();
}
if (child instanceof BubbleTextView) {
BubbleTextView btv = (BubbleTextView) child;
if (!dragOptions.isAccessibleDrag) {
dragOptions.preDragCondition = btv.startLongPressAction();
}
if (btv.isDisplaySearchResult()) {
dragOptions.preDragEndScale = (float) mAllAppsIconSize / btv.getIconSize();
}
}
if (dragOptions.preDragCondition != null) {
int xDragOffSet = dragOptions.preDragCondition.getDragOffset().x;
int yDragOffSet = dragOptions.preDragCondition.getDragOffset().y;
if (xDragOffSet != 0 || yDragOffSet != 0) {
dragLayerX += xDragOffSet;
dragLayerY += yDragOffSet;
}
}
final DragView dv;
if (contentView instanceof View) {
if (contentView instanceof LauncherAppWidgetHostView) {
mDragController.addDragListener(new AppWidgetHostViewDragListener(mLauncher));
}
dv = mDragController.startDrag(
contentView,
draggableView,
dragLayerX,
dragLayerY,
source,
dragObject,
dragRect,
scale * iconScale,
scale,
dragOptions);
} else {
dv = mDragController.startDrag(
drawable,
draggableView,
dragLayerX,
dragLayerY,
source,
dragObject,
dragRect,
scale * iconScale,
scale,
dragOptions);
}
return dv;
}
上面的代码都是做拖拽前的准备工作,下面的mDragController.startDrag()才是关键
contentView 或 drawable:拖拽的起始内容,可以是 View 或 Drawable 对象。
draggableView:实际参与拖拽的视图。
dragLayerX 和 dragLayerY:拖拽层上的起始坐标点。
source:拖拽源,用于识别拖拽操作的起始点。
dragObject:与拖拽相关联的对象,包含拖拽的数据或状态。
dragRect:定义拖拽视图的边界。
scale * iconScale:缩放比例,用于调整拖拽视图的大小。
scale:另一个缩放比例参数。
dragOptions:包含拖拽操作配置的选项。
关键代码来到了dv = mDragController.startDrag() , 跟着代码跳到了: DragController#startDrag() --> LauncherDragController#startDrag。
其中LauncherDragController是DragController的子类之一,最终调用的是LauncherDragController的startDrag()方法。
因此接着看LauncherDragController的startDrag()方法,因为代码很长,所以只看重点部分:
LauncherDragController.java
@Override
protected DragView startDrag(
@Nullable Drawable drawable,
@Nullable View view,
DraggableView originalView,
int dragLayerX,
int dragLayerY,
DragSource source,
ItemInfo dragInfo,
Rect dragRegion,
float initialDragViewScale,
float dragViewScaleOnDrop,
DragOptions options) {
if (PROFILE_DRAWING_DURING_DRAG) {
android.os.Debug.startMethodTracing("Launcher");
}
mActivity.hideKeyboard();
AbstractFloatingView.closeOpenViews(mActivity, false, TYPE_DISCOVERY_BOUNCE);
mOptions = options;
if (mOptions.simulatedDndStartPoint != null) {
mLastTouch.x = mMotionDown.x = mOptions.simulatedDndStartPoint.x;
mLastTouch.y = mMotionDown.y = mOptions.simulatedDndStartPoint.y;
}
final int registrationX = mMotionDown.x - dragLayerX;
final int registrationY = mMotionDown.y - dragLayerY;
final int dragRegionLeft = dragRegion == null ? 0 : dragRegion.left;
final int dragRegionTop = dragRegion == null ? 0 : dragRegion.top;
mLastDropTarget = null;
mDragObject = new DropTarget.DragObject(mActivity.getApplicationContext());
mDragObject.originalView = originalView;
mIsInPreDrag = mOptions.preDragCondition != null
&& !mOptions.preDragCondition.shouldStartDrag(0);
final Resources res = mActivity.getResources();
final float scaleDps = mIsInPreDrag
? res.getDimensionPixelSize(R.dimen.pre_drag_view_scale) : 0f;
final DragView dragView = mDragObject.dragView = drawable != null
? new LauncherDragView(
mActivity,
drawable,
registrationX,
registrationY,
initialDragViewScale,
dragViewScaleOnDrop,
scaleDps)
: new LauncherDragView(
mActivity,
view,
view.getMeasuredWidth(),
view.getMeasuredHeight(),
registrationX,
registrationY,
initialDragViewScale,
dragViewScaleOnDrop,
scaleDps);
dragView.setItemInfo(dragInfo);
mDragObject.dragComplete = false;
mDragObject.xOffset = mMotionDown.x - (dragLayerX + dragRegionLeft);
mDragObject.yOffset = mMotionDown.y - (dragLayerY + dragRegionTop);
mDragDriver = DragDriver.create(this, mOptions, mFlingToDeleteHelper::recordMotionEvent);
if (!mOptions.isAccessibleDrag) {
mDragObject.stateAnnouncer = DragViewStateAnnouncer.createFor(dragView);
}
mDragObject.dragSource = source;
mDragObject.dragInfo = dragInfo;
mDragObject.originalDragInfo = mDragObject.dragInfo.makeShallowCopy();
if (mOptions.preDragCondition != null) {
dragView.setHasDragOffset(mOptions.preDragCondition.getDragOffset().x != 0 ||
mOptions.preDragCondition.getDragOffset().y != 0);
}
if (dragRegion != null) {
dragView.setDragRegion(new Rect(dragRegion));
}
mActivity.getDragLayer().performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
dragView.show(mLastTouch.x, mLastTouch.y);
mDistanceSinceScroll = 0;
if (!mIsInPreDrag) {
callOnDragStart();
} else if (mOptions.preDragCondition != null) {
mOptions.preDragCondition.onPreDragStart(mDragObject);
}
handleMoveEvent(mLastTouch.x, mLastTouch.y);
if (!mActivity.isTouchInProgress() && options.simulatedDndStartPoint == null) {
// If it is an internal drag and the touch is already complete, cancel immediately
MAIN_EXECUTOR.submit(this::cancelDrag);
}
return dragView;
}
创建DragDriver对象,其内部有EventListener接口
DragDriver.java
并且DragController(及其子类)实现了这个接口
DragController.java
public abstract class DragController<T extends ActivityContext>
implements DragDriver.EventListener, TouchController {
这个EventListener接口会在DragDriver的onDragEvent(DragEvent event)中被触发
DragDriver.java
@Override
public boolean onDragEvent(DragEvent event) {
simulateSecondaryMotionEvent(event);
final int action = event.getAction();
switch (action) {
case DragEvent.ACTION_DRAG_STARTED:
mLastX = event.getX();
mLastY = event.getY();
return true;
case DragEvent.ACTION_DRAG_ENTERED:
return true;
case DragEvent.ACTION_DRAG_LOCATION:
mLastX = event.getX();
mLastY = event.getY();
mEventListener.onDriverDragMove(event.getX(), event.getY());
return true;
case DragEvent.ACTION_DROP:
mLastX = event.getX();
mLastY = event.getY();
mEventListener.onDriverDragMove(event.getX(), event.getY());
mEventListener.onDriverDragEnd(mLastX, mLastY);
return true;
case DragEvent.ACTION_DRAG_EXITED:
mEventListener.onDriverDragExitWindow();
return true;
case DragEvent.ACTION_DRAG_ENDED:
mEventListener.onDriverDragCancel();
return true;
default:
return false;
}
}
这个方法通过 DragEvent 对象来监听和响应拖拽过程中的各种事件,如拖拽开始、位置更新、释放、退出和结束。通过 mEventListener 回调接口,它能够通知其他组件拖拽事件的状态变化。
主要介绍拖拽事件的起点分析,包括介绍了几个拖拽过程中用到的对象,以及长按事件触发的拖拽过程。
BubbtextView首先设置了长按监听事件,最终workspace触发了startDrag()方法(在这一过程中,把原始BubbtextView隐藏,创建了一个DragView来代替)
关键的是创建一个DragDriver 对象,这个对象包含onDragEvent(),以及EventListener接口,并在onDragEvent中调用EventListener接口,换句话说调用了DragController的接口(DragController implements EventListener)从而引发了:
onDragStart() --> onDragEnter() --> onDragOver() --> onDragExit() --> onDrop() --> onDragEnd() 过程。
五、主要类文件功能总结