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

相关文章

网页解析 lxml 库--实战

lxml库使用流程 lxml 是 Python 的第三方解析库,完全使用 Python 语言编写,它对 XPath表达式提供了良好的支 持,因此能够了高效地解析 HTML/XML 文档。本节讲解如何通过 lxml 库解析 HTML 文档。 pip install lxml lxm| 库提供了一个 etree 模块,该模块专门用来解析 HTML/XML 文档,下面来介绍一下 lxml 库

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

Android平台播放RTSP流的几种方案探究(VLC VS ExoPlayer VS SmartPlayer)

技术背景 好多开发者需要遴选Android平台RTSP直播播放器的时候,不知道如何选的好,本文针对常用的方案,做个大概的说明: 1. 使用VLC for Android VLC Media Player(VLC多媒体播放器),最初命名为VideoLAN客户端,是VideoLAN品牌产品,是VideoLAN计划的多媒体播放器。它支持众多音频与视频解码器及文件格式,并支持DVD影音光盘,VCD影

cross-plateform 跨平台应用程序-03-如果只选择一个框架,应该选择哪一个?

跨平台系列 cross-plateform 跨平台应用程序-01-概览 cross-plateform 跨平台应用程序-02-有哪些主流技术栈? cross-plateform 跨平台应用程序-03-如果只选择一个框架,应该选择哪一个? cross-plateform 跨平台应用程序-04-React Native 介绍 cross-plateform 跨平台应用程序-05-Flutte

Spring框架5 - 容器的扩展功能 (ApplicationContext)

private static ApplicationContext applicationContext;static {applicationContext = new ClassPathXmlApplicationContext("bean.xml");} BeanFactory的功能扩展类ApplicationContext进行深度的分析。ApplicationConext与 BeanF

android-opencv-jni

//------------------start opencv--------------------@Override public void onResume(){ super.onResume(); //通过OpenCV引擎服务加载并初始化OpenCV类库,所谓OpenCV引擎服务即是 //OpenCV_2.4.3.2_Manager_2.4_*.apk程序包,存

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu