上一章墨香带你学Launcher之(七)- 小部件的加载、添加以及大小调节介绍了小部件的加载以及添加过程,基于我的计划对于Launcher的讲解基本要完成了,因此本篇是我对Launcher讲解的最后一部分,计划了很久,因为时间的问题一直没有写,今天趁着有空写完。写了八篇,不多,Launcher里面还有很多东西,有兴趣的可以自己继续研究,看完这些主要的其他都是问题了,有什么需要了解的可以留言。最新版的Launcher代码我已经放到github上,想看的自己可以去下载。

加载Icon

对于Icon的操作其实主要是加载、更新以及删除,加载主要是启动Launcher、安装应用,更新是在更新应用时更新Icon、删除是卸载应用时会删除Icon,因此我们可以从这几方面分析Icon的处理。

Launcher启动时Icon加载

Launcher的数据加载流程我在第二篇墨香带你学Launcher之(二)- 数据加载流程讲过,不熟悉的可以去看看。首先是将xml文件中配置的Apk信息解析保存到数据库,然后读取数据库,查看手机中是否存在该apk,如果有加载相关信息,加载流程在“loadWorkspace”方法中,在加载过程中会去生成对应的Icon,我们看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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方法,我们看看这个三个方法的代码:

第一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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代码不在详细讲,看看就会了。

我们接着看第二个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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方法获取,和上面一样了。

第三个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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代码:

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
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),我们看看这两个方法:

第一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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。

第二个方法:

1
2
3
4
5
6
7
8
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方法:

1
2
3
4
5
6
7
8
9
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方法创建的,我们跟踪一下这个方法:

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
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这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
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,看代码:

1
2
3
4
5
6
7
8
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,看代码:

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
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中,然后存入数据库,我们看看代码:

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
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代码:

1
2
3
4
5
6
7
8
9
10
11
12
    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对象的生成,我们看看代码:

1
2
3
4
5
6
7
8
9
10
11
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中来处理的:

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
@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这个执行方法,代码比较多,我们只贴重点部分,详细的可以去看源码:

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
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())) {
...
if (si.isPromise()) {
...
si.updateIcon(mIconCache);
}

if (appInfo != null && Intent.ACTION_MAIN.equals(si.intent.getAction())
&& si.itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
si.updateIcon(mIconCache);
...
}
...
}
...
}
}
}
}
}
}

在上面代码中我们看到OP_ADD(安装)、OP_UPDATE(更新)时都是调用的mIconCache.removeIconsForPkg,而和OP_REMOVE(卸载)时调用mIconCache.removeIconsForPkg方法,而在下面又调用了si.setIcon(icon)、si.updateIcon来更新Icon,我们分别来看看这四个方法,首先看第一个方法(removeIconsForPkg):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public synchronized void updateIconsForPkg(String packageName, UserHandleCompat user) {
removeIconsForPkg(packageName, user);
try {
PackageInfo info = mPackageManager.getPackageInfo(packageName,
PackageManager.GET_UNINSTALLED_PACKAGES);
long userSerial = mUserManager.getSerialNumberForUser(user);
for (LauncherActivityInfoCompat app : mLauncherApps.getActivityList(packageName, user)) {
addIconToDBAndMemCache(app, info, userSerial);
}
} catch (NameNotFoundException e) {
Log.d(TAG, "Package not found", e);
return;
}
}

首先调用removeIconsForPkg方法,也就是删除Icon,看代码:

1
2
3
4
5
6
7
public synchronized void removeIconsForPkg(String packageName, UserHandleCompat user) {
removeFromMemCacheLocked(packageName, user);
long userSerial = mUserManager.getSerialNumberForUser(user);
mIconDb.getWritableDatabase().delete(IconDB.TABLE_NAME,
IconDB.COLUMN_COMPONENT + " LIKE ? AND " + IconDB.COLUMN_USER + " = ?",
new String[] {packageName + "/%", Long.toString(userSerial)});
}

首先调用removeFromMemCacheLocked方法,其实这个方法就是从mCache中把缓存的CacheEntry对象删除,然后再从数据库删除Icon。然后回到updateIconsForPkg方法,接着调用addIconToDBAndMemCache方法,也就是添加Icon到数据库:

1
2
3
4
5
6
7
@Thunk void addIconToDBAndMemCache(LauncherActivityInfoCompat app, PackageInfo info,
long userSerial) {
// Reuse the existing entry if it already exists in the DB. This ensures that we do not
// create bitmap if it was already created during loader.
ContentValues values = updateCacheAndGetContentValues(app, false);
addIconToDB(values, app.getComponentName(), info, userSerial);
}

