Android換膚原理和Android-Skin-Loader框架解析
Android換膚技術(shù)已經(jīng)是很久之前就已經(jīng)被成熟使用的技術(shù)了,然而我最近才在學(xué)習(xí)和接觸熱修復(fù)的時(shí)候才看到。在看了一些換膚的方法之后,并且對(duì)市面上比較認(rèn)可的Android-Skin-Loader換膚框架的源碼進(jìn)行了分析總結(jié)。再次記錄一下祭奠自己逝去的時(shí)間。
換膚介紹
換膚本質(zhì)上是對(duì)資源的一中替換包括、字體、顏色、背景、圖片、大小等等。當(dāng)然這些我們都有成熟的api可以通過控制代碼邏輯做到。比如View的修改背景顏色 setBackgroundColor ,TextView的 setTextSize 修改字體等等。但是作為程序員我們?cè)趺茨苋淌軐?duì)每個(gè)頁面的每個(gè)元素一個(gè)行行代碼做換膚處理呢?我們需要用最少的代碼實(shí)現(xiàn)最容易維護(hù)和使用效果***(動(dòng)態(tài)切換,及時(shí)生效)的換膚框架。
換膚方式一:切換使用主題Theme
使用相同的資源id,但在不同的Theme下邊自定義不同的資源。我們通過主動(dòng)切換到不同的Theme從而切換界面元素創(chuàng)建時(shí)使用的資源。這種方案的代碼量不多發(fā),而且有個(gè)很明顯的缺點(diǎn)不支持已經(jīng)創(chuàng)建界面的換膚,必須重新加載界面元素。 GitHub Demo
換膚方式二:加載資源包
加載資源包是各種應(yīng)用程序都在使用的換膚方法,例如我們最常用的輸入法皮膚、瀏覽器皮膚等等。我們可以將皮膚的資源文件放入安裝包內(nèi)部,也可以進(jìn)行下載緩存到磁盤上。Android的應(yīng)用程序可以使用這種方式進(jìn)行換膚。GitHub上面有一個(gè)start非常高的換膚框架 Android-Skin-Loader 就是通過加載資源包對(duì)app進(jìn)行換膚。對(duì)這個(gè)框架的分析這個(gè)也是這篇文章主要的講述內(nèi)容。
對(duì)比一下發(fā)現(xiàn)切換Theme可以進(jìn)行小幅度的換膚設(shè)置(比如某個(gè)自定義組件的主題),而如果我們想要對(duì)整個(gè)app做主題切換那么通過加載資源包的這種方式目前應(yīng)該說是比較好的了。
Android換膚知識(shí)點(diǎn)
換膚相應(yīng)的API
我們先來看一下Android提供的一些基本的api,通過使用這些api可以在App內(nèi)部進(jìn)行資源對(duì)象的替換。
- 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 {{
- /********部分代碼省略*******/
- }
- /********部分代碼省略*******/
- }
這個(gè)是我們常用的Resources類的api,我們通??梢允褂迷谫Y源文件中定義的 @+id String類型,然后在編譯出的R.java中對(duì)應(yīng)的資源文件生產(chǎn)的id(int類型),從而通過這個(gè)id(int類型)調(diào)用Resources提供的這些api獲取到對(duì)應(yīng)的資源對(duì)象。這個(gè)在同一個(gè)app下沒有任何問題,但是在皮膚包中我們?cè)趺传@取這個(gè)id值呢。
- public class Resources{
- /********部分代碼省略*******/
- /**
- * 通過給的資源名稱返回一個(gè)資源的標(biāo)識(shí)id。
- *@paramname 描述資源的名稱
- *@paramdefType 資源的類型
- *@paramdefPackage 包名
- *
- *@return返回資源id,0標(biāo)識(shí)未找到該資源
- */
- 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這三個(gè)參數(shù)就可以在AssetManager中尋找相應(yīng)的PackageName中有沒有Type類型并且id值都能與參數(shù)對(duì)應(yīng)上的id,進(jìn)行返回。然后我們可以通過這個(gè)id再調(diào)用Resource的獲取資源的api就可以得到相應(yīng)的資源。
這里我們需要注意的一點(diǎn)是 getIdentifier(String name, String defType, String defPackage) 方法和 getString(int id) 方法所調(diào)用Resources對(duì)象的mAssets對(duì)象必須是同一個(gè),并且包含有PackageName這個(gè)資源包。
AssetManager構(gòu)造
怎么構(gòu)造一個(gè)包含特定packageName資源的AssetManager對(duì)象實(shí)例呢?
- public final class AssetManagerimplements 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 {@linkResources#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的構(gòu)造函數(shù)來看有 {@hide} 的朱姐,所以在其他類里面是直接創(chuàng)建AssetManager實(shí)例。但是不要忘記Java中還有反射機(jī)制可以創(chuàng)建類對(duì)象。
- AssetManager assetManager = AssetManager.class.newInstance();
讓創(chuàng)建的assetManager包含特定的PackageName的資源信息,怎么辦?我們?cè)贏ssetManager中找到相應(yīng)的api可以調(diào)用。
- public final class AssetManagerimplements 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;
- }
- }
- }
同樣改方法也不支持外部調(diào)用,我們只能通過反射的方法來調(diào)用。
- /**
- * 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();
- }
至此我們可以構(gòu)造屬于自己換膚的Resources了。
換膚Resources構(gòu)造
- 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());
- }
使用資源包中的資源換膚
我們將上述所有的代碼組合在一起就可以實(shí)現(xiàn),使用資源包中的資源對(duì)app進(jìn)行換膚。
- 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);
- /**
- * 插件資源對(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(drawable);
- textView.setText(text);
- }
通過上述介紹,我們可以簡(jiǎn)單的對(duì)當(dāng)前頁面進(jìn)行換膚了。但是想要做出一個(gè)一個(gè)成熟換膚框架那么僅僅這些還是不夠的,提高一下我們的思維高度,如果我們?cè)赩iew創(chuàng)建的時(shí)候就直接使用皮膚資源包中的資源文件,那么這無疑就使換膚更加的簡(jiǎn)單已維護(hù)。
LayoutInflater.Factory
看過我前一篇 遇見LayoutInflater&Factory 文章的這部分可以省略掉.
很幸運(yùn)Android給我們?cè)赩iew生產(chǎn)的時(shí)候做修改提供了法門。
- public abstract class LayoutInflater{
- /***部分代碼省略****/
- public interface Factory{
- public View onCreateView(String name, Context context, AttributeSet attrs);
- }
- public interface Factory2extends Factory{
- public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
- }
- /***部分代碼省略****/
- }
我們可以給當(dāng)前的頁面的Window對(duì)象在創(chuàng)建的時(shí)候設(shè)置Factory,那么在Window中的View進(jìn)行創(chuàng)建的時(shí)候就會(huì)先通過自己設(shè)置的Factory進(jìn)行創(chuàng)建。Factory使用方式和相關(guān)注意事項(xiàng)請(qǐng)移位到 遇見LayoutInflater&Factory ,關(guān)于Factory的相關(guān)知識(shí)點(diǎn)盡在其中。
Android-Skin-Loader解析
初始化
初始化換膚框架,導(dǎo)入需要換膚的資源包(當(dāng)前為一個(gè)apk文件,其中只有資源文件)。
- public class SkinApplicationextends Application{
- public void onCreate(){
- super.onCreate();
- initSkinLoader();
- }
- /**
- * Must call init first
- */
- private void initSkinLoader(){
- SkinManager.getInstance().init(this);
- SkinManager.getInstance().load();
- }
- }
構(gòu)造換膚對(duì)象
導(dǎo)入需要換膚的資源包,并構(gòu)造換膚的Resources實(shí)例。
- /**
- * Load resources from apk in asyc task
- *@paramskinPackagePath path of skin apk
- *@paramcallback 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();
- }
- };
- @Override
- protected 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();
- //檢索程序外的一個(gè)安裝包文件
- PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
- //獲取安裝包報(bào)名
- skinPackageName = mInfo.packageName;
- //構(gòu)建換膚的AssetManager實(shí)例
- AssetManager assetManager = AssetManager.class.newInstance();
- Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
- addAssetPath.invoke(assetManager, skinPkgPath);
- //構(gòu)建換膚的Resources實(shí)例
- Resources superRes = context.getResources();
- Resources skinResource = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
- //存儲(chǔ)當(dāng)前皮膚路徑
- 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);
- }
定義基類
換膚頁面的基類的通用代碼實(shí)現(xiàn)基本換膚功能。
- public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView{
- /***部分代碼省略****/
- //自定義LayoutInflater.Factory
- private SkinInflaterFactory mSkinInflaterFactory;
- @Override
- protected void onCreate(Bundle savedInstanceState){
- super.onCreate(savedInstanceState);
- try {
- //設(shè)置LayoutInflater的mFactorySet為true,表示還未設(shè)置mFactory,否則會(huì)拋出異常。
- Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
- field.setAccessible(true);
- 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();
- }
- }
- @Override
- protected void onResume(){
- super.onResume();
- //注冊(cè)皮膚管理對(duì)象
- SkinManager.getInstance().attach(this);
- }
- @Override
- protected void onDestroy(){
- super.onDestroy();
- //反注冊(cè)皮膚管理對(duì)象
- SkinManager.getInstance().detach(this);
- }
- /***部分代碼省略****/
- }
SkinInflaterFactory
- SkinInflaterFactory進(jìn)行View的創(chuàng)建并對(duì)View進(jìn)行換膚。
構(gòu)造View
- public class SkinInflaterFactoryimplements 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;
- }
- //創(chuàng)建View
- View view = createView(context, name, attrs);
- if (view == null){
- return null;
- }
- //如果View創(chuàng)建成功,對(duì)View進(jìn)行換膚
- parseSkinAttr(context, attrs, view);
- return view;
- }
- //創(chuàng)建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;
- }
- }
對(duì)生產(chǎn)的View進(jìn)行換膚
- public class SkinInflaterFactoryimplements Factory{
- //存儲(chǔ)當(dāng)前Activity中的需要換膚的View
- private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();
- /***部分代碼省略****/
- private void parseSkinAttr(Context context, AttributeSet attrs, View view){
- //當(dāng)前View的所有屬性標(biāo)簽
- 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屬性標(biāo)簽中屬性的value的值為引用類型
- if(attrValue.startsWith("@")){
- try {
- 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,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();
- }
- }
- }
- //如果當(dāng)前View需要換膚,那么添加在mSkinItems中
- if(!ListUtils.isEmpty(viewAttrs)){
- SkinItem skinItem = new SkinItem();
- skinItem.view = view;
- skinItem.attrs = viewAttrs;
- mSkinItems.add(skinItem);
- //是否是使用外部皮膚進(jìn)行換膚
- if(SkinManager.getInstance().isExternalSkin()){
- skinItem.apply();
- }
- }
- }
- }
資源獲取
通過當(dāng)前的資源id,找到對(duì)應(yīng)的資源name。再從皮膚包中找到該資源name所對(duì)應(yīng)的資源id。
- public class SkinManagerimplements ISkinLoader{
- /***部分代碼省略****/
- public int getColor(int resId){
- int originColor = context.getResources().getColor(resId);
- //是否沒有下載皮膚或者當(dāng)前使用默認(rèn)皮膚
- if(mResources == null || isDefaultSkin){
- return originColor;
- }
- //根據(jù)resId值獲取對(duì)應(yīng)的xml的的@+id的String類型的值
- String resName = context.getResources().getResourceEntryName(resId);
- //更具resName在皮膚包的mResources中獲取對(duì)應(yīng)的resId
- int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
- int trueColor = 0;
- try{
- //根據(jù)resId獲取對(duì)應(yīng)的資源value
- 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é)
換膚就是這么簡(jiǎn)單~!~!
文章到這里就全部講述完啦,若有其他需要交流的可以留言哦~!~!