8
加载Icon、设置壁纸
上一章墨香带你学Launcher之(七)- 小部件的加载、添加以及大小调节介绍了小部件的加载以及添加过程,基于我的计划对于Launcher的讲解基本要完成了,因此本篇是我对Launcher讲解的最后一部分,计划了很久,因为时间的问题一直没有写,今天趁着有空写完。写了八篇,不多,Launcher里面还有很多东西,有兴趣的可以自己继续研究,看完这些主要的其他都是问题了,有什么需要了解的可以留言。最新版的Launcher代码我已经放到github上,想看的自己可以去下载。
加载Icon
对于Icon的操作其实主要是加载、更新以及删除,加载主要是启动Launcher、安装应用,更新是在更新应用时更新Icon、删除是卸载应用时会删除Icon,因此我们可以从这几方面分析Icon的处理。
Launcher启动时Icon加载
Launcher的数据加载流程我在第二篇墨香带你学Launcher之(二)- 数据加载流程讲过,不熟悉的可以去看看。首先是将xml文件中配置的Apk信息解析保存到数据库,然后读取数据库,查看手机中是否存在该apk,如果有加载相关信息,加载流程在“loadWorkspace”方法中,在加载过程中会去生成对应的Icon,我们看一下代码:
if (itemReplaced) {
...
info = getAppShortcutInfo(manager, intent, user, context, null,
cursorIconInfo.iconIndex, titleIndex,
false, useLowResIcon);
...
} else if (restored) {
...
info = getRestoredItemInfo(c, titleIndex, intent,
promiseType, itemType, cursorIconInfo, context);
...
} else if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
info = getAppShortcutInfo(manager, intent, user, context, c,
cursorIconInfo.iconIndex, titleIndex,
allowMissingTarget, useLowResIcon);
} else {
info = getShortcutInfo(c, context, titleIndex, cursorIconInfo);
...
}
在段代码中主要有三个方法涉及到加载Icon,getAppShortcutInfo、getRestoredItemInfo以及getShortcutInfo方法,我们看看这个三个方法的代码:
第一个:
public ShortcutInfo getAppShortcutInfo(PackageManager manager, Intent intent,
UserHandleCompat user, Context context, Cursor c, int iconIndex, int titleIndex,
boolean allowMissingTarget, boolean useLowResIcon) {
...
final ShortcutInfo info = new ShortcutInfo();
mIconCache.getTitleAndIcon(info, componentName, lai, user, false, useLowResIcon);
if (mIconCache.isDefaultIcon(info.getIcon(mIconCache), user) && c != null) {
Bitmap icon = Utilities.createIconBitmap(c, iconIndex, context);
info.setIcon(icon == null ? mIconCache.getDefaultIcon(user) : icon);
}
...
}
在这段代码中主要是调用IconCache中的getTitleAndIcon方法,这个方法详细过程我们一会再看,然后判断是否是默认图标,如果是生成Icon图标,如果能生成则设置图标,如果不能生成则采用默认图标。Utilities.createIconBitmap代码不在详细讲,看看就会了。
我们接着看第二个方法:
public ShortcutInfo getRestoredItemInfo(Cursor c, int titleIndex, Intent intent,
int promiseType, int itemType, CursorIconInfo iconInfo, Context context) {
...
Bitmap icon = iconInfo.loadIcon(c, info, context);
// the fallback icon
if (icon == null) {
mIconCache.getTitleAndIcon(info, intent, info.user, false /* useLowResIcon */);
} else {
info.setIcon(icon);
}
...
}
这个方法中主要是调用CursorIconInfo中的loadIcon方法,代码我们一会再看,如果能获取到Icon则设置这个Icon,如果不能则通过IconCache.getTitleAndIcon方法获取,和上面一样了。
第三个方法:
ShortcutInfo getShortcutInfo(Cursor c, Context context,
int titleIndex, CursorIconInfo iconInfo) {
...
Bitmap icon = iconInfo.loadIcon(c, info, context);
// the fallback icon
if (icon == null) {
icon = mIconCache.getDefaultIcon(info.user);
info.usingFallbackIcon = true;
}
info.setIcon(icon);
return info;
}
这个方法中还是调用CursorIconInfo中的loadIcon方法,如果能获取,则设置图标,如果不能获取默认图标设置。从上面三个方法代码看其实最终调用了两个方法,一个是IconCache.getTitleAndIcon方法,一个是CursorIconInfo.loadIcon方法。
我们先看一下CursorIconInfo.loadIcon代码:
public Bitmap loadIcon(Cursor c, ShortcutInfo info, Context context) {
Bitmap icon = null;
int iconType = c.getInt(iconTypeIndex);
switch (iconType) {
case LauncherSettings.Favorites.ICON_TYPE_RESOURCE:
String packageName = c.getString(iconPackageIndex);
String resourceName = c.getString(iconResourceIndex);
if (!TextUtils.isEmpty(packageName) || !TextUtils.isEmpty(resourceName)) {
info.iconResource = new ShortcutIconResource();
info.iconResource.packageName = packageName;
info.iconResource.resourceName = resourceName;
icon = Utilities.createIconBitmap(packageName, resourceName, context);
}
if (icon == null) {
// Failed to load from resource, try loading from DB.
icon = Utilities.createIconBitmap(c, iconIndex, context);
}
break;
case LauncherSettings.Favorites.ICON_TYPE_BITMAP:
icon = Utilities.createIconBitmap(c, iconIndex, context);
info.customIcon = icon != null;
break;
}
return icon;
}
在这个方法中首先是从资源获取,如果获取不到,则从数据库获取,及Utilities.createIconBitmap(packageName, resourceName, context)和Utilities.createIconBitmap(c, iconIndex, context),我们看看这两个方法:
第一个方法:
public static Bitmap createIconBitmap(String packageName, String resourceName,
Context context) {
PackageManager packageManager = context.getPackageManager();
// the resource
try {
Resources resources = packageManager.getResourcesForApplication(packageName);
if (resources != null) {
final int id = resources.getIdentifier(resourceName, null, null);
return createIconBitmap(
resources.getDrawableForDensity(id, LauncherAppState.getInstance()
.getInvariantDeviceProfile().fillResIconDpi), context);
}
} catch (Exception e) {
// Icon not found.
}
return null;
}
这个方法是根据包名获取id,然后根据id获取drawable,由drawable生产Bitmap。
第二个方法:
public static Bitmap createIconBitmap(Cursor c, int iconIndex, Context context) {
byte[] data = c.getBlob(iconIndex);
try {
return createIconBitmap(BitmapFactory.decodeByteArray(data, 0, data.length), context);
} catch (Exception e) {
return null;
}
}
从数据库读取Icon的byte数据,然后生成图片。这样看就很清楚这个方法加载Icon的过程了。那么数据库中的Icon怎么来的我们回到前面再看IconCache.getTitleAndIcon方法:
public synchronized void getTitleAndIcon(
ShortcutInfo shortcutInfo, ComponentName component, LauncherActivityInfoCompat info,
UserHandleCompat user, boolean usePkgIcon, boolean useLowResIcon) {
CacheEntry entry = cacheLocked(component, info, user, usePkgIcon, useLowResIcon);
shortcutInfo.setIcon(getNonNullIcon(entry, user));
shortcutInfo.title = Utilities.trim(entry.title);
shortcutInfo.usingFallbackIcon = isDefaultIcon(entry.icon, user);
shortcutInfo.usingLowResIcon = entry.isLowResIcon;
}
我们看到了setIcon方法,那么是getNonNullIcon这个方法创建了Icon,这个方法有个我们不熟悉的对象entry,向上看这个entry是子啊上面通过cacheLocked方法创建的,我们跟踪一下这个方法:
private CacheEntry cacheLocked(ComponentName componentName, LauncherActivityInfoCompat info,
UserHandleCompat user, boolean usePackageIcon, boolean useLowResIcon) {
ComponentKey cacheKey = new ComponentKey(componentName, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
entry = new CacheEntry();
mCache.put(cacheKey, entry);
// Check the DB first.
if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
if (info != null) {
entry.icon = Utilities.createIconBitmap(info.getBadgedIcon(mIconDpi), mContext);
} else {
if (usePackageIcon) {
CacheEntry packageEntry = getEntryForPackageLocked(
componentName.getPackageName(), user, false);
if (packageEntry != null) {
if (DEBUG) Log.d(TAG, "using package default icon for " +
componentName.toShortString());
entry.icon = packageEntry.icon;
entry.title = packageEntry.title;
entry.contentDescription = packageEntry.contentDescription;
}
}
if (entry.icon == null) {
entry.icon = getDefaultIcon(user);
}
}
}
...
}
return entry;
}
首先是从mCache中获取,如果存在CacheEntry对象,则不需要再创建,如果没有则要创建改对象,然后加载到mCache中,然后通过调用getEntryFromDB方法从数据库查询是否有改对象信息,如果没有则要创建对应Icon,我们先看看getEntryFromDB这个方法:
private boolean getEntryFromDB(ComponentKey cacheKey, CacheEntry entry, boolean lowRes) {
...
try {
if (c.moveToNext()) {
entry.icon = loadIconNoResize(c, 0, lowRes ? mLowResOptions : null);
entry.isLowResIcon = lowRes;
...
}
} finally {
c.close();
}
return false;
}
该方法通过查询数据库来生成Icon,调用方法loadIconNoResize,看代码:
private static Bitmap loadIconNoResize(Cursor c, int iconIndex, BitmapFactory.Options options) {
byte[] data = c.getBlob(iconIndex);
try {
return BitmapFactory.decodeByteArray(data, 0, data.length, options);
} catch (Exception e) {
return null;
}
}
和上面的一样,就不用讲了。
回到cacheLocked方法中,如果数据库中没有,要继续创建Icon,首先判断LauncherActivityInfoCompat是否为空,调用Utilities.createIconBitmap方法获取Icon,代码就不贴了,也不难,如果为空的话会判断usePackageIcon(根据包名获取Icon),如果用的话则会调用getEntryForPackageLocked方法获取CacheEntry,看代码:
private CacheEntry getEntryForPackageLocked(String packageName, UserHandleCompat user,
boolean useLowResIcon) {
ComponentKey cacheKey = getPackageKey(packageName, user);
CacheEntry entry = mCache.get(cacheKey);
if (entry == null || (entry.isLowResIcon && !useLowResIcon)) {
entry = new CacheEntry();
boolean entryUpdated = true;
// Check the DB first.
if (!getEntryFromDB(cacheKey, entry, useLowResIcon)) {
try {
...
Drawable drawable = mUserManager.getBadgedDrawableForUser(
appInfo.loadIcon(mPackageManager), user);
entry.icon = Utilities.createIconBitmap(drawable, mContext);
entry.title = appInfo.loadLabel(mPackageManager);
entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, user);
entry.isLowResIcon = false;
// Add the icon in the DB here, since these do not get written during
// package updates.
ContentValues values =
newContentValues(entry.icon, entry.title.toString(), mPackageBgColor);
addIconToDB(values, cacheKey.componentName, info,
mUserManager.getSerialNumberForUser(user));
} catch (NameNotFoundException e) {
if (DEBUG) Log.d(TAG, "Application not installed " + packageName);
entryUpdated = false;
}
}
// Only add a filled-out entry to the cache
if (entryUpdated) {
mCache.put(cacheKey, entry);
}
}
return entry;
}
代码和cacheLocked方法很像,也是先判断数据库中是否存在,不存在就要加载,这里有个方法addIconToDB,看上面ContentValues的注释,就是把Icon存到数据库中,原来是在这里存入数据库的,其实Icon的信息首先放入ContentValues中,然后存入数据库,我们看看代码:
private ContentValues newContentValues(Bitmap icon, String label, int lowResBackgroundColor) {
ContentValues values = new ContentValues();
values.put(IconDB.COLUMN_ICON, Utilities.flattenBitmap(icon));
values.put(IconDB.COLUMN_LABEL, label);
values.put(IconDB.COLUMN_SYSTEM_STATE, mSystemState);
if (lowResBackgroundColor == Color.TRANSPARENT) {
values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(
Bitmap.createScaledBitmap(icon,
icon.getWidth() / LOW_RES_SCALE_FACTOR,
icon.getHeight() / LOW_RES_SCALE_FACTOR, true)));
} else {
synchronized (this) {
if (mLowResBitmap == null) {
mLowResBitmap = Bitmap.createBitmap(icon.getWidth() / LOW_RES_SCALE_FACTOR,
icon.getHeight() / LOW_RES_SCALE_FACTOR, Bitmap.Config.RGB_565);
mLowResCanvas = new Canvas(mLowResBitmap);
mLowResPaint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
}
mLowResCanvas.drawColor(lowResBackgroundColor);
mLowResCanvas.drawBitmap(icon, new Rect(0, 0, icon.getWidth(), icon.getHeight()),
new Rect(0, 0, mLowResBitmap.getWidth(), mLowResBitmap.getHeight()),
mLowResPaint);
values.put(IconDB.COLUMN_ICON_LOW_RES, Utilities.flattenBitmap(mLowResBitmap));
}
}
return values;
}
通过Utilities.flattenBitmap(icon)方法将Icon转换成byte数组然后存入数据库。再回到cacheLocked方法中,如果还是没有获取到Icon,那么只能获取系统默认Icon了,也就是我们自己写app的默认Icon图标(机器人图标)。这个是我们加载配置文件中的Apk信息时加载Icon的过程,我们再看看加载所有app时是不是也是这样,我们先看加载方法loadAllApps代码:
private void loadAllApps() {
...
// Create the ApplicationInfos
for (int i = 0; i < apps.size(); i++) {
LauncherActivityInfoCompat app = apps.get(i);
// This builds the icon bitmaps.
mBgAllAppsList.add(new AppInfo(mContext, app, user, mIconCache));
}
...
}
我们看到主要是AppInfo对象的生成,我们看看代码:
public AppInfo(Context context, LauncherActivityInfoCompat info, UserHandleCompat user,
IconCache iconCache) {
this.componentName = info.getComponentName();
this.container = ItemInfo.NO_ID;
flags = initFlags(info);
firstInstallTime = info.getFirstInstallTime();
iconCache.getTitleAndIcon(this, info, true /* useLowResIcon */);
intent = makeLaunchIntent(context, info, user);
this.user = user;
}
从上面代码我们看到其实还是调用getTitleAndIcon方法,又回到我们上面讲的过程了。
APK安装、更新、卸载时Icon处理
APK的安装、卸载、更新、可用以及不可用在墨香带你学Launcher之(四)-应用安装、更新、卸载时的数据加载中讲到过,不清楚的可以去看看,这几个实现方法是在LauncherModel中来处理的:
@Override
public void onPackageChanged(String packageName, UserHandleCompat user) {
int op = PackageUpdatedTask.OP_UPDATE;
enqueuePackageUpdated(new PackageUpdatedTask(op, new String[]{packageName},
user));
}
@Override
public void onPackageRemoved(String packageName, UserHandleCompat user) {
int op = PackageUpdatedTask.OP_REMOVE;
enqueuePackageUpdated(new PackageUpdatedTask(op, new String[]{packageName},
user));
}
@Override
public void onPackageAdded(String packageName, UserHandleCompat user) {
int op = PackageUpdatedTask.OP_ADD;
enqueuePackageUpdated(new PackageUpdatedTask(op, new String[]{packageName},
user));
}
@Override
public void onPackagesAvailable(String[] packageNames, UserHandleCompat user,
boolean replacing) {
if (!replacing) {
enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_ADD, packageNames,
user));
if (mAppsCanBeOnRemoveableStorage) {
startLoaderFromBackground();
}
} else {
// If we are replacing then just update the packages in the list
enqueuePackageUpdated(new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE,
packageNames, user));
}
}
@Override
public void onPackagesUnavailable(String[] packageNames, UserHandleCompat user,
boolean replacing) {
if (!replacing) {
enqueuePackageUpdated(new PackageUpdatedTask(
PackageUpdatedTask.OP_UNAVAILABLE, packageNames,
user));
}
}
我们看代码发现其实都是PackageUpdatedTask这个执行方法,代码比较多,我们只贴重点部分,详细的可以去看源码:
private class PackageUpdatedTask implements Runnable {
...
public void run() {
...
switch (mOp) {
case OP_ADD: {
for (int i = 0; i < N; i++) {
...
mIconCache.updateIconsForPkg(packages[i], mUser);
...
}
...
break;
}
case OP_UPDATE:
for (int i = 0; i < N; i++) {
...
mIconCache.updateIconsForPkg(packages[i], mUser);
...
}
break;
case OP_REMOVE: {
...
for (int i = 0; i < N; i++) {
...
mIconCache.removeIconsForPkg(packages[i], mUser);
}
}
case OP_UNAVAILABLE:
for (int i = 0; i < N; i++) {
...
}
break;
}
...
// Update shortcut infos
if (mOp == OP_ADD || mOp == OP_UPDATE) {
...
synchronized (sBgLock) {
for (ItemInfo info : sBgItemsIdMap) {
if (info instanceof ShortcutInfo && mUser.equals(info.user)) {
...
// Update shortcuts which use iconResource.
if ((si.iconResource != null)
&& packageSet.contains(si.iconResource.packageName)) {
Bitmap icon = Utilities.createIconBitmap(
si.iconResource.packageName,
si.iconResource.resourceName, context);
if (icon != null) {
si.setIcon(icon);
...
}
}
ComponentName cn = si.getTargetComponent();
if (cn != null && packageSet.contains(cn.getPackageName()