首先调用updateCacherAndGetContentValues这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Thunk ContentValues updateCacheAndGetContentValues(LauncherActivityInfoCompat app,
boolean replaceExisting) {
final ComponentKey key = new ComponentKey(app.getComponentName(), app.getUser());
CacheEntry entry = null;
if (!replaceExisting) {
entry = mCache.get(key);
// We can't reuse the entry if the high-res icon is not present.
if (entry == null || entry.isLowResIcon || entry.icon == null) {
entry = null;
}
}
if (entry == null) {
entry = new CacheEntry();
entry.icon = Utilities.createIconBitmap(app.getBadgedIcon(mIconDpi), mContext);
}
entry.title = app.getLabel();
entry.contentDescription = mUserManager.getBadgedLabelForUser(entry.title, app.getUser());
mCache.put(new ComponentKey(app.getComponentName(), app.getUser()), entry);

return newContentValues(entry.icon, entry.title.toString(), mActivityBgColor);
}

这个方法是生成新的CacheEntry,以及Icon,放将其放置到mCache中缓存,就是我们上面删除的那个,然后通过调用newContentValues方法将Icon转换成byte数组放到ContentValues中,最后存入数据库中。这就是我们安装,更新,卸载时对于Icon的数据库操作。我们在Icon生成后其实要放到相应的应用对象中,以方便我们显示到桌面上,其实就是(setIcon(icon)、si.updateIcon(mIconCache))这两个方法,第一个是直接将生成好的Icon放入到ShortcutInfo中,另一个是从缓存获取,我们来看从缓存获取这个方法:

1
2
3
public void updateIcon(IconCache iconCache) {
updateIcon(iconCache, shouldUseLowResIcon());
}

调用updateIcon方法:

1
2
3
4
5
6
public void updateIcon(IconCache iconCache, boolean useLowRes) {
if (itemType == LauncherSettings.Favorites.ITEM_TYPE_APPLICATION) {
iconCache.getTitleAndIcon(this, promisedIntent != null ? promisedIntent : intent, user,
useLowRes);
}
}

我们看到此时调用了iconCache.getTitleAndIcon方法,也就是又回到我们之前将的获取Icon的方法了。

整个Icon加载的流程基本就是这些,有些我没有详细讲解,自己看看就好了,Icon会放到ShortcutInfo中,在绑定图标的时候会读取出来显示到桌面上,流程就是这样的,如果要做切换主题其实就是从这里入手。

设置壁纸

原生桌面长按桌面空白处,会出现壁纸、widget和设置三个菜单,我们点击壁纸会进入壁纸选择设置界面,也就是WallpaperPickerActivity,WallpaperPickerActivity继承WallpaperCropActivity,所以有些操作可能分别在这两个类中进行。

设置壁纸是从WallpaperCropActivity中的setWallpaper方法开始的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void setWallpaper(Uri uri, final boolean finishActivityWhenDone) {
int rotation = BitmapUtils.getRotationFromExif(getContext(), uri);
BitmapCropTask cropTask = new BitmapCropTask(
getContext(), uri, null, rotation, 0, 0, true, false, null);
final Point bounds = cropTask.getImageBounds();
Runnable onEndCrop = new Runnable() {
public void run() {
updateWallpaperDimensions(bounds.x, bounds.y);
if (finishActivityWhenDone) {
setResult(Activity.RESULT_OK);
finish();
}
}
};
cropTask.setOnEndRunnable(onEndCrop);
cropTask.setNoCrop(true);
cropTask.execute();
}

其中BitmapCropTask是一个异步任务,也就是执行异步任务设置壁纸然后调用onEndCrop中的run方法结束改界面,返回桌面。异步任务执行顺序是:onPreExecute–>doInBackground–>onPostExecute。我们看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BitmapCropTask extends AsyncTask<Void, Void, Boolean> {

// Helper to setup input stream
private InputStream regenerateInputStream() {
...
}

public boolean cropBitmap() {
...
}

@Override
protected Boolean doInBackground(Void... params) {
return cropBitmap();
}

@Override
protected void onPostExecute(Boolean result) {
...
}
}

