admin管理员组文章数量:1516870
项目需求:编写主题app,一键切换桌面app图标和系统壁纸。
Android版本:8.1
需求是写一个系统主题app,实现类似于华为手机内置系统主题app的功能,原生android是没有主题app的,网上都是app换肤框架,是给自己单独的app换主题,百般无奈只能自己动手写了。还好我们是在源码基础上开发,可以任性的自定义功能。
步骤一
先找到Launcher加载各种app的地方
Launcher的初始化过程:
@OverrideprotectedvoidonCreate(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
publicclassLauncherModelextendsBroadcastReceiver去看它的startLoader
publicbooleanstartLoader(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(newRunnable(){publicvoidrun(){
oldCallbacks.clearPendingBinds();}});stopLoader();
LoaderResults loaderResults =newLoaderResults(mApp, sBgDataModel,
mBgAllAppsList, synchronousBindPage, mCallbacks);if(mModelLoaded &&!mIsLoaderTaskRunning){
loaderResults.bindWorkspace();
loaderResults.bindAllApps();
loaderResults.bindDeepShortcuts();
loaderResults.bindWidgets();returntrue;}else{startLoaderForResults(loaderResults);}}}returnfalse;}第一次初始化mModelLoaded肯定是false无疑,所以进入startLoaderForResults(loaderResults)
publicvoidstartLoaderForResults(LoaderResults results){synchronized(mLock){stopLoader();
mLoaderTask =newLoaderTask(mApp, mBgAllAppsList, sBgDataModel, results,packName,themeName);runOnWorkerThread(mLoaderTask);}}LoaderTask是一个Runnable,
privatestaticvoidrunOnWorkerThread(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方法
publicvoidrun(){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 stepif(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 stepif(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 了。
privatevoidloadAllApps(){finallong loadTime = DEBUG_LOADERS ? SystemClock.uptimeMillis():0;final List<UserHandle> profiles = mUserManager.getUserProfiles();
mBgAllAppsList.clear();for(UserHandle user : profiles){...//省略// Create the ApplicationInfosfor(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(newAppInfo(app, user, quietMode), app);}...//省略
ManagedProfileHeuristic.onAllAppsLoaded(mApp.getContext(), apps, user);}...//省略}其中,for循环用LauncherActivityInfo封装给AppInfo,再添加进list里,
publicvoidadd(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的,
publicsynchronizedvoidgetTitleAndIcon(ItemInfoWithIcon info,
LauncherActivityInfo activityInfo,boolean useLowResIcon){// If we already have activity info, no need to use package icongetTitleAndIcon(info, Provider.of(activityInfo),false, useLowResIcon);}---privatesynchronizedvoidgetTitleAndIcon(@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 =newComponentKey(componentName, user);
CacheEntry entry = mCache.get(cacheKey);if(entry == null ||(entry.isLowResIcon &&!useLowResIcon)){
entry =newCacheEntry();
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){returngetFullResIcon(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 LauncherAppsServicefinalint iconRes = mActivityInfo.getIconResource();
Drawable icon = null;// Get the preferred density icon from the app's resourcesif(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 iconif(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了,
publicstaticfinalint ic_launcher_browser_fake=0x7f08006e;publicstaticfinalint ic_launcher_phone_fake=0x7f080080;publicstaticfinalint 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();}效果图:
版权声明:本文标题:告别单调:Android用户必学的系统主题切换攻略 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://www.betaflare.com/biancheng/1770646832a3256969.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。


发表评论