主目录见:Android高级进阶知识(这是总目录索引)
Launcher3源码地址:Launcher3-master
[This tutorial was written by Ticoo]
Google Launcher3默认是抽屉型的桌面,到Android 8.0依然是没有这样的功能。这样的功能是手机厂商提供给我们的,不得不说,横向排列的桌面
更适合国人的使用习惯,可能是使用iphone的习惯吧。
好,那我们如何实现这样功能呢?其实并不会太难的。
在Launcher加载流程里,我们知道桌面的数据是在LauncherModel的 LoaderTask完成加载的
我们在loadAndBindAllApps()方法调用之后添加一个verifyApplications()方法调用,为什么在这里调用呢?
因为只用当应用数据加载完全后,我们才能讲所有的应用进行横向绑定到Workspace的操作
@Override
public void run() {
AppTypeHelper.configSystemAppIcon(mContext);
synchronized (mLock) {
if (mStopped) {
return;
}
mIsLoaderTaskRunning = true;
}
// Optimize for end-user experience: if the Launcher is up and // running with the
// All Apps interface in the foreground, load All Apps first. Otherwise, load the
// workspace first (default).
keep_running:
{
if (DEBUG_LOADERS) {
Log.d(TAG, "step 1: loading workspace");
}
loadAndBindWorkspace();
if (mStopped) {
break keep_running;
}
waitForIdle();
// second step
if (DEBUG_LOADERS) {
Log.d(TAG, "step 2: loading all apps");
}
loadAndBindAllApps();
}
if (LauncherAppState.getInstance().getInvariantDeviceProfile()
.isDisableAllApps) {
verifyApplications();
}
// Clear out this reference, otherwise we end up holding it until all of the
// callback runnables are done.
mContext = null;
synchronized (mLock) {
// If we are still the last one to be scheduled, remove ourselves.
if (mLoaderTask == this) {
mLoaderTask = null;
}
mIsLoaderTaskRunning = false;
mHasLoaderCompletedOnce = true;
}
}
这里呢,我简单的添加了一个布尔值 LauncherAppState.getInstance().getInvariantDeviceProfile().isDisableAllApps 表示是否启用横屏桌面,小伙伴开发的时候建议做成开关的方式,以满足不同的产品需求。
verifyApplications方法里怎么实现呢?来看
private void verifyApplications() {
final Context context = mApp.getContext();
// Cross reference all the applications in our apps list with items in the workspace
ArrayList<ItemInfo> tmpInfos;
ArrayList<ItemInfo> added = new ArrayList<ItemInfo>();
synchronized (sBgLock) {
for (AppInfo app : mBgAllAppsList.data) {
tmpInfos = getItemInfoForComponentName(app.componentName, app.user);
if (tmpInfos.isEmpty()) {
// ignore the apps
if (mIgnoreAppsList.contain(app.componentName.getPackageName())) {
continue;
}
// We are missing an application icon, so add this to the workspace
added.add(app);
// This is a rare event, so lets log it
// Log.e(TAG, "Missing Application on load: " + app);
}
}
}
if (!added.isEmpty()) {
addAndBindAddedWorkspaceItems(context, added);
}
}
如果小伙伴有用心看加载流程的细节的话,在loadAndBindAllApps()方法里,会把获取到的所有应用信息保存到 AllAppsList这个类里,也就是 mBgAllAppsList.data 里面,故
我们遍历data数据,将需要绑定的数据绑定到Workspace上就可以了。这里还有一个方法 getItemInfoForComponentName ,作用是 mBgAllAppsList.data的数据跟sBgItemsIdMap里
的数据做匹配,避免因为线程的关系将不必要的数据添加到桌面
拿到数据的备份added集合后,我们使用LauncherModel里的 addAndBindAddedWorkspaceItems 方法添加item
/**
* Adds the provided items to the workspace.
*/
public void addAndBindAddedWorkspaceItems(final Context context,
final ArrayList<? extends ItemInfo> workspaceApps) {
final Callbacks callbacks = getCallback();
if (workspaceApps.isEmpty()) {
return;
}
// Process the newly added applications and add them to the database first
Runnable r = new Runnable() {
@Override
public void run() {
final ArrayList<ItemInfo> addedShortcutsFinal = new ArrayList<ItemInfo>();
final ArrayList<Long> addedWorkspaceScreensFinal = new ArrayList<Long>();
// Get the list of workspace screens. We need to append to this list and
// can not use sBgWorkspaceScreens because loadWorkspace() may not have been
// called.
ArrayList<Long> workspaceScreens = loadWorkspaceScreensDb(context);
synchronized (sBgLock) {
for (ItemInfo item : workspaceApps) {
if (item instanceof ShortcutInfo) {
// Short-circuit this logic if the icon exists somewhere on the workspace
if (shortcutExists(context, item.getIntent(), item.user)) {
continue;
}
}
// Find appropriate space for the item.
Pair<Long, int[]> coords = findSpaceForItem(context,
workspaceScreens, addedWorkspaceScreensFinal,
1, 1);
long screenId = coords.first;
int[] cordinates = coords.second;
ItemInfo itemInfo;
if (item instanceof ShortcutInfo || item instanceof FolderInfo) {
itemInfo = item;
} else if (item instanceof AppInfo) {
itemInfo = ((AppInfo) item).makeShortcut();
} else {
throw new RuntimeException("Unexpected info type");
}
// Add the shortcut to the db
addItemToDatabase(context, itemInfo,
LauncherSettings.Favorites.CONTAINER_DESKTOP,
screenId, cordinates[0], cordinates[1]);
// Save the ShortcutInfo for binding in the workspace
addedShortcutsFinal.add(itemInfo);
}
}
// Update the workspace screens
updateWorkspaceScreenOrder(context, workspaceScreens);
if (!addedShortcutsFinal.isEmpty()) {
runOnMainThread(new Runnable() {
@Override
public void run() {
Callbacks cb = getCallback();
if (callbacks == cb && cb != null) {
final ArrayList<ItemInfo> addAnimated = new ArrayList<ItemInfo>();
final ArrayList<ItemInfo> addNotAnimated = new ArrayList<ItemInfo>();
if (!addedShortcutsFinal.isEmpty()) {
ItemInfo info = addedShortcutsFinal.get(addedShortcutsFinal.size() - 1);
long lastScreenId = info.screenId;
for (ItemInfo i : addedShortcutsFinal) {
if (i.screenId == lastScreenId) {
addAnimated.add(i);
} else {
addNotAnimated.add(i);
}
}
}
callbacks.bindAppsAdded(addedWorkspaceScreensFinal,
addNotAnimated, addAnimated, null);
}
}
});
}
}
};
runOnWorkerThread(r);
}
这里就跟加载流程里的绑定worksspace的Screen类似了。简单的介绍一下细节,
- 从数据库拿到ScreenId信息 workspaceScreens,遍历需要添加的item信息
- 通过findSpaceForItem 方法在workspace上找到空余的位置,如果没有位置会新创建一个Screen出来。
- 根据ItemInfo的类型创建ShortcutInfo,将ShortcutInfo,screen order信息更新到数据库
- 拿到Launcher 这个callbacks调用 bindAppsAdded,开始绑定到workspace
@Override
public void bindAppsAdded(final ArrayList<Long> newScreens,
final ArrayList<ItemInfo> addNotAnimated,
final ArrayList<ItemInfo> addAnimated,
final ArrayList<AppInfo> addedApps) {
Log.e(TAG, "bindAppsAdded");
Runnable r = new Runnable() {
@Override
public void run() {
bindAppsAdded(newScreens, addNotAnimated, addAnimated, addedApps);
}
};
if (waitUntilResume(r)) {
return;
}
// Add the new screens
if (newScreens != null) {
bindAddScreens(newScreens);
}
// We add the items without animation on non-visible pages, and with
// animations on the new page (which we will try and snap to).
if (addNotAnimated != null && !addNotAnimated.isEmpty()) {
bindItems(addNotAnimated, 0,
addNotAnimated.size(), false);
}
if (addAnimated != null && !addAnimated.isEmpty()) {
bindItems(addAnimated, 0,
addAnimated.size(), true);
}
// Remove the extra empty screen
mWorkspace.removeExtraEmptyScreen(false, false);
if (addedApps != null && mAppsView != null) {
mAppsView.addApps(addedApps);
}
}
可以发现,会先使用新生成的ScreenId创建screen,之后才开始bindItems, 如果继续往bindItems里看你就会发现,会在WorkSpace里调用addInScreenFromBind,完成图标的创建。
这里有个地方值得我们提一下,就是 waitUntilResume 方法的使用,在很多地方都会使用这个方法。 作用是在Launcher onResume的时候再执行我们的Runnable。通常,类似的操作
我们会直接在onResume调用或实现,如果操作一多,onResume里就会很臃肿,不好维护
@Thunk
boolean waitUntilResume(Runnable run, boolean deletePreviousRunnables) {
if (mPaused) {
if (LOGD) {
Log.d(TAG, "Deferring update until onResume");
}
if (deletePreviousRunnables) {
while (mBindOnResumeCallbacks.remove(run)) {
}
}
mBindOnResumeCallbacks.add(run);
return true;
} else {
return false;
}
}
这里使用的是一个状态的机制,在mPaused的状态,把需要执行的runnable添加到mBindOnResumeCallbacks,在onResume的时候在遍历出来执行即可
这样就能将抽屉型的Launcher改造成横向的Launcher了,当然改完之后可能会有一些bug,比如桌面里的应用都是 ShortcutInfo类型的,在拖拽时没有查看信息的功能等等
就需要小伙伴自己修改啦
感谢阅读~