首先初始化,然后执行doInBackground方法,其实这个方法中执行的是cropBitmap方法,代码:

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
public boolean cropBitmap() {
...
if (mSetWallpaper) {
//获取WallpaperManager对象
wallpaperManager = WallpaperManager.getInstance(mContext.getApplicationContext());
}

if (mSetWallpaper && mNoCrop) {
try {
//不需要裁切的情况下,直接通过URI获取图片流
InputStream is = regenerateInputStream();
if (is != null) {
//如果图片存在,设置壁纸
wallpaperManager.setStream(is);
Utils.closeSilently(is);
}
} catch (IOException e) {
Log.w(LOGTAG, "cannot write stream to wallpaper", e);
failure = true;
}
return !failure;
} else {// 如果需要裁切
// Find crop bounds (scaled to original image size)
...

//获取图片的大小范围
Point bounds = getImageBounds();
//判断是否需要旋转
if (mRotation > 0) {
rotateMatrix.setRotate(mRotation);
inverseRotateMatrix.setRotate(-mRotation);
...
}

mCropBounds.roundOut(roundedTrueCrop);
//如果宽高小于0则视为失败
if (roundedTrueCrop.width() <= 0 || roundedTrueCrop.height() <= 0) {
...
return false;
}

// 根据宽高比来设置缩放倍数
int scaleDownSampleSize = Math.max(1, Math.min(roundedTrueCrop.width() / mOutWidth,
roundedTrueCrop.height() / mOutHeight));
...
try {
//通过流读取图片
is = regenerateInputStream();
...
decoder = BitmapRegionDecoder.newInstance(is, false);
Utils.closeSilently(is);
} catch (IOException e) {
...
} finally {
...
}

Bitmap crop = null;
if (decoder != null) {
// Do region decoding to get crop bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
if (scaleDownSampleSize > 1) {
options.inSampleSize = scaleDownSampleSize;
}
// 获取切割图片
crop = decoder.decodeRegion(roundedTrueCrop, options);
decoder.recycle();
}

if (crop == null) {//获取切割图片失败
// BitmapRegionDecoder has failed, try to crop in-memory
is = regenerateInputStream();
Bitmap fullSize = null;
if (is != null) {
BitmapFactory.Options options = new BitmapFactory.Options();
if (scaleDownSampleSize > 1) {
options.inSampleSize = scaleDownSampleSize;
}
//获取原始图片
fullSize = BitmapFactory.decodeStream(is, null, options);
Utils.closeSilently(is);
}
if (fullSize != null) {
// 计算切割图片的范围
...
//生成切割图片
crop = Bitmap.createBitmap(fullSize, roundedTrueCrop.left,
roundedTrueCrop.top, roundedTrueCrop.width(),
roundedTrueCrop.height());
}
}

...

if (mOutWidth > 0 && mOutHeight > 0 || mRotation > 0) {
...

Matrix m = new Matrix();
// 不需要旋转
if (mRotation == 0) {
m.setRectToRect(cropRect, returnRect, Matrix.ScaleToFit.FILL);
} else {//旋转
...
}

//生成新的旋转后的图片
Bitmap tmp = Bitmap.createBitmap((int) returnRect.width(),
(int) returnRect.height(), Bitmap.Config.ARGB_8888);
if (tmp != null) {
Canvas c = new Canvas(tmp);
Paint p = new Paint();
p.setFilterBitmap(true);
c.drawBitmap(crop, m, p);
crop = tmp;
}
}

if (mSaveCroppedBitmap) {
mCroppedBitmap = crop;
}

// Compress to byte array
ByteArrayOutputStream tmpOut = new ByteArrayOutputStream(2048);
//压缩图片成数组
if (crop.compress(CompressFormat.JPEG, DEFAULT_COMPRESS_QUALITY, tmpOut)) {
// If we need to set to the wallpaper, set it
if (mSetWallpaper && wallpaperManager != null) {
try {
byte[] outByteArray = tmpOut.toByteArray();
//设置壁纸
wallpaperManager.setStream(new ByteArrayInputStream(outByteArray));
if (mOnBitmapCroppedHandler != null) {
mOnBitmapCroppedHandler.onBitmapCropped(outByteArray);
}
} catch (IOException e) {
...
}
}
} else {
...
}
}
return !failure; // True if any of the operations failed

整个过程看上面代码,解释都卸载注释里面了,一些裁切计算问题看看代码就知道了,最终就是转换成流的形式进行设置壁纸。

最后

Github地址:https://github.com/yuchuangu85/Launcher3_mx/tree/launcher3_6.0

Android开发群:192508518

微信公众账号:Code-MX
qr_code_mx

注:本文原创,转载请注明出处,多谢。