1. Workspace 介绍 在 Android 手机上,我们通常说的桌面其实就是 launcher ,再往小了说就是: Workspace 。 Workspace 是桌面在实现时的抽象定义。桌面上显示的应用图标、文件夹和小部件都是显示在 Workspace 中的,我们可以增删应用快捷图标,增删文件夹,增删小部件。
在手机重启或关机后 Workspace 中这么多 Widget 的状态怎么保存呢?
答案是:launcher 使用了一个专门的数据库保存了这些 Widget 的状态,以便下次重启后依然能按照最新的变动显示。
下面从 launcher.db 数据库创建 、 Workspace 数据加载这两点展开分析。
2. launcher.db 数据库创建 launcher.db 的创建得从 LauncherProvider 展开,在该类中可以看到 LauncherProvider#createDbIfNotExists() 方法:packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 protected synchronized void createDbIfNotExists () { if (mOpenHelper == null ) { mOpenHelper = DatabaseHelper.createDatabaseHelper( getContext(), false ); RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper); } }
在整个 Launcher 只有这一个位置实例化了 DatabaseHelper ,而且在对数据库进行操作时都会调用到 LauncherProvider#createDbIfNotExists() . 接着看 LauncherProvider.DatabaseHelper#createDatabaseHelper() :
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 static DatabaseHelper createDatabaseHelper (Context context, boolean forMigration) { return createDatabaseHelper(context, null , forMigration); } static DatabaseHelper createDatabaseHelper (Context context, String dbName, boolean forMigration) { if (dbName == null ) { dbName = InvariantDeviceProfile.INSTANCE.get(context).dbFile; } DatabaseHelper databaseHelper = new DatabaseHelper (context, dbName, forMigration); if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) { databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true ); } databaseHelper.mHotseatRestoreTableExists = tableExists( databaseHelper.getReadableDatabase(), Favorites.HYBRID_HOTSEAT_BACKUP_TABLE); databaseHelper.initIds(); return databaseHelper; }
到此数据库就创建完成了,接下来就是建表。LauncherProvider.DatabaseHelper#onCreate() :
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public void onCreate (SQLiteDatabase db) { if (LOGD) Log.d(TAG, "creating new launcher database" ); mMaxItemId = 1 ; addFavoritesTable(db, false ); mMaxItemId = initializeMaxItemId(db); if (!mForMigration) { onEmptyDbCreated(); } } protected void onEmptyDbCreated () { Utilities.getPrefs(mContext).edit().putBoolean(getKey(EMPTY_DATABASE_CREATED), true ) .commit(); }
实际建表操作在 LauncherProvider.DatabaseHelper#onCreate() 方法里,但在 LauncherProvider.DatabaseHelper#createDatabaseHelper() 里也有个同样得建表操作,注意这里:是不会重复建表得,有相应得判断。
onEmptyDbCreated() 方法中记录了一个 EMPTY_DATABASE_CREATED 标记,表示空数据库创建了。该标记在 loadWorkspace 时, loadDefaultFavoritesIfNecessary 方法用到了此标记:
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 synchronized private void loadDefaultFavoritesIfNecessary () { SharedPreferences sp = Utilities.getPrefs(getContext()); if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false )) { clearFlagEmptyDbCreated(); } } private void clearFlagEmptyDbCreated () { Utilities.getPrefs(getContext()).edit() .remove(mOpenHelper.getKey(EMPTY_DATABASE_CREATED)).commit(); }
这里使用这个标记判断是否需要加载默认的 workspace 配置数据到数据库,最后一行代码 clearFlagEmptyDbCreated() 方法调用,用于清空了这个标记,下次就不需要再次加载了。
从中得出一个结论, launcher正常在首次加载时,才会加载默认配置到数据库,其他情况是不会加载的 。
3. Workspace 数据加载 Workspace 的数据加载在 LoaderTask#loadWorkspace() 方法开始的。
LoaderTask#loadWorkspace() :
packages/apps/Launcher3/src/com/android/launcher3/model/LoaderTask.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 protected void loadWorkspace ( List<ShortcutInfo> allDeepShortcuts, Uri contentUri, String selection, @Nullable LoaderMemoryLogger logger) { 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 (!GridSizeMigrationTaskV2.migrateGridIfNeeded(context)) { clearDb = true ; } if (clearDb) { LauncherSettings.Settings.call(contentResolver, LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB); } Log.d(TAG, "loadWorkspace: loading default favorites" ); LauncherSettings.Settings.call(contentResolver, LauncherSettings.Settings.METHOD_LOAD_DEFAULT_FAVORITES); }
上述代码分为两个重点位置 :
加载布局
获取数据库信息
1. 先看第一点:加载布局
注意:LauncherProvider#call() 方法这里就补贴出来了,自己去看。
上述 LauncherSettings.Settings.call() 方法的实现在 LauncherProvider 中,该方法是:读取布局的方法,桌面布局有默认布局和自定义布局。默认布局是在首次开机,恢复出厂设置,清空桌面数据的时候;Launcher 运行期间会把桌面布局存在数据库里,而开机时会去读取数据库,根据数据库来决定布局。
LauncherProvider#call() 方法每次执行时,都会执行 createDbIfNotExists() 检查是否有数据库,如果没有则创建一次数据库。 即如果数据库为空就会创建数据库;实际使用时,在首次开机,恢复出厂设置,清空桌面数据的时候数据库为空,这种情况下就会创建一个空的数据库。
LauncherProvider#createDbIfNotExists() :
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 protected synchronized void createDbIfNotExists () { if (mOpenHelper == null ) { mOpenHelper = DatabaseHelper.createDatabaseHelper( getContext(), false ); RestoreDbTask.restoreIfNeeded(getContext(), mOpenHelper); } static DatabaseHelper createDatabaseHelper (Context context, String dbName, boolean forMigration) { if (!tableExists(databaseHelper.getReadableDatabase(), Favorites.TABLE_NAME)) { databaseHelper.addFavoritesTable(databaseHelper.getWritableDatabase(), true ); } }
根据上述代码接着看 LauncherProvider#addFavoritesTable() :
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 private void addFavoritesTable (SQLiteDatabase db, boolean optional) { Favorites.addTableToDb(db, getDefaultUserSerial(), optional); } 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," + "iconPackage TEXT," + "iconResource TEXT," + "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等
在 loadWorkspace() 的开始实际进行的第一个操作是:判断是否有桌面布局数据库,从而好读取数据。如果没有用户布局数据则采用 loadDefaultFavoritesIfNecessary() 方法。实际上没有用户布局数据的场景就是第一次创建数据库的场景。所以 loadDefaultFavoritesIfNecessary() 的含义是读取默认布局,仅在首次开机,恢复出厂设置或清除 Launcher 数据的时候使用。
接着看 LauncherProvider#loadDefaultFavoritesIfNecessary() :
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 synchronized private void loadDefaultFavoritesIfNecessary () { SharedPreferences sp = Utilities.getPrefs(getContext()); if (sp.getBoolean(mOpenHelper.getKey(EMPTY_DATABASE_CREATED), false )) { Log.d(TAG, "loading default workspace" ); AppWidgetHost widgetHost = mOpenHelper.newLauncherWidgetHost(); AutoInstallsLayout loader = createWorkspaceLoaderFromAppRestriction(widgetHost); if (loader == null ) { loader = AutoInstallsLayout.get(getContext(),widgetHost, mOpenHelper); } if (loader == null ) { final Partner partner = Partner.get(getContext().getPackageManager()); if (partner != null && partner.hasDefaultLayout()) { final Resources partnerRes = partner.getResources(); int workspaceResId = partnerRes.getIdentifier(Partner.RES_DEFAULT_LAYOUT, "xml" , partner.getPackageName()); if (workspaceResId != 0 ) { loader = new DefaultLayoutParser (getContext(), mOpenHelper.mAppWidgetHost, mOpenHelper, partnerRes, workspaceResId); } } } final boolean usingExternallyProvidedLayout = loader != null ; if (loader == null ) { loader = getDefaultLayoutParser(widgetHost); } mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); if ((mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), loader) <= 0 ) && usingExternallyProvidedLayout) { mOpenHelper.createEmptyDB(mOpenHelper.getWritableDatabase()); mOpenHelper.loadFavorites(mOpenHelper.getWritableDatabase(), getDefaultLayoutParser(widgetHost)); } clearFlagEmptyDbCreated(); } }
通过上面代码可知: loadDefaultFavoritesIfNecessary() 方法的作用为:获取 loader (布局),和将读取的布局存入数据库。
获取 AutoInstallsLayout 方法,首先获取 layoutName ,这个名字就是xml名字。在原生代码 res/xml/ 文件夹下面有 default_workspace.xml 、default_workspace_3x3.xml、 default_workspace_4x4.xml、default_workspace_5x5.xml、default_workspace_5x6.xml 一共5个布局文件。
下面则是采用 多个方式 来获取布局 xml,因为不知道 xml 文件的具体名字所以采用递进的方法来获取。
先看第一种 :应用约束,调用 createWorkspaceLoaderFromAppRestriction() ,获取用户设置的一组用于限制应用功能的 Bundle 串,获取 Bundle 里 workspace.configuration.package.name 具体的应用包名,获取 WorkSpace 默认配置资源。 LauncherProvider#createWorkspaceLoaderFromAppRestriction(widgetHost) :
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private AutoInstallsLayout createWorkspaceLoaderFromAppRestriction (AppWidgetHost widgetHost) { Context ctx = getContext(); final String authority; if (!TextUtils.isEmpty(mProviderAuthority)) { authority = mProviderAuthority; } else { authority = Settings.Secure.getString(ctx.getContentResolver(), "launcher3.layout.provider" ); } if (TextUtils.isEmpty(authority)) { return null ; } ProviderInfo pi = ctx.getPackageManager().resolveContentProvider(authority, 0 ); if (pi == null ) { return null ; } Uri uri = getLayoutUri(authority, ctx); try (InputStream in = ctx.getContentResolver().openInputStream(uri)) { String layout = new String (IOUtils.toByteArray(in)); XmlPullParser parser = Xml.newPullParser(); parser.setInput(new StringReader (layout)); return new AutoInstallsLayout (ctx, widgetHost, mOpenHelper, ctx.getPackageManager().getResourcesForApplication(pi.applicationInfo), () -> parser, AutoInstallsLayout.TAG_WORKSPACE); } catch (Exception e) { Log.e(TAG, "Error getting layout stream from: " + authority , e); return null ; } }
再看第二种 :从 intent 关键字 ACTION_LAUNCHER_CUSTOMIZATION 即是 “android.autoinstalls.config.action.PLAY_AUTO_INSTALL” 来获取,autoinstall 可以在手机中集成对应工具,这样默认布局除了手机自带的应用外,还可以提供一些自动下载的应用。
AutoInstallsLayout#get() :packages/apps/Launcher3/src/com/android/launcher3/AutoInstallsLayout.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 static AutoInstallsLayout get (Context context, AppWidgetHost appWidgetHost, LayoutParserCallback callback) { Pair<String, Resources> customizationApkInfo = PackageManagerHelper.findSystemApk( ACTION_LAUNCHER_CUSTOMIZATION, context.getPackageManager()); if (customizationApkInfo == null ) { return null ; } String pkg = customizationApkInfo.first; Resources targetRes = customizationApkInfo.second; InvariantDeviceProfile grid = LauncherAppState.getIDP(context); String layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES_WITH_HOSTEAT, grid.numColumns, grid.numRows, grid.numDatabaseHotseatIcons); int layoutId = targetRes.getIdentifier(layoutName, "xml" , pkg); if (layoutId == 0 ) { Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying layout without hosteat" ); layoutName = String.format(Locale.ENGLISH, FORMATTED_LAYOUT_RES, grid.numColumns, grid.numRows); layoutId = targetRes.getIdentifier(layoutName, "xml" , pkg); } if (layoutId == 0 ) { Log.d(TAG, "Formatted layout: " + layoutName + " not found. Trying the default layout" ); layoutId = targetRes.getIdentifier(LAYOUT_RES, "xml" , pkg); } if (layoutId == 0 ) { Log.e(TAG, "Layout definition not found in package: " + pkg); return null ; } return new AutoInstallsLayout (context, appWidgetHost, callback, targetRes, layoutId, TAG_WORKSPACE); }
总之: AutoInstallsLayout.get() 根据传入的参数,读取对应的xml文件 。
再看第三种 :从系统内置的 partner 应用里获取workspace默认配置。 这种就不过多介绍了。
看第四种 :是最常用的一种,我们能控制的本地布局,调用 getDefaultLayoutParser() 获取我们 Launcher 里的默认资源。
packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 private DefaultLayoutParser getDefaultLayoutParser (AppWidgetHost widgetHost) { int defaultLayout = LauncherAppState.getIDP(getContext()).defaultLayoutId; return new DefaultLayoutParser (getContext(), widgetHost, mOpenHelper, getContext().getResources(), defaultLayout); }
packages/apps/Launcher3/src/com/android/launcher3/LauncherAppState.java
1 2 3 public static InvariantDeviceProfile getIDP (Context context) { return LauncherAppState.getInstance(context).getInvariantDeviceProfile(); }
loadDefaultFavoritesIfNecessary() 方法又分为:读取布局、存储布局。
存储布局的主要方法是: loadFavorites() ,由于文章过于长了,这里就不在作分析了。
2. 获取数据库信息 回到开始的 LoaderTask#loadWorkspace() 方法。
该类剩下部分的代码还是非常多,后面将拆开分析。
LoaderTask#loadWorkspace() packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 protected void loadWorkspace ( List<ShortcutInfo> allDeepShortcuts, Uri contentUri, String selection, @Nullable LoaderMemoryLogger logger) { 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); Map<ShortcutKey, ShortcutInfo> shortcutKeyToPinnedShortcuts = new HashMap <>(); final LoaderCursor c = new LoaderCursor ( contentResolver.query(contentUri, null , selection, null , null ), contentUri, mApp, mUserManagerState); final Bundle extras = c.getExtras(); mDbName = extras == null ? null : extras.getString(LauncherSettings.Settings.EXTRA_DB_NAME); try { final int appWidgetIdIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.APPWIDGET_ID); final int appWidgetProviderIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.APPWIDGET_PROVIDER); final int spanXIndex = c.getColumnIndexOrThrow (LauncherSettings.Favorites.SPANX); final int spanYIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.SPANY); final int rankIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.RANK); final int optionsIndex = c.getColumnIndexOrThrow( LauncherSettings.Favorites.OPTIONS); } } ``` 上述代码创建了 **LoaderCursor** 游标,用于暂时存储从数据库中提取的数据块,且创建是根据 table 名字来获取对应的数据库 **table**, 这里的名字是 **Favorites**。 接着看下 **LoaderCursor** 的构造方法: **LoaderCursor#LoaderCursor()** **packages/apps/Launcher3/src/com/android/launcher3/model/LoaderCursor.java** ```java public LoaderCursor (Cursor cursor, Uri contentUri, LauncherAppState app, UserManagerState userManagerState) { super (cursor); allUsers = userManagerState.allUsers; mContentUri = contentUri; mContext = app.getContext(); mIconCache = app.getIconCache(); mIDP = app.getInvariantDeviceProfile(); mPM = mContext.getPackageManager(); iconIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON); iconPackageIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_PACKAGE); iconResourceIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ICON_RESOURCE); titleIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.TITLE); idIndex = getColumnIndexOrThrow(LauncherSettings.Favorites._ID); containerIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CONTAINER); itemTypeIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.ITEM_TYPE); screenIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.SCREEN); cellXIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLX); cellYIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.CELLY); profileIdIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.PROFILE_ID); restoredIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.RESTORED); intentIndex = getColumnIndexOrThrow(LauncherSettings.Favorites.INTENT); }
整个构造器,定义了数据库中的所有词条,后面则使用这些词条来获取相应参数。
回到 loadWorkspace() ,看后面的部分。LoaderTask#loadWorkspace() packages/apps/Launcher3/src/com/android/launcher3/LauncherProvider.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 protected void loadWorkspace ( List<ShortcutInfo> allDeepShortcuts, Uri contentUri, String selection, @Nullable LoaderMemoryLogger logger) { synchronized (mBgDataModel) { while (!mStopped && c.moveToNext()) { try { if (c.user == null ) { c.markDeleted("User has been deleted" ); continue ; } boolean allowMissingTarget = false ; switch (c.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: intent = c.parseIntent(); if (intent == null ) { c.markDeleted("Invalid or null intent" ); continue ; } int disabledState = mUserManagerState.isUserQuiet(c.serialNumber) ? WorkspaceItemInfo.FLAG_DISABLED_QUIET_USER : 0 ; ComponentName cn = intent.getComponent(); targetPkg = cn == null ? intent.getPackage() : cn.getPackageName(); if (TextUtils.isEmpty(targetPkg) && c.itemType != LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT) { c.markDeleted("Only legacy shortcuts can have null package" ); continue ; } boolean validTarget = TextUtils.isEmpty(targetPkg) || mLauncherApps.isPackageEnabled(targetPkg, c.user); if (cn != null && validTarget && c.itemType != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { if (mLauncherApps.isActivityEnabled(cn, c.user)) { c.markRestored(); } else { intent = pmHelper.getAppLaunchIntent(targetPkg, c.user); if (intent != null ) { c.restoreFlag = 0 ; c.updater().put( LauncherSettings.Favorites.INTENT, intent.toUri(0 )).commit(); cn = intent.getComponent(); } else { c.markDeleted("Unable to find a launch target" ); continue ; } } } if (!TextUtils.isEmpty(targetPkg) && !validTarget) { if (c.restoreFlag != 0 ) { tempPackageKey.update(targetPkg, c.user); if (c.hasRestoreFlag(WorkspaceItemInfo.FLAG_RESTORE_STARTED)) { } else if (installingPkgs.containsKey(tempPackageKey)) { c.restoreFlag |= WorkspaceItemInfo.FLAG_RESTORE_STARTED; c.updater().put(LauncherSettings.Favorites.RESTORED, c.restoreFlag).commit(); } else { c.markDeleted("Unrestored app removed: " + targetPkg); continue ; } } else if (pmHelper.isAppOnSdcard(targetPkg, c.user)) { disabledState |= WorkspaceItemInfo.FLAG_DISABLED_NOT_AVAILABLE; allowMissingTarget = true ; } else if (!isSdCardReady) { mPendingPackages.add(new PackageUserKey (targetPkg, c.user)); allowMissingTarget = true ; } else { c.markDeleted("Invalid package removed: " + targetPkg); continue ; } } if ((c.restoreFlag & WorkspaceItemInfo.FLAG_SUPPORTS_WEB_UI) != 0 ) { validTarget = false ; } if (validTarget) { c.markRestored(); } boolean useLowResIcon = !c.isOnWorkspaceOrHotseat(); if (c.restoreFlag != 0 ) { info = c.getRestoredItemInfo(intent); } else if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) { info = c.getAppShortcutInfo( intent, allowMissingTarget, useLowResIcon, !FeatureFlags.ENABLE_BULK_WORKSPACE_ICON_LOADING.get()); } else if (c.itemType == LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { ShortcutKey key = ShortcutKey.fromIntent(intent, c.user); if (unlockedUsers.get(c.serialNumber)) { ShortcutInfo pinnedShortcut = shortcutKeyToPinnedShortcuts.get(key); if (pinnedShortcut == null ) { c.markDeleted("Pinned shortcut not found" ); continue ; } info = new WorkspaceItemInfo (pinnedShortcut, context); 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 { info = c.loadSimpleWorkspaceItem(); info.runtimeStatusFlags |= FLAG_DISABLED_LOCKED_USER; } } else { info = c.loadSimpleWorkspaceItem(); if (!TextUtils.isEmpty(targetPkg) && pmHelper.isAppSuspended(targetPkg, c.user)) { disabledState |= FLAG_DISABLED_SUSPENDED; } info.options = c.getInt(optionsIndex); 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 != LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT) { iconRequestInfos.add( c.createIconRequestInfo(info, useLowResIcon)); } c.applyCommonProperties(info); info.intent = intent; info.rank = c.getInt(rankIndex); info.spanX = 1 ; info.spanY = 1 ; info.runtimeStatusFlags |= disabledState; if (isSafeMode && !isSystemApp(context, 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, logger); } else { throw new RuntimeException ("Unexpected null WorkspaceItemInfo" ); } break ; case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: FolderInfo folderInfo = mBgDataModel.findOrMakeFolder(c.id); c.applyCommonProperties(folderInfo); folderInfo.title = c.getString(c.titleIndex); folderInfo.spanX = 1 ; folderInfo.spanY = 1 ; folderInfo.options = c.getInt(optionsIndex); c.markRestored(); c.checkAndAddItem(folderInfo, mBgDataModel, logger); break ; case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: if (WidgetsModel.GO_DISABLE_WIDGETS) { c.markDeleted("Only legacy shortcuts can have null package" ); continue ; } case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: boolean customWidget = c.itemType == LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET; int appWidgetId = c.getInt(appWidgetIdIndex); String savedProvider = c.getString(appWidgetProviderIndex); final ComponentName component; boolean isSearchWidget = (c.getInt(optionsIndex) & LauncherAppWidgetInfo.OPTION_SEARCH_WIDGET) != 0 ; if (isSearchWidget) { component = QsbContainerView.getSearchComponentName(context); if (component == null ) { c.markDeleted("Discarding SearchWidget without packagename " ); continue ; } } 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 { if (isProviderReady) { appWidgetInfo = new LauncherAppWidgetInfo (appWidgetId, provider.provider); int status = c.restoreFlag & ~LauncherAppWidgetInfo.FLAG_RESTORE_STARTED & ~LauncherAppWidgetInfo.FLAG_PROVIDER_NOT_READY; if (!wasProviderReady) { 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)) { } else if (installProgress != null ) { appWidgetInfo.restoreStatus |= LauncherAppWidgetInfo.FLAG_RESTORE_STARTED; } else if (!isSafeMode) { c.markDeleted("Unrestored widget removed: " + component); continue ; } appWidgetInfo.installProgress = installProgress == null ? 0 : installProgress; } if (appWidgetInfo.hasRestoreFlag( LauncherAppWidgetInfo.FLAG_DIRECT_CONFIG)) { appWidgetInfo.bindOptions = c.parseIntent(); } c.applyCommonProperties(appWidgetInfo); appWidgetInfo.spanX = c.getInt(spanXIndex); appWidgetInfo.spanY = c.getInt(spanYIndex); appWidgetInfo.options = c.getInt(optionsIndex); appWidgetInfo.user = c.user; appWidgetInfo.sourceContainer = c.getInt(sourceContainerIndex); if (appWidgetInfo.spanX <= 0 || appWidgetInfo.spanY <= 0 ) { c.markDeleted("Widget has invalid size: " + appWidgetInfo.spanX + "x" + appWidgetInfo.spanY); continue ; } 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!" ); continue ; } if (!customWidget) { String providerName = appWidgetInfo.providerName.flattenToString(); if (!providerName.equals(savedProvider) || (appWidgetInfo.restoreStatus != c.restoreFlag)) { c.updater() .put(LauncherSettings.Favorites.APPWIDGET_PROVIDER, providerName) .put(LauncherSettings.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); } } mModelDelegate.loadItems(mUserManagerState, shortcutKeyToPinnedShortcuts); mModelDelegate.loadStringCache(mBgDataModel.stringCache); if (mStopped) { mBgDataModel.clear(); return ; } mItemsDeleted = c.commitDeleted(); 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(); for (int rank = 0 ; rank < size; ++rank) { WorkspaceItemInfo info = folder.contents.get(rank); info.rank = rank; if (info.usingLowResIcon() && info.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION && verifier.isItemInPreview(info.rank)) { mIconCache.getTitleAndIcon(info, false ); } } } c.commitRestoredItems(); } }
上述代码总结成:
通过 LauncherSettings.Favorites.CONTENT_URI 查询 Favorites 表的所有内容,拿到cursor。
遍历cursor,进行数据的整理。每一行数据都有一个对应的itemType,标志着这一行的数据对应的是一个应用、还是一个Widget或文件夹等。不同的类型会进行不同的处理。
对于图标类型( itemType 是ITEM_TYPE_SHORTCUT,ITEM_TYPE_APPLICATION,ITEM_TYPE_DEEP_SHORTCUT),首先经过一系列判断,判断其是否还可用(比如应用在 Launcher 未启动时被卸载导致不可用),不可用的话就标记为可删除,继续循环。如果可用的话,就根据当前 cursor 的内容,生成一个 ShortcutInfo 对象,保存到BgDataModel。
对于文件夹类型(itemType是ITEM_TYPE_FOLDER),直接生成一个对应的FolderInfo对象,保存到BgDataModel。
对于AppWidget(itemType是ITEM_TYPE_APPWIDGET,ITEM_TYPE_CUSTOM_APPWIDGET),也需要经过是否可用的判断,但是可用条件与图标类型是有差异的。如果可用,生成一个LauncherAppWidgetInfo对象,保存到BgDataModel。
所有数据库里读出的内容已经分类完毕,并且保存到了内存(BgDataModel)中。最后开始处理之前标记为可删除的内容。显示从数据库中删除对应的行,然后还要判断此次删除操作是否带来了其他需要删除的内容。比如某个文件夹或者某一页只有一个图标,这个图标因为某些原因被删掉了,那么此文件夹或页面也需要被删掉。
4. Workspace 数据绑定 这一步将 sBgDataModel 中的图标放到桌面上。 放置的时候为了提高用户体现,优先放置当前屏幕的图标和 widget,然后再放其他屏幕的图标和 widget,这样用户能更快的看到图标显示完成。BaseLoaderResults#bindWorkspace()
//BaseLoaderResults.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void bindWorkspace (boolean incrementBindId) { ArrayList<ItemInfo> workspaceItems = new ArrayList <>(); ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList <>(); final IntArray orderedScreenIds = new IntArray (); ArrayList<FixedContainerItems> extraItems = new ArrayList <>(); 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; } for (Callbacks cb : mCallbacksList) { new WorkspaceBinder (cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId, workspaceItems, appWidgets, extraItems, orderedScreenIds).bind(); } }
上述代码做了两个操作:一个优先找出当前屏幕、二个绑定操作。 这里重点关注绑定操作 BaseLoaderResults.WorkspaceBinder#bind() :
// BaseLoaderResults.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 private void bind () { final IntSet currentScreenIds = mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds); Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks); ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList <>(); ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList <>(); ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList <>(); ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList <>(); if (TestProtocol.sDebugTracing) { Log.d(TestProtocol.NULL_INT_SET, "bind (1) currentScreenIds: " + currentScreenIds + ", pointer: " + mCallbacks + ", name: " + mCallbacks.getClass().getName()); } filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems, otherWorkspaceItems); if (TestProtocol.sDebugTracing) { Log.d(TestProtocol.NULL_INT_SET, "bind (2) currentScreenIds: " + currentScreenIds); } filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets, otherAppWidgets); final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); sortWorkspaceItemsSpatially(idp, currentWorkspaceItems); sortWorkspaceItemsSpatially(idp, otherWorkspaceItems); executeCallbacksTask(c -> { c.clearPendingBinds(); c.startBinding(); }, mUiExecutor); executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); bindWorkspaceItems(currentWorkspaceItems, mUiExecutor); bindAppWidgets(currentAppWidgets, mUiExecutor); bindWorkspaceItems(otherWorkspaceItems, pendingExecutor); bindAppWidgets(otherAppWidgets, pendingExecutor); executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor); }
上述代码最后面的四个绑定操作:
c.startBinding()
c.bindScreens()
bindWorkspaceItems()
bindAppWidgets()
四个绑定操作中,下面将对: **c.bindScreens()**、 bindWorkspaceItems() 这两个展开分析。
4.1 第一个绑定操作 像 c.startBinding() 、 c.bindScreens() 这两个直接回调到 Launcher.java 中。
这里先看下 c.bindScreens() 方法 Launcher#bindScreens() :
packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @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()) { mWorkspace.addExtraEmptyScreens(); } bindAddScreens(orderedScreenIds); mWorkspace.unlockWallpaperFromDefaultPageOnNextLayout(); }
以上完成了屏幕的添加,随后就添加桌面的图标和 widget ,于是传入了当前显示屏幕的图标和 widget 。
4.2 第二个绑定操作 接着看第二个绑定操作 bindWorkspaceItems() ,绑定图标是回调 Launcher.java 的对应方法,且绑定时按照不同 item 类型进行不同的绘制。 看 Launcher#bindItems() :
packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public void bindItems ( final List<ItemInfo> items, final boolean forceAnimateIcons, final boolean focusFirstItemForAccessibility) { final Collection<Animator> bounceAnims = new ArrayList <>(); boolean canAnimatePageChange = canAnimatePageChange(); Workspace<?> workspace = mWorkspace; int newItemsScreenId = -1 ; int end = items.size(); View newView = null ; for (int i = 0 ; i < end; i++) { final ItemInfo item = items.get(i); if (item.container == LauncherSettings.Favorites.CONTAINER_HOTSEAT && mHotseat == null ) { continue ; } final View view; switch (item.itemType) { case LauncherSettings.Favorites.ITEM_TYPE_APPLICATION: case LauncherSettings.Favorites.ITEM_TYPE_SHORTCUT: case LauncherSettings.Favorites.ITEM_TYPE_DEEP_SHORTCUT: { WorkspaceItemInfo info = (WorkspaceItemInfo) item; view = createShortcut(info); break ; } case LauncherSettings.Favorites.ITEM_TYPE_FOLDER: { view = FolderIcon.inflateFolderAndIcon(R.layout.folder_icon, this , (ViewGroup) workspace.getChildAt(workspace.getCurrentPage()), (FolderInfo) item); break ; } case LauncherSettings.Favorites.ITEM_TYPE_APPWIDGET: case LauncherSettings.Favorites.ITEM_TYPE_CUSTOM_APPWIDGET: { view = inflateAppWidget((LauncherAppWidgetInfo) item); if (view == null ) { continue ; } break ; } default : throw new RuntimeException ("Invalid Item Type" ); } } }
上述代码有三个需要重点关注的位置: createShortcut(info) 、 inflateFolderAndIcon() 、 inflateAppWidget() 。
4.2.1 第一个关注点 createShortcut(info) 第一个重点关注Launcher#createShortcut() :packages/apps/Launcher3/src/com/android/launcher3/Launcher.java
1 2 3 4 5 6 7 8 9 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(ItemClickHandler.INSTANCE); favorite.setOnFocusChangeListener(mFocusHandler); return favorite; }
这里面又有三个关键方法,非常值得关注。 第一个 BubbleTextView#applyFromWorkspaceItem() :
packages/apps/Launcher3/src/com/android/launcher3/BubbleTextView.java
1 2 3 4 5 6 7 8 9 10 11 12 @UiThread public void applyFromWorkspaceItem (WorkspaceItemInfo info, boolean promiseStateChanged) { applyIconAndLabel(info); setItemInfo(info); applyLoadingState(promiseStateChanged); applyDotState(info, false ); setDownloadStateContentDescription(info, info.getProgressLevel()); }
第二个 favorite.setOnClickListener(ItemClickHandler.INSTANCE) 这里传入的是 ItemClickHandler 中的 OnClickListener 。设置图标点击事件,看 ItemClickHandler#onClick() :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private static void onClick (View v) { if (v.getWindowToken() == null ) return ; Launcher launcher = Launcher.getLauncher(v.getContext()); if (!launcher.getWorkspace().isFinishedSwitchingState()) return ; Object tag = v.getTag(); if (tag instanceof WorkspaceItemInfo) { onClickAppShortcut(v, (WorkspaceItemInfo) tag, launcher); } else if (tag instanceof FolderInfo) { if (v instanceof FolderIcon) { onClickFolderIcon(v); } } else if (tag instanceof AppInfo) { startAppShortcutOrInfoActivity(v, (AppInfo) tag, launcher); } else if (tag instanceof LauncherAppWidgetInfo) { if (v instanceof PendingAppWidgetHostView) { onClickPendingWidget((PendingAppWidgetHostView) v, launcher); } } else if (tag instanceof SearchActionItemInfo) { onClickSearchAction(launcher, (SearchActionItemInfo) tag); } }
第三个 favorite.setOnFocusChangeListener(mFocusHandler): 外接键盘选择功能。被focus的图标会有灰色背景显示被选中。此外还有一定动画效果,都在focus类里。 第一个关注 Launcher#createShortcut() 方法就到此结束。
4.2.2 第二个关注点 inflateFolderAndIcon() 接下来看第二个关注的方法 FolderIcon#inflateFolderAndIcon():packages/apps/Launcher3/src/com/android/launcher3/folder/FolderIcon.java
1 2 3 4 5 6 7 8 9 10 11 public static <T extends Context & ActivityContext> FolderIcon inflateFolderAndIcon (int resId, T activityContext, ViewGroup group, FolderInfo folderInfo) { Folder folder = Folder.fromXml(activityContext); FolderIcon icon = inflateIcon(resId, activityContext, group, folderInfo); folder.setFolderIcon(icon); folder.bind(folderInfo); icon.setFolder(folder); return icon; }
这里注意:FolderIcon是文件夹的图标,Folder 是打开时的文件夹 (不是里面的应用图标)。
到这里可以发现应用图标是 textview 而文件夹是 FrameLayout 。后面就不过多介绍了,和应用一样生成名字,大小,click,focus 等。
最后看第三个关注点 Launcher#inflateAppWidget() ,看里面的 AppWidgetHost.createView() :
frameworks/base/core/java/android/appwidget/AppWidgetHost.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public final AppWidgetHostView createView (Context context, int appWidgetId, AppWidgetProviderInfo appWidget) { if (sService == null ) { return null ; } AppWidgetHostView view = onCreateView(context, appWidgetId, appWidget); view.setInteractionHandler(mInteractionHandler); view.setAppWidget(appWidgetId, appWidget); synchronized (mViews) { mViews.put(appWidgetId, view); } RemoteViews views; try { views = sService.getAppWidgetViews(mContextOpPackageName, appWidgetId); } catch (RemoteException e) { throw new RuntimeException ("system server dead?" , e); } view.updateAppWidget(views); return view; }
以上 bindItems 就是按照分类把每种类型的桌面的 view 一个一个的创造出来。完成了当前屏幕的绘制,而后进行其他屏幕的 view 绘制。都在同一个方法调用绑定 BaseLoaderResults#bind() ,只是传入的 list 为 otherWorkspaceItems 和 otherAppWidgets 。
至此 Workspace 的数据加载与绑定结束。这里当我注释掉 loadAllApps() 后,当前屏幕是有应用图标的(我这是:相册、Google助理、Play商店、最下面电话、短信等图标都有) ,但上滑界面进入到 AllApps 界面时,没有任何图标。
本文链接: http://longzhiye.top/2024/02/17/2024-02-17/