本文来自http://blog.csdn.net/chenshaoyang0011 转载请申明文章出处!
文中如有纰漏之处,望不吝指教~~~欢迎讨论,共同学习~~~
Launcher在应用启动的时候,需要加载AppWidget,shortcut等内容项,通过调用LauncherModel.startLoader(),开始加载的工作。launcherModel中加载好的内容会通过
LauncherModel.Callbacks接口的回调函数将数据传给需要的组件,那先来看看Callbacks的定义:
public interface Callbacks { public boolean setLoadOnResume(); public int getCurrentWorkspaceScreen(); public void startBinding(); public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end); public void bindFolders(HashMap<Long,FolderInfo> folders); public void finishBindingItems(); public void bindAppWidget(LauncherAppWidgetInfo info); public void bindAllApplications(ArrayList<ApplicationInfo> apps); public void bindAppsAdded(ArrayList<ApplicationInfo> apps); public void bindAppsUpdated(ArrayList<ApplicationInfo> apps); public void bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent); public void bindPackagesUpdated(); public boolean isAllAppsVisible(); public void bindSearchablesChanged(); }简单的了解下每个方法的用途:
setLoadOnResume() 由于Launcher继承自Activity,因此Launcher可能会处于paused状态(onPause()被调用),则有可能在这段时间内资源可能
发生了改变,如应用被删除或新应用安装,因此需要在onResume()中调用此方法进行重新加载。
getCurrentWorkspace() 获取当前屏幕的序号
startBinding() 通知Launcher加载开始,并更新Workspace上的shortcuts
bindItems(ArrayList<ItemInfo> shortcuts, int start, int end) 加载一批内容项到Workspace,加载的内容项包括,Application、shortcut、folder。
bindFolders(HashMap<Long, FolderInfo> folders) 加载folder的内容
finishBindingItems() 通知Launcher加载结束。
bindAppWidget(LauncherAppWidgetInfo item) 加载AppWidget到Workspace
bindAllApplications(final ArrayList<ApplicationInfo> apps) 在All Apps页加载所有应用的Icon
bindAppsAdded(ArrayList<ApplicationInfo> apps) 通知Launcher一个新的应用被安装,并加载这个应用
bindAppsUpdated(ArrayList<ApplicationInfo> apps) 通知Launcher一个应用发生了更新
bindAppsRemoved(ArrayList<ApplicationInfo> apps, boolean permanent) 通知Launcher一个应用被删除了
bindPackagesUpdated() 通知Launcher多个应用发生了更新
isAllAppsVisible()用于在加载的过程中记录当前Launcher的状态,返回true则当前显示的All Apps
bindSearchablesChanged()当搜索/删除框状态发生改变时调用
了解了每个方法的作用之后,就可以开始进一步的分析了。
首先让我们回顾一下整个加载过程的流程是怎样的
通过在Launcher中调用LauncherModel.startLoader()方法,开始加载内容。
public void startLoader(Context context, boolean isLaunching) { synchronized (mLock) { ...... // Don't bother to start the thread if we know it's not going to do anything if (mCallbacks != null && mCallbacks.get() != null) { ...... mLoaderTask = new LoaderTask(context, isLaunching); sWorkerThread.setPriority(Thread.NORM_PRIORITY); sWorker.post(mLoaderTask); } } }mLoaderTask是一个Runnable,被添加到消息队列之后,它的run() 方法会被调用。
public void run() { ...... keep_running: { ...... if (loadWorkspaceFirst) { ...... loadAndBindWorkspace(); } else { ...... } if (mStopped) { break keep_running; } ...... waitForIdle(); // second step if (loadWorkspaceFirst) { ...... loadAndBindAllApps(); } else { ...... } ...... } ...... }
加载的工作由两部分组成,第一部分是为Workspace加载内容,第二部分则是为AllApps加载内容。每一部分的加载又可以分为两个步骤:1、由LauncherModel完成,主要
工作是从数据库中读取信息,并且按类别将内容项分装到不同的数据结构中。2、由Launcher来完成,通过LauncherModel.Callbacks接口定义的回调方法,从LauncherModel
中获取的数据,将其显示到桌面。
一、Workspace内容加载
run()中首先会调用loadAndBindWorkspace()方法开始Workspace的加载工作。
private void loadAndBindWorkspace() { ... if (!mWorkspaceLoaded) { loadWorkspace(); synchronized (LoaderTask.this) { if (mStopped) { return; } mWorkspaceLoaded = true; } } // Bind the workspace bindWorkspace(); }
因为WorkspaceLoaded=false,所以会调用loadWorkspace()读取内容数据,等数据读取完毕之后,再调用bindWorkspace()将数据
加载到Workspace中。
private void loadWorkspace() { ...... //存放container为CONTAINER_DESKTOP和CONTAINER_HOTSEAT类型的item sWorkspaceItems.clear(); //存放所有的AppWidget类型 sAppWidgets.clear(); //存放的FolderInfo.id和FolderInfo组成的映射对 sFolders.clear(); //所有的item的id和ItemInfo组成的映射对 sItemsIdMap.clear(); sDbIconCache.clear(); final ArrayList<Long> itemsToRemove = new ArrayList<Long>(); final Cursor c = contentResolver.query( LauncherSettings.Favorites.CONTENT_URI, null, null, null, null); // +1 for the hotseat (it can be larger than the workspace) // Load workspace in reverse order to ensure that latest items are loaded first (and // before any earlier duplicates) //代表屏幕中的每一个单位的方格是否被占用。 //第一维表示分屏的序号,其中最后一个代表Hotseat //第二维表示x方向方格的序号 //第三维表示y方向方格的序号 final ItemInfo occupied[][][] = new ItemInfo[Launcher.SCREEN_COUNT + 1][mCellCountX + 1][mCellCountY + 1]; try { ...... while (!mStopped && c.moveToNext()) { try { int itemType = c.getInt(itemTypeIndex); switch (itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: intentDescription = c.getString(intentIndex); try { intent = Intent.parseUri(intentDescription, 0); } catch (URISyntaxException e) { continue; } if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { info = getShortcutInfo(manager, intent, context, c, iconIndex, titleIndex, mLabelCache); } else { info = getShortcutInfo(c, context, iconTypeIndex, iconPackageIndex, iconResourceIndex, iconIndex, titleIndex); } if (info != null) { ...... // check & update map of what's occupied //检查这个item所占的空间是否空闲,true表示空闲 if (!checkItemPlacement(occupied, info)) { break; } switch (container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: case LauncherSettings.Favorites.CONTAINER_HOTSEAT: //当加载的item类型为ITEM_TYPE_APPLICATION或者ITEM_TYPE_SHORTCUT //并且所属的container为CONTAINER_DESKTOP或者CONTAINER_HOTSEAT时 //将其添加到sWorkspaceItems中 sWorkspaceItems.add(info); break; default: // Item is in a user folder //如果item的container不是上述两者,则代表它处于一个folder中 //将其添加到所属的folderInfo中 FolderInfo folderInfo = findOrMakeFolder(sFolders, container); folderInfo.add(info); break; } //所有的ITEM_TYPE_APPLICATION和ITEM_TYPE_SHORTCUT类型的item都需要 //加入到sItemsIdMap的映射对中。 sItemsIdMap.put(info.id, info); // now that we've loaded everthing re-save it with the // icon in case it disappears somehow. queueIconToBeChecked(sDbIconCache, info, c, iconIndex); } else { // Failed to load the shortcut, probably because the // activity manager couldn't resolve it (maybe the app // was uninstalled), or the db row was somehow screwed up. // Delete it. id = c.getLong(idIndex); Log.e(TAG, "Error loading shortcut " + id + ", removing it"); contentResolver.delete(LauncherSettings.Favorites.getContentUri( id, false), null, null); } break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: id = c.getLong(idIndex); FolderInfo folderInfo = findOrMakeFolder(sFolders, id); ..... // check & update map of what's occupied if (!checkItemPlacement(occupied, folderInfo)) { break; } switch (container) { case LauncherSettings.Favorites.CONTAINER_DESKTOP: case LauncherSettings.Favorites.CONTAINER_HOTSEAT: //folderInfo类型的item也需要添加到sWorkspaceItems中 sWorkspaceItems.add(folderInfo); break; } //添加到sItemsIdMap映射对中 sItemsIdMap.put(folderInfo.id, folderInfo); //添加到sFolder映射对中 sFolders.put(folderInfo.id, folderInfo); break; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: // Read all Launcher-specific widget details int appWidgetId = c.getInt(appWidgetIdIndex); id = c.getLong(idIndex); final AppWidgetProviderInfo provider = widgets.getAppWidgetInfo(appWidgetId); if (!isSafeMode && (provider == null || provider.provider == null || provider.provider.getPackageName() == null)) { ...... itemsToRemove.add(id); } else { appWidgetInfo = new LauncherAppWidgetInfo(appWidgetId); ...... container = c.getInt(containerIndex); ...... appWidgetInfo.container = c.getInt(containerIndex); // check & update map of what's occupied if (!checkItemPlacement(occupied, appWidgetInfo)) { break; } //添加到sItemsIdMap映射对 sItemsIdMap.put(appWidgetInfo.id, appWidgetInfo); //添加到sAppWidgets sAppWidgets.add(appWidgetInfo); } break; } } catch (Exception e) { ...... } } } finally { c.close(); } ...... }
loadWorkspace的工作就是从ContentProvider获取指定URI中的数据,并将它们分类存放到指定的数据结构中。分类的标准有两条:1、item的类型。包括ITEM_TYPE_APPLICATION ,ITEM_TYPE_SHORTCUT ,ITEM_TYPE_FOLDER,ITEM_TYPE_APPWIDGET四类。2、item所属的容器。包括CONTAINER_DESKTOP,
CONTAINER_HOTSEAT以及其它(主要指文件夹)。LauncherModel在读取完数据之后,通过LauncherModel.bindWorkspace()将数据传给到Launcher。进入LauncherModel.bindWorkspace()中:
private void bindWorkspace() { ...... mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //开始绑定 callbacks.startBinding(); } } }); ...... for (int i=0; i<N; i+=ITEMS_CHUNK) { final int start = i; final int chunkSize = (i+ITEMS_CHUNK <= N) ? ITEMS_CHUNK : (N-i); mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //绑定application、shortcut、folder三种内容 callbacks.bindItems(workspaceItems, start, start+chunkSize); } } }); } ...... mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //绑定folder callbacks.bindFolders(folders); } } }); ...... for (int i=0; i<N; i++) { final LauncherAppWidgetInfo widget = sAppWidgets.get(i); if (widget.screen == currentScreen) { mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //绑定当前屏的AppWidget callbacks.bindAppWidget(widget); } } }); } } for (int i=0; i<N; i++) { final LauncherAppWidgetInfo widget = sAppWidgets.get(i); if (widget.screen != currentScreen) { mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //绑定其它屏的AppWidget callbacks.bindAppWidget(widget); } } }); } } mHandler.post(new Runnable() { public void run() { Callbacks callbacks = tryGetCallbacks(oldCallbacks); if (callbacks != null) { //结束绑定 callbacks.finishBindingItems(); } } }); ...... }
可以看到,Launcher的内容绑定分为五步:分别对应着startBinding()、bindItems()、bindFolders()、bindAppWidgets()、finishBindingItems()的调用
Step1:调用Callbacks.startBinding()
由于Launcher实现了Callbacks接口,Launcher中的startBinding()被调用,进入Launcher.startBinding();
/** * Refreshes the shortcuts shown on the workspace. * * Implementation of the method from LauncherModel.Callbacks. */ public void startBinding() { final Workspace workspace = mWorkspace; mWorkspace.clearDropTargets(); int count = workspace.getChildCount(); for (int i = 0; i < count; i++) { // Use removeAllViewsInLayout() to avoid an extra requestLayout() and invalidate(). final CellLayout layoutParent = (CellLayout) workspace.getChildAt(i); layoutParent.removeAllViewsInLayout(); } if (mHotseat != null) { mHotseat.resetLayout(); } }
从方法中的内容我们可以看到,当被通知开始加载Workspace中内容时,Launcher重置了Workspace中的内容,Hotseat也通过resetLayout方法进行重置。
void resetLayout() { mContent.removeAllViewsInLayout(); // Add the Apps button Context context = getContext(); LayoutInflater inflater = LayoutInflater.from(context); BubbleTextView allAppsButton = (BubbleTextView) inflater.inflate(R.layout.application, mContent, false); ...... // Note: We do this to ensure that the hotseat is always laid out in the orientation of // the hotseat in order regardless of which orientation they were added int x = getCellXFromOrder(sAllAppsButtonRank); int y = getCellYFromOrder(sAllAppsButtonRank); mContent.addViewToCellLayout(allAppsButton, -1, 0, new CellLayout.LayoutParams(x,y,1,1), true); }
Hotseat中清空了装载的内容,然后重新加载allAppsButton。从这里也可以看到allAppsButton是固定到了Hotseat中,不同于Hotseat中的其他控件。
Step2:调用Callbacks.bindItems(ArrayList<ItemInfo> shortcuts, int start, int end)
准备工作完成之后,现在可以开始正式的加载工作了,首先被调用的是bindItems()方法
/** * Bind the items start-end from the list. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindItems(ArrayList<ItemInfo> shortcuts, int start, int end) { setLoadOnResume(); final Workspace workspace = mWorkspace; for (int i=start; i<end; i++) { final ItemInfo item = shortcuts.get(i); ...... switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: View shortcut = createShortcut((ShortcutInfo)item); workspace.addInScreen(shortcut, item.container, item.screen, item.cellX, item.cellY, 1, 1, false); break; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: FolderIcon newFolder = FolderIcon.fromXml(R.layout.folder_icon, this, (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), (FolderInfo) item, mIconCache); workspace.addInScreen(newFolder, item.container, item.screen, item.cellX, item.cellY, 1, 1, false); break; } } workspace.requestLayout(); }通过这个方法,将application、shortcut、folder三种item通过Workspace.addInScreen()添加到Workspace中
/** * 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 screen 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. * @param insert When true, the child is inserted at the beginning of the children list. */ void addInScreen(View child, long container, int screen, int x, int y, int spanX, int spanY, boolean insert) { ...... //Workspace一共有五个分屏,每个分屏是一个CellLayout final CellLayout layout; if (container == LauncherSettings.Favorites.CONTAINER_HOTSEAT) { layout = mLauncher.getHotseat().getLayout(); child.setOnKeyListener(null); ...... if (screen < 0) { screen = mLauncher.getHotseat().getOrderInHotseat(x, y); } else { // Note: We do this to ensure that the hotseat is always laid out in the orientation // of the hotseat in order regardless of which orientation they were added //获取child的位置,返回true添加成功,false失败 x = mLauncher.getHotseat().getCellXFromOrder(screen); y = mLauncher.getHotseat().getCellYFromOrder(screen); } } else { // Show folder title if not in the hotseat if (child instanceof FolderIcon) { ((FolderIcon) child).setTextVisible(true); } layout = (CellLayout) getChildAt(screen); child.setOnKeyListener(new IconKeyEventListener()); } CellLayout.LayoutParams lp = (CellLayout.LayoutParams) child.getLayoutParams(); if (lp == null) { lp = new CellLayout.LayoutParams(x, y, spanX, spanY); } else { lp.cellX = x; lp.cellY = 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 int childId = LauncherModel.getCellLayoutChildId(container, screen, x, y, spanX, spanY); boolean markCellsAsOccupied = !(child instanceof Folder); //将child添加到CellLayout中去 if (!layout.addViewToCellLayout(child, insert ? 0 : -1, childId, lp, markCellsAsOccupied)) { ...... } if (!(child instanceof Folder)) { child.setHapticFeedbackEnabled(false); child.setOnLongClickListener(mLongClickListener); } if (child instanceof DropTarget) { mDragController.addDropTarget((DropTarget) child); } }
通过addInScreen()就能将child添加到指定的CellLayout中去。CellLayout共有六个,Workspace中五个,Hotseat一个。
Step3:调用Callbacks.bindFolders(HashMap<Long, FolderInfo> folders)
Launcher.bindFolders()中的代码只有三行:
public void bindFolders(HashMap<Long, FolderInfo> folders) { setLoadOnResume(); sFolders.clear(); sFolders.putAll(folders); }获取到当前的Folder的映射表。
Step4:调用Callbacks.bindAppWidgets(LauncherAppWidgetInfo item)
现在开始加载AppWidget到Workspace:
/** * Add the views for a widget to the workspace. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindAppWidget(LauncherAppWidgetInfo item) { setLoadOnResume(); ...... final Workspace workspace = mWorkspace; final int appWidgetId = item.appWidgetId; final AppWidgetProviderInfo appWidgetInfo = mAppWidgetManager.getAppWidgetInfo(appWidgetId); ...... item.hostView = mAppWidgetHost.createView(this, appWidgetId, appWidgetInfo); item.hostView.setAppWidget(appWidgetId, appWidgetInfo); item.hostView.setTag(item); workspace.addInScreen(item.hostView, item.container, item.screen, item.cellX, item.cellY, item.spanX, item.spanY, false); addWidgetToAutoAdvanceIfNeeded(item.hostView, appWidgetInfo); workspace.requestLayout(); ...... }
先获取到AppWidget的相关信息之后,调用Workspace.addInScreen()添加到Workspace。AppWidget是Android系统的一大特色,可
以在桌面上快捷的获取实时信息和对一些指定应用进行控制。AppWidget是需要自动更新的(如果应用中设置了更新),因此除了
将其添加到桌面我们需要更具需要设置自动更新。进而调用addWidgetToAutoAdvanceifNeeded()来实现此功能,关于如何实现自动
更新AppWidget的话题,本文暂不做分析。bindAppWidgets()一共被调用两次,这样做的目的是增加流畅感,第一次调用的时候为
当前显示的分屏添加AppWidget,第二次调用的时候为其他未显示的分屏添加AppWidget。这样就给用户带来了一种流畅的用户体验。
Step5:调用Callbacks.finishBindingItems()
通过上面的操作,所有item就已经悉数被添加到Workspace当中,此时调用finishBindingItems()通知Launcher添加完毕。
/** * Callback saying that there aren't any more items to bind. * * Implementation of the method from LauncherModel.Callbacks. */ public void finishBindingItems() { setLoadOnResume(); ...... mWorkspaceLoading = false; // If we received the result of any pending adds while the loader was running (e.g. the // widget configuration forced an orientation change), process them now. for (int i = 0; i < sPendingAddList.size(); i++) { completeAdd(sPendingAddList.get(i)); } sPendingAddList.clear(); ...... mWorkspace.post(mBuildLayersRunnable); }
当Workspace正在加载的时候,有一些操作发生却还未执行,在finishBindingItems()中来执行这些操作,调用completeAdd()来完成
还未来得及完成的操作。紧接着又向Workspace的消息队列里加入了mBuildLayersRunnable,mBuildLayersRunnable是Runnable的
一个实例,它的功能就是迫使每个View都完成渲染的工作,即及时的现实到桌面中现好了所有需要的内容了。那下一步就是需要向
All Apps页中加载内容了。
二、AllApps的内容加载
回到mLoaderTask.run()方法中,当bindWorkspace()执行结束之后,并通过waitForIdle()确认加载完成之后,就会调用
loadAndBindAllApps()来为AllApps页面加载内容。
private void loadAndBindAllApps() { ...... if (!mAllAppsLoaded) { //批量加载app和widget信息 loadAllAppsByBatch(); ...... } else { //无需重复加载,直接绑定 onlyBindAllApps(); } }
AllApps中的加载过程和Workspace中的加载过程大致是相同的,只是All Apps的加载和绑定过程被放到同一个方法loadAllAppsByBatch()中执行:
/** *批量的向加载内容 */ private void loadAllAppsByBatch() { ...... //设置Intent的action为ACTION_MAIN,category为CATEGORY_LAUNCHER //这样就筛选出桌面上显示的启动项了。 final Intent mainIntent = new Intent(Intent.ACTION_MAIN, null); mainIntent.addCategory(Intent.CATEGORY_LAUNCHER); final PackageManager packageManager = mContext.getPackageManager(); List<ResolveInfo> apps = null; int N = Integer.MAX_VALUE; int startIndex; int i=0; int batchSize = -1; while (i < N && !mStopped) { if (i == 0) { mAllAppsList.clear(); ...... //查询所有应该在桌面上显示的app apps = packageManager.queryIntentActivities(mainIntent, 0); ...... N = apps.size(); ...... if (mBatchSize == 0) { //mBatchSize==0表示一次性加载所有的应用 batchSize = N; } else { batchSize = mBatchSize; } ...... //将获取到的app的信息按名字进行排序 Collections.sort(apps, new LauncherModel.ShortcutNameComparator(packageManager, mLabelCache)); ...... } ...... startIndex = i; //添加一批应用信息到mAllAppsList,每一批添加N个 for (int j=0; i<N && j<batchSize; j++) { // This builds the icon bitmaps. mAllAppsList.add(new ApplicationInfo(packageManager, apps.get(i), mIconCache, mLabelCache)); i++; } //i < batchSize表示添加的是第一批信息 final boolean first = i <= batchSize; final Callbacks callbacks = tryGetCallbacks(oldCallbacks); final ArrayList<ApplicationInfo> added = mAllAppsList.added; //每添加完一批之后,将added重新清空 mAllAppsList.added = new ArrayList<ApplicationInfo>(); mHandler.post(new Runnable() { public void run() { ...... //Launcher实现了Callbacks接口,将获取到的数据回调给Launcher if (callbacks != null) { if (first) { callbacks.bindAllApplications(added); } else { callbacks.bindAppsAdded(added); } ...... } else { ...... } } }); ...... } ...... }
过程还是挺简单的,首先当然是查询所有的App了,通过向PackagedManager发送指定的Intent就能够获得安装好的应用的信息。查
询完毕之后,将数据封装到ArrayList<ApplicationInfo>对象中,然后通过Callbacks.bindAllApplication()或Callbacks.bindAppsAdded()
将数据传给Launcher。Launcher中的操作也比加载Workspace时简单多,毕竟这里只需要加载Icon。
/** * Add the icons for all apps. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindAllApplications(final ArrayList<ApplicationInfo> apps) { ...... // We just post the call to setApps so the user sees the progress bar // disappear-- otherwise, it just looks like the progress bar froze // which doesn't look great mAppsCustomizeTabHost.post(new Runnable() { public void run() { if (mAppsCustomizeContent != null) { mAppsCustomizeContent.setApps(apps); } } }); }
/** * A package was installed. * * Implementation of the method from LauncherModel.Callbacks. */ public void bindAppsAdded(ArrayList<ApplicationInfo> apps) { setLoadOnResume(); ...... if (mAppsCustomizeContent != null) { mAppsCustomizeContent.addApps(apps); } }
这样All Apps页面的加载也完成了。
到这一步,Launcher内容的加载过程也就完成了。
- 1楼hehuihehui3天前 17:29
- 好 共同学习 qq : 8 1 5 5 36 52