从配置文件解析出要显示在桌面的应用信息,然后添加到数据库的表
在应用添加的任务回调中把应用显示到桌面,并添加到数据库表中
屏蔽上滑显示应用列表页
自定义初始配置文件
cus_default_workspace_5x5.xml
<?xml version="1.0" encoding="utf-8"?>
<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 -->
<!-- Hotseat区域 -->
<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>
<!-- 倒数第二排 -->
<resolve
launcher:screen="0"
launcher:x="0"
launcher:y="3" >
<favorite launcher:packagename="com.google.android.myapplication"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="1"
launcher:y="3" >
<favorite launcher:packagename="com.android.calendar"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="2"
launcher:y="-2" >
<favorite launcher:packagename="com.android.camera2"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="3"
launcher:y="-2" >
<favorite launcher:packagename="com.android.dialer"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="3"
launcher:y="-2" >
<favorite launcher:packagename="com.android.dialer"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="4"
launcher:y="-2" >
<favorite launcher:packagename="com.android.contacts"/>
</resolve>
<!-- 倒数第一排 -->
<resolve
launcher:screen="0"
launcher:x="0"
launcher:y="4" >
<favorite launcher:packagename="org.chromium.webview_shell"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="1"
launcher:y="4" >
<favorite launcher:packagename="com.android.gallery3d"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="2"
launcher:y="-1" >
<favorite launcher:packagename="com.android.messaging"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="3"
launcher:y="-1" >
<favorite launcher:packagename="com.android.settings"/>
</resolve>
<resolve
launcher:screen="0"
launcher:x="4"
launcher:y="-1" >
<favorite launcher:packagename="com.android.quicksearchbox"/>
</resolve>
<!-- 第二页 -->
<resolve
launcher:screen="1"
launcher:x="0"
launcher:y="-1" >
<favorite launcher:packagename="com.android.documentsui"/>
</resolve>
</favorites>
device_profiles.xml的属性defaultLayoutId改成自定义的配置文件
<profiles xmlns:launcher="http://schemas.android.com/apk/res-auto" >
...
<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/cus_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>
</profiles>
DefaultLayoutParser继承自AutoInstallsLayout
它负责解析cus_default_workspace_5x5.xml配置文件
DefaultLayoutParser中的内部类AppShortcutWithUriParser会解析到resolve标签内的favorite 标签
在invalidPackageOrClass方法添加自己的逻辑,然后把数据添加数据库的favorites表中
@Override
protected int invalidPackageOrClass(XmlPullParser parser) {
final String uri = getAttributeValue(parser, ATTR_URI);
//获取packagename属性的值
final String packageNameValue = getAttributeValue(parser, "packagename");
Log.i(TAG, "invalidPackageOrClass uri: "+uri);
Log.i(TAG, "invalidPackageOrClass packageName: "+packageNameValue);
//添加对packagename属性的解析
if(!TextUtils.isEmpty(packageNameValue)){
try {
Intent launchIntent = mPackageManager.getLaunchIntentForPackage(
packageNameValue);
if (launchIntent == null) {
return -1;
}
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
String launchIntentUri = launchIntent.toUri(0);
ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(
packageNameValue, PackageManager.GET_META_DATA);
String appName = applicationInfo.loadLabel(mPackageManager).toString();
Log.i(TAG, "invalidPackageOrClass launchIntentUri: "+launchIntentUri);
Log.i(TAG, "invalidPackageOrClass appName: "+appName);
//添加到数据库
return addShortcut(appName, launchIntent, Favorites.ITEM_TYPE_APPLICATION);
} catch (PackageManager.NameNotFoundException e) {
Log.e(TAG, "app name not found");
return -1;
}
}
if (TextUtils.isEmpty(uri)) {
Log.e(TAG, "Skipping invalid <favorite> with no component or uri");
return -1;
}
final Intent metaIntent;
try {
metaIntent = Intent.parseUri(uri, 0);
} catch (URISyntaxException e) {
Log.e(TAG, "Unable to add meta-favorite: " + uri, e);
return -1;
}
ResolveInfo resolved = mPackageManager.resolveActivity(metaIntent,
PackageManager.MATCH_DEFAULT_ONLY);
final List<ResolveInfo> appList = mPackageManager.queryIntentActivities(
metaIntent, PackageManager.MATCH_DEFAULT_ONLY);
// Verify that the result is an app and not just the resolver dialog asking which
// app to use.
if (wouldLaunchResolverActivity(resolved, appList)) {
// If only one of the results is a system app then choose that as the default.
final ResolveInfo systemApp = getSingleSystemActivity(appList);
if (systemApp == null) {
// There is no logical choice for this meta-favorite, so rather than making
// a bad choice just add nothing.
Log.w(TAG, "No preference or single system activity found for "
+ metaIntent.toString());
return -1;
}
resolved = systemApp;
}
final ActivityInfo info = resolved.activityInfo;
final Intent intent = mPackageManager.getLaunchIntentForPackage(info.packageName);
if (intent == null) {
return -1;
}
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
return addShortcut(info.loadLabel(mPackageManager).toString(), intent,
Favorites.ITEM_TYPE_APPLICATION);
}
adb install Launcher3QuickStep.apk
adb reboot
可以看到数据插入到数据库中,数据库位置data/data/com.android.launcher3/databases/launcher.db
桌面效果
添加应用时,添加到桌面
PackageUpdatedTask,继承自BaseModelUpdateTask,BaseModelUpdateTask实现Runnable接口
安装、更新时触发PackageUpdatedTask的执行
LauncherModel
@Override
public void onPackageAdded(@NonNull final String packageName, @NonNull final UserHandle user) {
Log.i(TAG, "onPackageAdded: ");
int op = PackageUpdatedTask.OP_ADD;
enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName));
}
会执行到PackageUpdatedTask的execute()方法
@Override
public void execute(@NonNull final LauncherAppState app, @NonNull final BgDataModel dataModel,
@NonNull final AllAppsList appsList) {
final Context context = app.getContext();
final IconCache iconCache = app.getIconCache();
final String[] packages = mPackages;
final int N = packages.length;
final FlagOp flagOp;
final HashSet<String> packageSet = new HashSet<>(Arrays.asList(packages));
final Predicate<ItemInfo> matcher = mOp == OP_USER_AVAILABILITY_CHANGE
? ItemInfoMatcher.ofUser(mUser) // We want to update all packages for this user
: ItemInfoMatcher.ofPackages(packageSet, mUser);
final HashSet<ComponentName> removedComponents = new HashSet<>();
final HashMap<String, List<LauncherActivityInfo>> activitiesLists = new HashMap<>();
switch (mOp) {
case OP_ADD: {
for (int i = 0; i < N; i++) {
if (DEBUG) Log.d(TAG, "mAllAppsList.addPackage " + packages[i]);
iconCache.updateIconsForPkg(packages[i], mUser);
if (FeatureFlags.PROMISE_APPS_IN_ALL_APPS.get()) {
appsList.removePackage(packages[i], mUser);
}
activitiesLists.put(
packages[i], appsList.addPackage(context, packages[i], mUser));
//添加到桌面的逻辑
//构建WorkspaceItemInfo,参考ItemInstallQueue的PendingInstallShortcutInfo获取ItemInfo逻辑
Log.i(TAG, "execute packages : "+packages[i]);
WorkspaceItemInfo workspaceItemInfo = new WorkspaceItemInfo();
workspaceItemInfo.itemType = ITEM_TYPE_APPLICATION;
workspaceItemInfo.user = mUser;
List<LauncherActivityInfo> laiList =
context.getSystemService(LauncherApps.class)
.getActivityList(packages[i], mUser);
LauncherActivityInfo lai;
boolean usePackageIcon = laiList.isEmpty();
if (usePackageIcon) {
lai = null;
workspaceItemInfo.intent = makeLaunchIntent(new ComponentName(packages[i], ""))
.setPackage(packages[i]);
workspaceItemInfo.status |= WorkspaceItemInfo.FLAG_AUTOINSTALL_ICON;
} else {
lai = laiList.get(0);
workspaceItemInfo.intent = makeLaunchIntent(lai);
}
LauncherAppState.getInstance(context).getIconCache()
.getTitleAndIcon(workspaceItemInfo, () -> lai, usePackageIcon, false);
Pair<ItemInfo, Object> itemInfoObjectPair = Pair.create(workspaceItemInfo, null);
ArrayList<Pair<ItemInfo, Object>> pairArrayList = new ArrayList<>();
pairArrayList.add(itemInfoObjectPair);
//调用LauncherModel更新到数据和桌面
//addAndBindAddedWorkspaceItems会添加数据库,并设置应用在几行几列,然后添加到桌面
app.getModel().addAndBindAddedWorkspaceItems(pairArrayList);
}
flagOp = FlagOp.NO_OP.removeFlag(WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE);
break;
}
...
手动测试这个方法调用,假设已经安装了一个测试应用demoAPP,包名时com.google.android.demoapp
//手动模拟桌面添加apk,demoapp已经预安装到设备
List<UserHandle> userHandleList = mUserCache.getUserProfiles();
for (UserHandle userHandle : userHandleList){
mApp.getModel().onPackageAdded("com.google.android.demoapp", userHandle);
}
看下数据库的信息,以及日志
![Untitled]
所在页面和行列数据

拖拽时,隐藏Remove功能
未处理前效果
查看drop_target_bar.xml的DropTargetBar视图,包含DeleteDropTarget和SecondaryDropTarget,DeleteDropTarget负责移除,SecondaryDropTarget负责卸载
DeleteDropTarget,继承自ButtonDropTarget,ButtonDropTarget实现了DropTarget接口
应用图标在移动时会触发DropTarget的onDragStart()的方法调用
DeleteDropTarget
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
super.onDragStart(dragObject, options);
setTextBasedOnDragSource(dragObject.dragInfo);
setControlTypeBasedOnDragSource(dragObject.dragInfo);
}
看下super.onDragStart()的逻辑
ButtonDropTarget
@Override
public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) {
if (options.isKeyboardDrag) {
mActive = false;
} else {
setupItemInfo(dragObject.dragInfo);
//这个控制是否可以显示
mActive = supportsDrop(dragObject.dragInfo);
}
setVisibility(mActive ? View.VISIBLE : View.GONE);
mAccessibleDrag = options.isAccessibleDrag;
setOnClickListener(mAccessibleDrag ? this : null);
}
//父类是一个抽象方法
protected abstract boolean supportsDrop(ItemInfo info);
DeleteDropTarget
@Override
protected boolean supportsDrop(ItemInfo info) {
// return true;
//改成false,变为不可见
return false;
}
效果
屏蔽上滑时显示列表页面
AllAppsTransitionController的setStateWithAnimation()方法逻辑注释掉
/**
* Creates an animation which updates the vertical transition progress and updates all the
* dependent UI using various animation events
*/
@Override
public void setStateWithAnimation(LauncherState toState,
StateAnimationConfig config, PendingAnimation builder) {
// if (mLauncher.isInState(ALL_APPS) && !ALL_APPS.equals(toState)) {
// // For atomic animations, we close the keyboard immediately.
// if (!config.userControlled && mShouldControlKeyboard) {
// mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
// }
//
// builder.addEndListener(success -> {
// // Reset pull back progress and alpha after switching states.
// ALL_APPS_PULL_BACK_TRANSLATION.set(this, ALL_APPS_PULL_BACK_TRANSLATION_DEFAULT);
// ALL_APPS_PULL_BACK_ALPHA.set(this, ALL_APPS_PULL_BACK_ALPHA_DEFAULT);
//
// // We only want to close the keyboard if the animation has completed successfully.
// // The reason is that with keyboard sync, if the user swipes down from All Apps with
// // the keyboard open and then changes their mind and swipes back up, we want the
// // keyboard to remain open. However an onCancel signal is sent to the listeners
// // (success = false), so we need to check for that.
// if (config.userControlled && success && mShouldControlKeyboard) {
// mLauncher.getAppsView().getSearchUiManager().getEditText().hideKeyboard();
// }
// });
// }
//
// float targetProgress = toState.getVerticalProgress(mLauncher);
// if (Float.compare(mProgress, targetProgress) == 0) {
// setAlphas(toState, config, builder);
// // Fail fast
// return;
// }
//
// // need to decide depending on the release velocity
// Interpolator verticalProgressInterpolator = config.getInterpolator(ANIM_VERTICAL_PROGRESS,
// config.userControlled ? LINEAR : DEACCEL_1_7);
// Animator anim = createSpringAnimation(mProgress, targetProgress);
// anim.setInterpolator(verticalProgressInterpolator);
// anim.addListener(getProgressAnimatorListener());
// builder.add(anim);
//
// setAlphas(toState, config, builder);
//
// if (ALL_APPS.equals(toState) && mLauncher.isInState(NORMAL)) {
// mLauncher.getAppsView().performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY,
// HapticFeedbackConstants.FLAG_IGNORE_VIEW_SETTING);
// }
}
PortraitStatesTouchController的canInterceptTouch()方法改为番号false
@Override
protected boolean canInterceptTouch(MotionEvent ev) {
// If we are swiping to all apps instead of overview, allow it from anywhere.
boolean interceptAnywhere = mLauncher.isInState(NORMAL);
if (mCurrentAnimation != null) {
AllAppsTransitionController allAppsController = mLauncher.getAllAppsController();
if (ev.getY() >= allAppsController.getShiftRange() * allAppsController.getProgress()
|| interceptAnywhere) {
// If we are already animating from a previous state, we can intercept as long as
// the touch is below the current all apps progress (to allow for double swipe).
return true;
}
// Otherwise, don't intercept so they can scroll recents, dismiss a task, etc.
return false;
}
if (mLauncher.isInState(ALL_APPS)) {
// In all-apps only listen if the container cannot scroll itself
if (!mLauncher.getAppsView().shouldContainerScroll(ev)) {
return false;
}
} else if (mLauncher.isInState(OVERVIEW)) {
if (!mOverviewPortraitStateTouchHelper.canInterceptTouch(ev)) {
return false;
}
} else {
// For non-normal states, only listen if the event originated below the hotseat height
if (!interceptAnywhere && !isTouchOverHotseat(mLauncher, ev)) {
return false;
}
}
if (getTopOpenViewWithType(mLauncher, TYPE_ACCESSIBLE | TYPE_ALL_APPS_EDU) != null) {
return false;
}
// return true;
//修改为false
return false;
}