Android换肤原理和Android-Skin-Loader框架解析

2024-02-09 07:08

本文主要是介绍Android换肤原理和Android-Skin-Loader框架解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

换皮肤啦

前言

Android换肤技术已经是很久之前就已经被成熟使用的技术了,然而我最近才在学习和接触热修复的时候才看到。在看了一些换肤的方法之后,并且对市面上比较认可的Android-Skin-Loader换肤框架的源码进行了分析总结。再次记录一下祭奠自己逝去的时间。

文章目录

  • 前言
  • 换肤介绍
    • 换肤方式一:切换使用主题Theme
    • 换肤方式二:加载资源包
  • Android换肤知识点
    • 换肤相应的API
    • AssetManager构造
    • 换肤Resources构造
    • 使用资源包中的资源换肤
    • LayoutInflater.Factory
  • Android-Skin-Loader解析
    • 初始化
    • 构造换肤对象
    • 定义基类
    • SkinInflaterFactory
      • 构造View
      • 对生产的View进行换肤
    • 资源获取
    • 其他
  • 总结

换肤介绍

换肤本质上是对资源的一中替换包括、字体、颜色、背景、图片、大小等等。当然这些我们都有成熟的api可以通过控制代码逻辑做到。比如View的修改背景颜色setBackgroundColor,TextView的setTextSize修改字体等等。但是作为程序员我们怎么能忍受对每个页面的每个元素一个行行代码做换肤处理呢?我们需要用最少的代码实现最容易维护和使用效果完美(动态切换,及时生效)的换肤框架。

换肤方式一:切换使用主题Theme

使用相同的资源id,但在不同的Theme下边自定义不同的资源。我们通过主动切换到不同的Theme从而切换界面元素创建时使用的资源。这种方案的代码量不多发,而且有个很明显的缺点不支持已经创建界面的换肤,必须重新加载界面元素。GitHub Demo

换肤方式二:加载资源包

加载资源包是各种应用程序都在使用的换肤方法,例如我们最常用的输入法皮肤、浏览器皮肤等等。我们可以将皮肤的资源文件放入安装包内部,也可以进行下载缓存到磁盘上。Android的应用程序可以使用这种方式进行换肤。GitHub上面有一个start非常高的换肤框架Android-Skin-Loader 就是通过加载资源包对app进行换肤。对这个框架的分析这个也是这篇文章主要的讲述内容。

对比一下发现切换Theme可以进行小幅度的换肤设置(比如某个自定义组件的主题),而如果我们想要对整个app做主题切换那么通过加载资源包的这种方式目前应该说是比较好的了。

Android换肤知识点

换肤相应的API

我们先来看一下Android提供的一些基本的api,通过使用这些api可以在App内部进行资源对象的替换。

public class Resources {public String getString(int id) throws NotFoundException {CharSequence res = mAssets.getResourceText(id);if (res != null) {return res;}throw new NotFoundException("String resource ID #0x"+ Integer.toHexString(id));}public Drawable getDrawable(int id) throws NotFoundException {/********部分代码省略*******/}public int getColor(int id) throws NotFoundException {{/********部分代码省略*******/}/********部分代码省略*******/
}

这个是我们常用的Resources类的api,我们通常可以使用在资源文件中定义的@+idString类型,然后在编译出的R.java中对应的资源文件生产的id(int类型),从而通过这个id(int类型)调用Resources提供的这些api获取到对应的资源对象。这个在同一个app下没有任何问题,但是在皮肤包中我们怎么获取这个id值呢。

public class Resources {/********部分代码省略*******//*** 通过给的资源名称返回一个资源的标识id。* @param name 描述资源的名称* @param defType 资源的类型* @param defPackage 包名* * @return 返回资源id,0标识未找到该资源*/public int getIdentifier(String name, String defType, String defPackage) {if (name == null) {throw new NullPointerException("name is null");}try {return Integer.parseInt(name);} catch (Exception e) {// Ignore}return mAssets.getResourceIdentifier(name, defType, defPackage);}
}

Resources提供了可以通过@+id、Type、PackageName这三个参数就可以在AssetManager中寻找相应的PackageName中有没有Type类型并且id值都能与参数对应上的id,进行返回。然后我们可以通过这个id再调用Resource的获取资源的api就可以得到相应的资源。

这里我们需要注意的一点是getIdentifier(String name, String defType, String defPackage)方法和getString(int id)方法所调用Resources对象的mAssets对象必须是同一个,并且包含有PackageName这个资源包。

AssetManager构造

怎么构造一个包含特定packageName资源的AssetManager对象实例呢?

public final class AssetManager implements AutoCloseable {/********部分代码省略*******//*** Create a new AssetManager containing only the basic system assets.* Applications will not generally use this method, instead retrieving the* appropriate asset manager with {@link Resources#getAssets}.    Not for* use by applications.* {@hide}*/public AssetManager() {synchronized (this) {if (DEBUG_REFS) {mNumRefs = 0;incRefsLocked(this.hashCode());}init(false);if (localLOGV) Log.v(TAG, "New asset manager: " + this);ensureSystemAssets();}}

从AssetManager的构造函数来看有{@hide}的朱姐,所以在其他类里面是直接创建AssetManager实例。但是不要忘记Java中还有反射机制可以创建类对象。

AssetManager assetManager = AssetManager.class.newInstance();

让创建的assetManager包含特定的PackageName的资源信息,怎么办?我们在AssetManager中找到相应的api可以调用。

public final class AssetManager implements AutoCloseable {/********部分代码省略*******//*** Add an additional set of assets to the asset manager.  This can be* either a directory or ZIP file.  Not for use by applications.  Returns* the cookie of the added asset, or 0 on failure.* {@hide}*/public final int addAssetPath(String path) {synchronized (this) {int res = addAssetPathNative(path);if (mStringBlocks != null) {makeStringBlocks(mStringBlocks);}return res;}}
}

同样改方法也不支持外部调用,我们只能通过反射的方法来调用。

/*** apk路径*/
String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";
AssetManager assetManager = null;
try {AssetManager assetManager = AssetManager.class.newInstance();AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);
} catch (Throwable th) {th.printStackTrace();
}

至此我们可以构造属于自己换肤的Resources了。

换肤Resources构造

public Resources getSkinResources(Context context){/*** 插件apk路径*/String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";AssetManager assetManager = null;try {AssetManager assetManager = AssetManager.class.newInstance();AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);} catch (Throwable th) {th.printStackTrace();}return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}

使用资源包中的资源换肤

我们将上述所有的代码组合在一起就可以实现,使用资源包中的资源对app进行换肤。

public Resources getSkinResources(Context context){/*** 插件apk路径*/String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";AssetManager assetManager = null;try {AssetManager assetManager = AssetManager.class.newInstance();AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);} catch (Throwable th) {th.printStackTrace();}return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}
@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);ImageView imageView = (ImageView) findViewById(R.id.imageView);TextView textView = (TextView) findViewById(R.id.text);/*** 插件资源对象*/Resources resources = getSkinResources(this);/*** 获取图片资源*/Drawable drawable = resources.getDrawable(resources.getIdentifier("night_icon", "drawable","com.tzx.skin"));/*** 获取Color资源*/int color = resources.getColor(resources.getIdentifier("night_color","color","com.tzx.skin"));imageView.setImageDrawable(drawable);textView.setText(text);}

通过上述介绍,我们可以简单的对当前页面进行换肤了。但是想要做出一个一个成熟换肤框架那么仅仅这些还是不够的,提高一下我们的思维高度,如果我们在View创建的时候就直接使用皮肤资源包中的资源文件,那么这无疑就使换肤更加的简单已维护。

LayoutInflater.Factory

看过我前一篇遇见LayoutInflater&Factory文章的这部分可以省略掉.

很幸运Android给我们在View生产的时候做修改提供了法门。

public abstract class LayoutInflater {/***部分代码省略****/public interface Factory {public View onCreateView(String name, Context context, AttributeSet attrs);}public interface Factory2 extends Factory {public View onCreateView(View parent, String name, Context context, AttributeSet attrs);}/***部分代码省略****/
}

我们可以给当前的页面的Window对象在创建的时候设置Factory,那么在Window中的View进行创建的时候就会先通过自己设置的Factory进行创建。Factory使用方式和相关注意事项请移位到遇见LayoutInflater&Factory,关于Factory的相关知识点尽在其中。

Android-Skin-Loader解析

初始化

  • 初始化换肤框架,导入需要换肤的资源包(当前为一个apk文件,其中只有资源文件)。
public class SkinApplication extends Application {public void onCreate() {super.onCreate();initSkinLoader();}/*** Must call init first*/private void initSkinLoader() {SkinManager.getInstance().init(this);SkinManager.getInstance().load();}
}

构造换肤对象

