一 前言
Android的換膚技術(shù)已經(jīng)是很久之前就已經(jīng)被成熟使用的技術(shù)了,然而我最近才在學(xué)習(xí)和接觸熱修復(fù)的時(shí)候才看到住册,在看了一些換膚的方法之后,并且對(duì)市面上比較認(rèn)可的Android的皮膚瓮具,裝載機(jī)換膚框架的源碼進(jìn)行了分析總結(jié)荧飞。再次記錄一下祭奠自己逝去的時(shí)間。
二 換膚介紹
換膚本質(zhì)上是對(duì)資源的一中替換包括名党,字體叹阔,顏色,背景传睹,圖片耳幢,大小等等。當(dāng)然這些我們都有成熟的API可以通過控制代碼邏輯做到欧啤。比如查看的修改背景顏色setBackgroundColor
睛藻,TextView中的setTextSize
。修改字體等等但是作為程序員我們怎么能忍受對(duì)每個(gè)頁面的每個(gè)元素一個(gè)行行代碼做換膚處理呢堂油?我們需要用最少的代碼實(shí)現(xiàn)最容易維護(hù)和使用效果完美(動(dòng)態(tài)切換修档,及時(shí)生效)的換膚框架。
1.換膚方式一:切換使用主題主題
使用相同的資源ID府框,但在不同的主題下邊自定義不同的資源吱窝。我們通過主動(dòng)切換到不同的主題從而切換界面元素創(chuàng)建時(shí)使用的資源。這種方案的代碼量不多發(fā)迫靖,而且有個(gè)很明顯的缺點(diǎn)不支持已經(jīng)創(chuàng)建界面的換膚院峡,必須重新加載界面元素.GitHub Demo
2. 換膚方式二:加載資源包
加載資源包是各種應(yīng)用程序都在使用的換膚方法,例如我們最常用的輸入法皮膚系宜,瀏覽器皮膚等等照激。我們可以將皮膚的資源文件放入安裝包內(nèi)部,也可以進(jìn)行下載緩存到磁盤上.Android的應(yīng)用程序可以使用這種方式進(jìn)行換膚.GitHub上面有一個(gè)開始非常高的換膚框架Android的皮膚下載器就是通過加載資源包對(duì)應(yīng)用程序進(jìn)行換膚盹牧。對(duì)這個(gè)框架的分析這個(gè)也是這篇文章主要的講述內(nèi)容俩垃。
對(duì)比一下發(fā)現(xiàn)切換主題可以進(jìn)行小幅度的換膚設(shè)置(比如某個(gè)自定義組件的主題)励幼,而如果我們想要對(duì)整個(gè)應(yīng)用程序做主題切換那么通過加載資源包的這種方式目前應(yīng)該說是比較好的了。
三 Android的換膚知識(shí)點(diǎn)
1. 換膚相應(yīng)的API
我們先來看一下Android的提供的一些基本的API口柳,通過使用這些API可以在應(yīng)用程序內(nèi)部進(jìn)行資源對(duì)象的替換苹粟。
公共類資源{
public String getString(int id)throws NotFoundException {
CharSequence res = mAssets.getResourceText(id);
if(res!= null){
返回資源;
}
拋出新的NotFoundException(“字符串資源ID#0x”
+ Integer.toHexString(id));
}
public Drawable getDrawable(int id)throws NotFoundException {
/ ********部分代碼省略******* /
}
public int getColor(int id)throws NotFoundException {{
/ ********部分代碼省略******* /
}
/ ********部分代碼省略******* /
}
這個(gè)是我們常用的資源類的API跃闹,我們通城断鳎可以使用在資源文件中定義的@+id字符串類型,然后在編譯出的R.java中對(duì)應(yīng)的資源文件生產(chǎn)的編號(hào)(INT類型)望艺,從而通過這個(gè)ID(INT類型)調(diào)用資源提供的這些API獲取到對(duì)應(yīng)的資源對(duì)象苛秕。這個(gè)在同一個(gè)應(yīng)用程序下沒有任何問題,但是在皮膚包中我們怎么獲取這個(gè)ID值呢找默。
公共類資源{
/ ********部分代碼省略******* /
/ **
*通過給的資源名稱返回一個(gè)資源的標(biāo)識(shí)id艇劫。
* @paramname描述資源的名稱
* @ paramdefType資源的類型
* @paramdefPackage包名
*
* @返回返回資源ID,0標(biāo)識(shí)未找到該資源
* /
public int getIdentifier(String name啡莉,String defType港准,String defPackage){
if(name == null){
拋出新的NullPointerException(“name is null”);
}
嘗試{
return Integer.parseInt(name);
} catch(例外e){
// 忽視
}
return mAssets.getResourceIdentifier(name旨剥,defType咧欣,defPackage);
}
}
資源提供了可以通過@+id,類型轨帜,PACKAGENAME這三個(gè)參數(shù)就可以在AssetManager中尋找相應(yīng)的軟件包名中有沒有輸入類型并且ID值都能與參數(shù)對(duì)應(yīng)上的ID魄咕,進(jìn)行返回。然后我們可以通過這個(gè)ID再調(diào)用資源的獲取資源的API就可以得到相應(yīng)的資源蚌父。
我們這里需要注意的一點(diǎn)一的英文getIdentifier(String name, String defType, String defPackage)方法狀語從句:getString(int id)方法所調(diào)用資源對(duì)象的mAssets對(duì)象必須是同一個(gè)哮兰,并且包含有PACKAGENAME這個(gè)資源包。
2.AssetManager構(gòu)造
怎么構(gòu)造一個(gè)包含特定的packageName資源的AssetManager對(duì)象實(shí)例呢苟弛?
public final class AssetManagerimplements AutoCloseable {
/ ********部分代碼省略******* /
/ **
*創(chuàng)建僅包含基本系統(tǒng)資產(chǎn)的新AssetManager喝滞。
*應(yīng)用程序通常不會(huì)使用此方法,而是檢索
* {@ linkResources#getAssets}的適當(dāng)資產(chǎn)經(jīng)理膏秫。不是為了
*由應(yīng)用程序使用右遭。
* {@hide}
* /
public AssetManager(){
synchronized(this){
if(DEBUG_REFS){
mNumRefs = 0;
incRefsLocked(this.hashCode());
}
INIT(假);
if(localLOGV)Log.v(TAG,“新資產(chǎn)經(jīng)理:”+這個(gè));
ensureSystemAssets();
}
}
從AssetManager構(gòu)造的函數(shù)來看有{@hide}的朱姐缤削,所以在其他類里面是直接創(chuàng)建AssetManager實(shí)例窘哈。但是不要忘記的Java中還有反射機(jī)制可以創(chuàng)建類對(duì)象。
1
AssetManager assetManager = AssetManager.class.newInstance();
讓創(chuàng)建的assetManager包含特定的PACKAGENAME的資源信息亭敢,怎么辦滚婉?我們在AssetManager中找到相應(yīng)的API可以調(diào)用。
public final class AssetManagerimplements AutoCloseable {
/ ********部分代碼省略******* /
/ **
*向資產(chǎn)經(jīng)理添加一組額外資產(chǎn)帅刀。這可以
*目錄或ZIP文件让腹。不適用于應(yīng)用程序远剩。返回
*添加資產(chǎn)的cookie,或失敗時(shí)為0骇窍。
* {@hide}
* /
public final int addAssetPath(String path){
synchronized(this){
int res = addAssetPathNative(path);
if(mStringBlocks民宿!= null){
makeStringBlocks(mStringBlocks);
}
返回資源;
}
}
}
同樣改方法也不支持外部調(diào)用,我們只能通過反射的方法來調(diào)用像鸡。
/ **
* apk路徑
* /
String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;
AssetManager assetManager = null;
嘗試{
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod(“addAssetPath”活鹰,String.class).invoke(assetManager,apkPath);
} catch(Throwable th){
th.printStackTrace();
}
至此我們可以構(gòu)造屬于自己換膚的資源了只估。
3.換膚資源構(gòu)造
public Resources getSkinResources(Context context){
/ **
*插件apk路徑
* /
String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;
AssetManager assetManager = null;
嘗試{
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod(“addAssetPath”志群,String.class).invoke(assetManager,apkPath);
} catch(Throwable th){
th.printStackTrace();
}
返回新資源(assetManager蛔钙,context.getResources()锌云。getDisplayMetrics(),context.getResources()吁脱。getConfiguration());
}
4.使用資源包中的資源換膚
我們將上述所有的代碼組合在一起就可以實(shí)現(xiàn)桑涎,使用資源包中的資源對(duì)應(yīng)用程序進(jìn)行換膚。
public Resources getSkinResources(Context context){
/ **
*插件apk路徑
* /
String apkPath = Environment.getExternalStorageDirectory()+“/ skin.apk”;
AssetManager assetManager = null;
嘗試{
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager.class.getDeclaredMethod(“addAssetPath”兼贡,String.class).invoke(assetManager攻冷,apkPath);
} catch(Throwable th){
th.printStackTrace();
}
返回新資源(assetManager,context.getResources()遍希。getDisplayMetrics()等曼,context.getResources()。getConfiguration());
}
@覆蓋
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);
/ **
*插件資源對(duì)象
* /
Resources resources = getSkinResources(this);
/ **
*獲取圖片資源
* /
Drawable drawable = resources.getDrawable(resources.getIdentifier(“night_icon”凿蒜,“drawable”禁谦,“com.tzx.skin”));
/ **
*獲取文本資源
* /
int color = resources.getColor(resources.getIdentifier(“night_color”,“color”废封,“com.tzx.skin”));
imageView.setImageDrawable(繪制);
textView.setText(文本);
}
通過上述介紹州泊,我們可以簡單的對(duì)當(dāng)前頁面進(jìn)行換膚了。但是想要做出一個(gè)一個(gè)成熟換膚框架那么僅僅這些還是不夠的漂洋,提高一下我們的思維高度遥皂,如果我們在查看創(chuàng)建的時(shí)候就直接使用皮膚資源包中的資源文件,那么這無疑就使換膚更加的簡單已維護(hù)氮发。
5. LayoutInflater.Factory
我看過一篇前遇見LayoutInflater及工廠文章的這部分可以省略掉渴肉。
很幸運(yùn)的Android給我們在查看生產(chǎn)的時(shí)候做修改提供了法門。
公共抽象類LayoutInflater {
/ ***部分代碼省略**** /
公共接口工廠{
public View onCreateView(String name爽冕,Context context仇祭,AttributeSet attrs);
}
public interface Factory2extends Factory {
public View onCreateView(查看父級(jí),字符串名稱颈畸,上下文上下文乌奇,AttributeSet attrs);
}
/ ***部分代碼省略**** /
}
我們可以給當(dāng)前的頁面的窗口對(duì)象在創(chuàng)建的時(shí)候設(shè)置工廠没讲,那么在窗口中的視圖進(jìn)行創(chuàng)建的時(shí)候就會(huì)先通過自己設(shè)置的工廠進(jìn)行創(chuàng)建.Factory方式使用相關(guān)狀語從句:注意事項(xiàng)請移位到遇見LayoutInflater及工廠,關(guān)于工廠的相關(guān)知識(shí)點(diǎn)盡在其中礁苗。
四 Android的皮膚爬凑,裝載機(jī)解析
1. 初始化
- 初始化換膚框架,導(dǎo)入需要換膚的資源包(當(dāng)前為一個(gè)APK文件试伙,其中只有資源文件)嘁信。
公共類SkinApplicationextends Application {
public void onCreate(){
super.onCreate();
initSkinLoader();
}
/ **
*必須先調(diào)用init
* /
private void initSkinLoader(){
。SkinManager.getInstance()的init(本);
SkinManager.getInstance()負(fù)載();
}
}
2.構(gòu)造換膚對(duì)象
導(dǎo)入需要換膚的資源包疏叨,并構(gòu)造換膚的資源實(shí)例潘靖。
/ **
*在asyc任務(wù)中從apk加載資源
* @ paramskinPackagePath皮膚路徑apk
* @paramcallback回調(diào)通知用戶
* /
public void load(String skinPackagePath,final ILoaderListener callback){
新的AsyncTask(){
protected void onPreExecute(){
if(callback蚤蔓!= null){
callback.onStart();
}
};
@覆蓋
protected資源doInBackground(String ... params){
嘗試{
if(params.length == 1){
String skinPkgPath = params [0];
File file = new File(skinPkgPath);
if(file == null ||卦溢!file.exists()){
return null;
}
PackageManager mPm = context.getPackageManager();
//檢索程序外的一個(gè)安裝包文件
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath,PackageManager.GET_ACTIVITIES);
//獲取安裝包報(bào)名
skinPackageName = mInfo.packageName;
//構(gòu)建換膚的AssetManager實(shí)例
AssetManager assetManager = AssetManager.class.newInstance();
方法addAssetPath = assetManager.getClass()秀又。getMethod(“addAssetPath”单寂,String.class);
addAssetPath.invoke(assetManager,skinPkgPath);
//構(gòu)建換膚的資源實(shí)例
資源superRes = context.getResources();
資源skinResource = new Resources(assetManager吐辙,superRes.getDisplayMetrics()宣决,superRes.getConfiguration());
//存儲(chǔ)當(dāng)前皮膚路徑
SkinConfig.saveSkinPath(context,skinPkgPath);
skinPath = skinPkgPath;
isDefaultSkin = false;
return skinResource;
}
return null;
} catch(例外e){
e.printStackTrace();
return null;
}
};
protected void onPostExecute(參考資料結(jié)果){
mResources =結(jié)果;
if(mResources袱讹!= null){
if(callback疲扎!= null)callback.onSuccess();
//更新多有可換膚的界面
notifySkinUpdate();
}其他{
isDefaultSkin = true;
if(callback昵时!= null)callback.onFailed();
}
};
} .execute(skinPackagePath);
}
定義基類
換膚頁面的基類的通用代碼實(shí)現(xiàn)基本換膚功能捷雕。
public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView {
/ ***部分代碼省略**** /
//自定義LayoutInflater.Factory
private SkinInflaterFactory mSkinInflaterFactory;
@覆蓋
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
嘗試{
//設(shè)置LayoutInflater的mFactorySet為真壹甥,表示還未設(shè)置mFactory救巷,否則會(huì)拋出異常。
Field field = LayoutInflater.class.getDeclaredField(“mFactorySet”);
field.setAccessible(真);
field.setBoolean(getLayoutInflater()句柠,false);
//設(shè)置LayoutInflater的MFactory
mSkinInflaterFactory = new SkinInflaterFactory();
getLayoutInflater()setFactory(mSkinInflaterFactory)浦译。
} catch(NoSuchFieldException e){
e.printStackTrace();
} catch(IllegalArgumentException e){
e.printStackTrace();
} catch(IllegalAccessException e){
e.printStackTrace();
}
}
@覆蓋
protected void onResume(){
super.onResume();
//注冊皮膚管理對(duì)象
。SkinManager.getInstance()連接(本);
}
@覆蓋
protected void onDestroy(){
super.onDestroy();
//反注冊皮膚管理對(duì)象
溯职。SkinManager.getInstance()分離(本);
}
/ ***部分代碼省略**** /
}
3.SkinInflaterFactory
SkinInflaterFactory進(jìn)行查看的創(chuàng)建并對(duì)視圖進(jìn)行換膚精盅。
構(gòu)造查看
公共類SkinInflaterFactoryimplements Factory {
/ ***部分代碼省略**** /
public View onCreateView(String name,Context context谜酒,AttributeSet attrs){
//讀取查看的皮膚:使屬性叹俏,假的為不需要換膚
//如果不允許進(jìn)行優(yōu)化,請簡單地跳過它
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE僻族,SkinConfig.ATTR_SKIN_ENABLE粘驰,false);
if(屡谐!isSkinEnable){
return null;
}
//創(chuàng)建視圖
View view = createView(context,name蝌数,attrs);
if(view == null){
return null;
}
//如果視圖創(chuàng)建成功愕掏,對(duì)視圖進(jìn)行換膚
parseSkinAttr(context,attrs顶伞,view);
返回視圖;
}
//創(chuàng)建視圖饵撑,類比可以查看LayoutInflater的createViewFromTag方法
private View createView(Context context,String name唆貌,AttributeSet attrs){
View view = null;
嘗試{
if(-1 == name.indexOf('肄梨。')){
if(“查看”.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);
}
李(“即將創(chuàng)造”+名稱);
} catch(例外e){
Le(“創(chuàng)建時(shí)出錯(cuò)”“+ +名+”:“+ e.getMessage());
view = null;
}
返回視圖;
}
}
4.對(duì)生產(chǎn)的景觀進(jìn)行換膚
公共類SkinInflaterFactoryimplements Factory {
//存儲(chǔ)當(dāng)前活動(dòng)中的需要換膚的查看
private List mSkinItems = new ArrayList();
/ ***部分代碼省略**** /
private void parseSkinAttr(Context context诱担,AttributeSet attrs,View view){
//當(dāng)前查看的所有屬性標(biāo)簽
List viewAttrs = new ArrayList();
for(int i = 0; i <attrs.getAttributeCount(); i ++){
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
如果(电爹!AttrFactory.isSupportedAttr(attrName)){
繼續(xù);
}
//過濾視圖屬性標(biāo)簽中屬性的值的值為引用類型
如果(attrValue.startsWith( “@”)){
嘗試{
int id = Integer.parseInt(attrValue.substring(1));
String entryName = context.getResources()蔫仙。getResourceEntryName(id);
String typeName = context.getResources()。getResourceTypeName(id);
//構(gòu)造SkinAttr實(shí)例丐箩,attrname摇邦,ID,entryName參數(shù)typeName
//屬性的名稱(背景)屎勘,屬性的ID值(INT類型)施籍,屬性的ID值(@ + ID,串類型)概漱,屬性的值類型(顏色)
SkinAttr mSkinAttr = AttrFactory.get(attrName丑慎,id,entryName,typeName);
if(mSkinAttr立哑!= null){
viewAttrs.add(mSkinAttr);
}
} catch(NumberFormatException e){
e.printStackTrace();
} catch(NotFoundException e){
e.printStackTrace();
}
}
}
//如果當(dāng)前視圖需要換膚夜惭,那么添加在mSkinItems中
如果(!ListUtils.isEmpty(viewAttrs)){
SkinItem skinItem = new SkinItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
mSkinItems.add(skinItem);
//是否是使用外部皮膚進(jìn)行換膚
如果(SkinManager.getInstance()铛绰。isExternalSkin()){
skinItem.apply();
}
}
}
}
5.資源獲取
通過當(dāng)前的資源ID诈茧,找到對(duì)應(yīng)的資源名稱。再從皮膚包中找到該資源名稱所對(duì)應(yīng)的資源ID捂掰。
公共類SkinManagerimplements ISkinLoader {
/ ***部分代碼省略**** /
public int getColor(int resId){
int originColor = context.getResources()敢会。getColor(resId);
//是否沒有下載皮膚或者當(dāng)前使用默認(rèn)皮膚
if(mResources == null || isDefaultSkin){
return originColor;
}
//根據(jù)渣油值獲取對(duì)應(yīng)的XML的的@ + ID的字符串類型的值
String resName = context.getResources()。getResourceEntryName(resId);
//更具resName在皮膚包的mResources中獲取對(duì)應(yīng)的渣油
int trueResId = mResources.getIdentifier(resName这嚣,“color”鸥昏,skinPackageName);
int trueColor = 0;
嘗試{
//根據(jù)渣油獲取對(duì)應(yīng)的資源值
trueColor = mResources.getColor(trueResId);
} catch(NotFoundException e){
e.printStackTrace();
trueColor = originColor;
}
return trueColor;
}
public Drawable getDrawable(int resId){...}
}
其他
除此之外再增加以下對(duì)于皮膚的管理API(下載,監(jiān)聽回調(diào)姐帚,應(yīng)用吏垮,取消,異常處理罐旗,擴(kuò)展模塊等等)膳汪。
五 總結(jié)
換膚就是這么簡單?!?九秀!
視頻→
hook源碼實(shí)現(xiàn)阿里無閃爍換膚鏈接:https://pan.baidu.com/s/1E9gdeeLADBiszUU-DFRWvw