項目需求:編寫主題app,一鍵切換桌面app圖標和系統壁紙。
Android版本:8.1
需求是寫一個系統主題app,實現類似於華爲手機內置系統主題app的功能,原生android是沒有主題app的,網上都是app換膚框架,是給自己單獨的app換主題,百般無奈只能自己動手寫了。還好我們是在源碼基礎上開發,可以任性的自定義功能。
步驟一
先找到Launcher加載各種app的地方
Launcher的初始化過程:
@Override
protected void onCreate(Bundle savedInstanceState) {
...
LauncherAppState app = LauncherAppState.getInstance(this);
mModel = app.setLauncher(this);
...
if (!mModel.startLoader(currentScreen)) {
mDragLayer.setAlpha(0);
} else {
mWorkspace.setCurrentPage(currentScreen);
setWorkspaceLoading(true);
}
...
}
進入LauncherModel,發現LauncherModel 居然是BroadcastReceiver
public class LauncherModel extends BroadcastReceiver
去看它的startLoader
public boolean startLoader(int synchronousBindPage) {
// Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems
InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING);
synchronized (mLock) {
if (mCallbacks != null && mCallbacks.get() != null) {
final Callbacks oldCallbacks = mCallbacks.get();
// Clear any pending bind-runnables from the synchronized load process.
mUiExecutor.execute(new Runnable() {
public void run() {
oldCallbacks.clearPendingBinds();
}
});
stopLoader();
LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel,
mBgAllAppsList, synchronousBindPage, mCallbacks);
if (mModelLoaded && !mIsLoaderTaskRunning) {
loaderResults.bindWorkspace();
loaderResults.bindAllApps();
loaderResults.bindDeepShortcuts();
loaderResults.bindWidgets();
return true;
} else {
startLoaderForResults(loaderResults);
}
}
}
return false;
}
第一次初始化mModelLoaded肯定是false無疑,所以進入startLoaderForResults(loaderResults)
public void startLoaderForResults(LoaderResults results) {
synchronized (mLock) {
stopLoader();
mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results,packName,themeName);
runOnWorkerThread(mLoaderTask);
}
}
LoaderTask是一個Runnable,
private static void runOnWorkerThread(Runnable r) {
if (sWorkerThread.getThreadId() == Process.myTid()) {
r.run();
} else {
// If we are not on the worker thread, then post to the worker handler
sWorker.post(r);
}
}
所以應該去看LoaderTask的run方法
public void run() {
synchronized (this) {
// Skip fast if we are already stopped.
if (mStopped) {
return;
}
}
try (LauncherModel.LoaderTransaction transaction = mApp.getModel().beginLoader(this)) {
if (DEBUG_LOADERS) Log.d(TAG, "step 1.1: loading workspace");
loadWorkspace();
if (DEBUG_LOADERS) Log.d(TAG, "step 1.2: bind workspace workspace");
mResults.bindWorkspace();
if (DEBUG_LOADERS) Log.d(TAG, "step 2.1: loading all apps");
loadAllApps();
mResults.bindAllApps();
if (DEBUG_LOADERS) Log.d(TAG, "step 2.3: Update icon cache");
updateIconCache();
// third step
if (DEBUG_LOADERS) Log.d(TAG, "step 3.1: loading deep shortcuts");
loadDeepShortcuts();
if (DEBUG_LOADERS) Log.d(TAG, "step 3.2: bind deep shortcuts");
mResults.bindDeepShortcuts();
// fourth step
if (DEBUG_LOADERS) Log.d(TAG, "step 4.1: loading widgets");
mBgDataModel.widgetsModel.update(mApp, null);
if (DEBUG_LOADERS) Log.d(TAG, "step 4.2: Binding widgets");
mResults.bindWidgets();
transaction.commit();
} catch (CancellationException e) {
}
}
這個方法很大,但是邏輯是順序的,分別先加載並綁定了 workspace ,然後再加載所有app的view,由於我們想找到的是加載app圖標的地方,所以肯定是 loadAllApps 了。
private void loadAllApps() {
final long loadTime = DEBUG_LOADERS ? SystemClock.uptimeMillis() : 0;
final List<UserHandle> profiles = mUserManager.getUserProfiles();
mBgAllAppsList.clear();
for (UserHandle user : profiles) {
...//省略
// Create the ApplicationInfos
for (int i = 0; i < apps.size(); i++) {
LauncherActivityInfo app = apps.get(i);
// This builds the icon bitmaps.
Log.i(TAG, "loadAllApps--> app=" + app.getName());
mBgAllAppsList.add(new AppInfo(app, user, quietMode), app);
}
...//省略
ManagedProfileHeuristic.onAllAppsLoaded(mApp.getContext(), apps, user);
}
...//省略
}
其中,for循環用LauncherActivityInfo封裝給AppInfo,再添加進list裏,
public void add(AppInfo info, LauncherActivityInfo activityInfo) {
if (!mAppFilter.shouldShowApp(info.componentName)) {
return;
}
if (findAppInfo(info.componentName, info.user) != null) {
return;
}
mIconCache.getTitleAndIcon(info, activityInfo, true /* useLowResIcon */);
data.add(info);
added.add(info);
}
進入了AllAppsList,使用的是IconCache來管理appinfo的,
public synchronized void getTitleAndIcon(ItemInfoWithIcon info,
LauncherActivityInfo activityInfo, boolean useLowResIcon) {
// If we already have activity info, no need to use package icon
getTitleAndIcon(info, Provider.of(activityInfo), false, useLowResIcon);
}
---
private synchronized void getTitleAndIcon(
@NonNull ItemInfoWithIcon infoInOut,
@NonNull Provider<LauncherActivityInfo> activityInfoProvider,
boolean usePkgIcon, boolean useLowResIcon) {
CacheEntry entry = cacheLocked(infoInOut.getTargetComponent(), activityInfoProvider,
infoInOut.user, usePkgIcon, useLowResIcon);
applyCacheEntry(entry, infoInOut);
}
繼續封裝AppInfo
protected CacheEntry cacheLocked(
@NonNull ComponentName componentName,
@NonNull Provider<LauncherActivityInfo> infoProvider,
UserHandle user, boolean usePackageIcon, boolean useLowResIcon) {
Preconditions.assertWorkerThread();
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.
LauncherActivityInfo info = null;
boolean providerFetchedOnce = false;
if (!getEntryFromDB(cacheKey, entry, useLowResIcon) || DEBUG_IGNORE_CACHE) {
info = infoProvider.get();
providerFetchedOnce = true;
if (info != null) {
Log.i(TAG, "cacheLocked--> 1 create icon ="+info.getName());
entry.icon = LauncherIcons.createBadgedIconBitmap(
getFullResIcon(info), info.getUser(), mContext,
infoProvider.get().getApplicationInfo().targetSdkVersion);
}
}
...//省略
}
return entry;
}
在LauncherIcons的createBadgedIconBitmap裏處理appIcon,其中第一個參數來自於getFullResIcon(info)
public Drawable getFullResIcon(LauncherActivityInfo info) {
return getFullResIcon(info, true);
}
---
public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
}
---
public Drawable getIcon(LauncherActivityInfo info, int iconDpi, boolean flattenDrawable) {
return info.getIcon(iconDpi);
}
獲取的Icon資源來自於 LauncherActivityInfo
public Drawable getIcon(int density) {
// TODO: Go through LauncherAppsService
final int iconRes = mActivityInfo.getIconResource();
Drawable icon = null;
// Get the preferred density icon from the app's resources
if (density != 0 && iconRes != 0) {
try {
final Resources resources
= mPm.getResourcesForApplication(mActivityInfo.applicationInfo);
icon = resources.getDrawableForDensity(iconRes, density);
} catch (NameNotFoundException | Resources.NotFoundException exc) {
}
}
// Get the default density icon
if (icon == null) {
icon = mActivityInfo.loadIcon(mPm);
}
return icon;
}
關鍵點就是
final Resources resources
= mPm.getResourcesForApplication(mActivityInfo.applicationInfo);
icon = resources.getDrawableForDensity(iconRes, density);
通過各個app的啓動Activity(就是每個app都有的主Activity),獲取到對應的ApplicationInfo,然後獲取到對應包的資源Resources,然後根據那個Resources找到對應Id的drawable.
所以我們找到了Launcher加載app列表,獲取app圖標的地方了,接下來就是怎麼攔截這個操作,使用我們自己主題app的圖標了。
步驟二
使Launcher加載我們自己寫的包裏的資源。
在上面的獲取Resources的時候,是通過對應的ApplicationInfo來獲取的,也就是每個應用程序都有自己對應的Resources對象來管理當前app的資源,我們怎麼去獲取到我們自己寫的包的Resources呢,如果要先去獲取Activityinfo,那就很繞彎了,很巧的是,還有個可用的重載方法。
getResourcesForApplication(ApplicationInfo app)
getResourcesForApplication(String appPackageName)
直接根據包名就可以獲取到資源Resources對象,當然這裏的報名直接寫我們自己的app包名。
所以接下來就是在getFullResIcon那裏做攔截,先到我們寫的app裏找資源,更改如下
public Drawable getFullResIcon(LauncherActivityInfo info, boolean flattenDrawable) {
if (!TextUtils.isEmpty(mPackname)) {
Drawable drawable = getFullResIconLanco(info.getActivityInfo());
if (drawable != null) {
return drawable;
}
}
return mIconProvider.getIcon(info, mIconDpi, flattenDrawable);
}
public Drawable getFullResIconLanco(ActivityInfo info) {
Drawable drawable = null;
Resources resourcesX;
Resources resourcesY;
try {
resourcesX= mPackageManager.getResourcesForApplication(
info.applicationInfo);
resourcesY = mPackageManager.getResourcesForApplication(
mPackname);
} catch (PackageManager.NameNotFoundException e) {
resourcesX = null;
resourcesY = null;
}
if (resourcesX != null && resourcesY != null) {
int iconId = info.getIconResource();
if (iconId != 0) {
drawable = getFullResIcon(resourcesX, resourcesY, iconId);
if (drawable != null) {
return drawable;
}
}
}
return drawable;
}
當獲取對應Icon的時候,一併得到我們自己app的Resources
private Drawable getFullResIcon(Resources resourcesReal,Resources resourcesFake, int iconId) {
Drawable d;
try {
String name = resourcesReal.getResourceEntryName(iconId);
String type = resourcesReal.getResourceTypeName(iconId);
if (!mThemename.equals("")) {
name = name + "_" + mThemename;
}
int fakeid = resourcesFake.getIdentifier(name,"drawable", mPackname);
Log.i(TAG, "getFullResIcon--> name = " + name + "--type =" + type+"--iconId="+iconId+"--fakeid="+fakeid);
d = resourcesFake.getDrawableForDensity(fakeid, mIconDpi);
} catch (Resources.NotFoundException e) {
d = null;
}
return d ;
}
根據真實的Resources獲取到對應icon的名稱,然後根據名稱,在我們寫的app的Resources裏找到同名的id,再找到對應的Drawable。
其中,根據mThemename主題名稱的不同,我們加載不同的icon名稱,
比如:如果主題名字叫theme,正常的appIcon名稱 ic_launcher_main, 對應我們包裏的icon就命名成ic_launcher_main_theme, 不同主題加不同的後綴,用於區分不同的id,
關於mThemename和mPackname,主題名和包名的傳遞,是在我們自己app裏直接發送Intent過來的,然後把對應名稱設置到IconCache這個類裏來就行了,別忘了,前面特別提到LauncherModel是個BroadcastReceiver,發廣播的事情不提了。
步驟三
在我們的app裏定義不同的主題包
首先要內置第三方app,然後在makefile裏區別資源包,我的方法是給出三個目錄
LOCAL_RESOURCE_DIR := \
$(LOCAL_PATH)/res \
$(LOCAL_PATH)/res_fake\
$(LOCAL_PATH)/res_xposed \
創建不同的主題資源,其餘的就是對應圖片命名,然後別忘了引用他們,不然編譯不能生成對應R資源。
<resources>
<drawable name="ic_launcher_browser_fake">@drawable/ic_launcher_browser_fake</drawable>
<drawable name="ic_launcher_phone_fake">@drawable/ic_launcher_phone_fake</drawable>
<drawable name="ic_launcher_mms_fake">@drawable/ic_launcher_mms_fake</drawable>
</resources>
然後R文件裏就會有我們的ID了,
public static final int ic_launcher_browser_fake=0x7f08006e;
public static final int ic_launcher_phone_fake=0x7f080080;
public static final int ic_launcher_mms_fake=0x7f08007d;
這樣,在Launcher找我們包裏的圖片的時候,找的就是這些我們加進來的圖片id.
Tips
Launcher 初始化之後,就不會再加載一次圖片,因爲那些app列表信息會存儲到數據庫裏,要想每次我們切換主題都有效,就必須強制清空數據,
mIconCache.clear();
這是必須的,以及
if (clearDb) {
Log.d(TAG, "loadWorkspace: resetting launcher database");
LauncherSettings.Settings.call(contentResolver,
LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
}
這是清除數據庫,具體的都在源碼裏。只有這樣纔會更新,不然就從緩存裏和數據庫裏找資源了。
步驟四
更改對應壁紙
這裏只需要在切換主題的時候更換壁紙,
try {
if (mWallPaperid == 0) {
clearWallpaper();
}else {
WallpaperManager.getInstance(this).
setBitmap(BitmapFactory.decodeResource(getResources(), mWallPaperid));
}
} catch (IOException e) {
e.printStackTrace();
}
效果圖:
總結:
以上過程思路都是我個人摸索的,我不知道像華爲和小米那些公司是怎麼在處理主題切換的,不過我能想到的是,他們也是改ResID來主題app裏找resource,只不過會封裝的很多,這也許只有等以後技術厲害了纔會知道吧~
關於這個app,我這裏只是簡單實現了一鍵切換桌面app圖標和壁紙,其他的比如系統其他圖標和動態壁紙這些還不清楚怎麼實現,還有關於網絡下載主題加載資源包的問題,這裏也沒有給出辦法,不過目前需求只是內置幾套主題可以,以後升級加載的事再說。