  • 导入需要换肤的资源包,并构造换肤的Resources实例。
/*** Load resources from apk in asyc task* @param skinPackagePath path of skin apk* @param callback callback to notify user*/
public void load(String skinPackagePath, final ILoaderListener callback) {new AsyncTask<String, Void, Resources>() {protected void onPreExecute() {if (callback != null) {callback.onStart();}};@Overrideprotected Resources doInBackground(String... params) {try {if (params.length == 1) {String skinPkgPath = params[0];File file = new File(skinPkgPath); if(file == null || !file.exists()){return null;}PackageManager mPm = context.getPackageManager();//检索程序外的一个安装包文件PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);//获取安装包报名skinPackageName = mInfo.packageName;//构建换肤的AssetManager实例AssetManager assetManager = AssetManager.class.newInstance();Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);addAssetPath.invoke(assetManager, skinPkgPath);//构建换肤的Resources实例Resources superRes = context.getResources();Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());//存储当前皮肤路径SkinConfig.saveSkinPath(context, skinPkgPath);skinPath = skinPkgPath;isDefaultSkin = false;return skinResource;}return null;} catch (Exception e) {e.printStackTrace();return null;}};protected void onPostExecute(Resources result) {mResources = result;if (mResources != null) {if (callback != null) callback.onSuccess();//更新多有可换肤的界面notifySkinUpdate();}else{isDefaultSkin = true;if (callback != null) callback.onFailed();}};}.execute(skinPackagePath);
}

定义基类

  • 换肤页面的基类的通用代码实现基本换肤功能。
public class BaseFragmentActivity extends FragmentActivity implements ISkinUpdate, IDynamicNewView{/***部分代码省略****///自定义LayoutInflater.Factoryprivate SkinInflaterFactory mSkinInflaterFactory;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);try {//设置LayoutInflater的mFactorySet为true,表示还未设置mFactory,否则会抛出异常。Field field = LayoutInflater.class.getDeclaredField("mFactorySet");field.setAccessible(true);field.setBoolean(getLayoutInflater(), false);//设置LayoutInflater的MFactorymSkinInflaterFactory = new SkinInflaterFactory();getLayoutInflater().setFactory(mSkinInflaterFactory);} catch (NoSuchFieldException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} }@Overrideprotected void onResume() {super.onResume();//注册皮肤管理对象SkinManager.getInstance().attach(this);}@Overrideprotected void onDestroy() {super.onDestroy();//反注册皮肤管理对象SkinManager.getInstance().detach(this);}/***部分代码省略****/
}

SkinInflaterFactory

  • SkinInflaterFactory进行View的创建并对View进行换肤。

构造View

public class SkinInflaterFactory implements Factory {/***部分代码省略****/public View onCreateView(String name, Context context, AttributeSet attrs) {//读取View的skin:enable属性,false为不需要换肤// if this is NOT enable to be skined , simplly skip it boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);if (!isSkinEnable){return null;}//创建ViewView view = createView(context, name, attrs);if (view == null){return null;}//如果View创建成功,对View进行换肤parseSkinAttr(context, attrs, view);return view;}//创建View,类比可以查看LayoutInflater的createViewFromTag方法private View createView(Context context, String name, AttributeSet attrs) {View view = null;try {if (-1 == name.indexOf('.')){if ("View".equals(name)) {view = LayoutInflater.from(context).createView(name, "android.view.", attrs);} if (view == null) {view = LayoutInflater.from(context).createView(name, "android.widget.", attrs);} if (view == null) {view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs);} }else {view = LayoutInflater.from(context).createView(name, null, attrs);}L.i("about to create " + name);} catch (Exception e) { L.e("error while create 【" + name + "】 : " + e.getMessage());view = null;}return view;}
}

对生产的View进行换肤

public class SkinInflaterFactory implements Factory {//存储当前Activity中的需要换肤的Viewprivate List<SkinItem> mSkinItems = new ArrayList<SkinItem>();/***部分代码省略****/private void parseSkinAttr(Context context, AttributeSet attrs, View view) {//当前View的所有属性标签List<SkinAttr> viewAttrs = new ArrayList<SkinAttr>();for (int i = 0; i < attrs.getAttributeCount(); i++){String attrName = attrs.getAttributeName(i);String attrValue = attrs.getAttributeValue(i);if(!AttrFactory.isSupportedAttr(attrName)){continue;}//过滤view属性标签中属性的value的值为引用类型if(attrValue.startsWith("@")){try {int id = Integer.parseInt(attrValue.substring(1));String entryName = context.getResources().getResourceEntryName(id);String typeName = context.getResources().getResourceTypeName(id);//构造SkinAttr实例,attrname,id,entryName,typeName//属性的名称(background)、属性的id值(int类型),属性的id值(@+id,string类型),属性的值类型(color)SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);if (mSkinAttr != null) {viewAttrs.add(mSkinAttr);}} catch (NumberFormatException e) {e.printStackTrace();} catch (NotFoundException e) {e.printStackTrace();}}}//如果当前View需要换肤,那么添加在mSkinItems中if(!ListUtils.isEmpty(viewAttrs)){SkinItem skinItem = new SkinItem();skinItem.view = view;skinItem.attrs = viewAttrs;mSkinItems.add(skinItem);//是否是使用外部皮肤进行换肤if(SkinManager.getInstance().isExternalSkin()){skinItem.apply();}}}
}

资源获取

通过当前的资源id,找到对应的资源name。再从皮肤包中找到该资源name所对应的资源id。

public class SkinManager implements ISkinLoader{/***部分代码省略****/public int getColor(int resId){int originColor = context.getResources().getColor(resId);//是否没有下载皮肤或者当前使用默认皮肤if(mResources == null || isDefaultSkin){return originColor;}//根据resId值获取对应的xml的的@+id的String类型的值String resName = context.getResources().getResourceEntryName(resId);//更具resName在皮肤包的mResources中获取对应的resIdint trueResId = mResources.getIdentifier(resName, "color", skinPackageName);int trueColor = 0;try{//根据resId获取对应的资源valuetrueColor = mResources.getColor(trueResId);}catch(NotFoundException e){e.printStackTrace();trueColor = originColor;}return trueColor;}public Drawable getDrawable(int resId){...}
}

其他

除此之外再增加以下对于皮肤的管理api(下载、监听回调、应用、取消、异常处理、扩展模块等等)。

总结

换肤就是这么简单

文章到这里就全部讲述完啦,若有其他需要交流的可以留言哦

想阅读作者的更多文章,可以查看我 个人博客 和公共号:
振兴书城

这篇关于Android换肤原理和Android-Skin-Loader框架解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/693489

相关文章

Android 悬浮窗开发示例((动态权限请求 | 前台服务和通知 | 悬浮窗创建 )

《Android悬浮窗开发示例((动态权限请求|前台服务和通知|悬浮窗创建)》本文介绍了Android悬浮窗的实现效果,包括动态权限请求、前台服务和通知的使用,悬浮窗权限需要动态申请并引导... 目录一、悬浮窗 动态权限请求1、动态请求权限2、悬浮窗权限说明3、检查动态权限4、申请动态权限5、权限设置完毕后

Android里面的Service种类以及启动方式

《Android里面的Service种类以及启动方式》Android中的Service分为前台服务和后台服务,前台服务需要亮身份牌并显示通知,后台服务则有启动方式选择,包括startService和b... 目录一句话总结:一、Service 的两种类型:1. 前台服务(必须亮身份牌)2. 后台服务(偷偷干

修改若依框架Token的过期时间问题

《修改若依框架Token的过期时间问题》本文介绍了如何修改若依框架中Token的过期时间,通过修改`application.yml`文件中的配置来实现,默认单位为分钟,希望此经验对大家有所帮助,也欢迎... 目录修改若依框架Token的过期时间修改Token的过期时间关闭Token的过期时js间总结修改若依

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

在Rust中要用Struct和Enum组织数据的原因解析

《在Rust中要用Struct和Enum组织数据的原因解析》在Rust中,Struct和Enum是组织数据的核心工具,Struct用于将相关字段封装为单一实体,便于管理和扩展,Enum用于明确定义所有... 目录为什么在Rust中要用Struct和Enum组织数据?一、使用struct组织数据:将相关字段绑

MySQL中的MVCC底层原理解读

《MySQL中的MVCC底层原理解读》本文详细介绍了MySQL中的多版本并发控制(MVCC)机制,包括版本链、ReadView以及在不同事务隔离级别下MVCC的工作原理,通过一个具体的示例演示了在可重... 目录简介ReadView版本链演示过程总结简介MVCC(Multi-Version Concurr

使用Java实现一个解析CURL脚本小工具

《使用Java实现一个解析CURL脚本小工具》文章介绍了如何使用Java实现一个解析CURL脚本的工具,该工具可以将CURL脚本中的Header解析为KVMap结构,获取URL路径、请求类型,解析UR... 目录使用示例实现原理具体实现CurlParserUtilCurlEntityICurlHandler

深入解析Spring TransactionTemplate 高级用法(示例代码)

《深入解析SpringTransactionTemplate高级用法(示例代码)》TransactionTemplate是Spring框架中一个强大的工具,它允许开发者以编程方式控制事务,通过... 目录1. TransactionTemplate 的核心概念2. 核心接口和类3. TransactionT

数据库使用之union、union all、各种join的用法区别解析

《数据库使用之union、unionall、各种join的用法区别解析》:本文主要介绍SQL中的Union和UnionAll的区别,包括去重与否以及使用时的注意事项,还详细解释了Join关键字,... 目录一、Union 和Union All1、区别:2、注意点:3、具体举例二、Join关键字的区别&php