溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android換膚的原理是什么

發(fā)布時間:2021-06-28 16:04:23 來源:億速云 閱讀:235 作者:Leah 欄目:移動開發(fā)

Android換膚的原理是什么,針對這個問題,這篇文章詳細介紹了相對應的分析和解答,希望可以幫助更多想解決這個問題的小伙伴找到更簡單易行的方法。

換膚介紹

換膚本質(zhì)上是對資源的一中替換包括、字體、顏色、背景、圖片、大小等等。當然這些我們都有成熟的api可以通過控制代碼邏輯做到。比如View的修改背景顏色  setBackgroundColor ,TextView的 setTextSize  修改字體等等。但是作為程序員我們怎么能忍受對每個頁面的每個元素一個行行代碼做換膚處理呢?我們需要用最少的代碼實現(xiàn)最容易維護和使用效果***(動態(tài)切換,及時生效)的換膚框架。

換膚方式一:切換使用主題Theme

使用相同的資源id,但在不同的Theme下邊自定義不同的資源。我們通過主動切換到不同的Theme從而切換界面元素創(chuàng)建時使用的資源。這種方案的代碼量不多發(fā),而且有個很明顯的缺點不支持已經(jīng)創(chuàng)建界面的換膚,必須重新加載界面元素。  GitHub Demo

換膚方式二:加載資源包

加載資源包是各種應用程序都在使用的換膚方法,例如我們最常用的輸入法皮膚、瀏覽器皮膚等等。我們可以將皮膚的資源文件放入安裝包內(nèi)部,也可以進行下載緩存到磁盤上。Android的應用程序可以使用這種方式進行換膚。GitHub上面有一個start非常高的換膚框架  Android-Skin-Loader 就是通過加載資源包對app進行換膚。對這個框架的分析這個也是這篇文章主要的講述內(nèi)容。

對比一下發(fā)現(xiàn)切換Theme可以進行小幅度的換膚設置(比如某個自定義組件的主題),而如果我們想要對整個app做主題切換那么通過加載資源包的這種方式目前應該說是比較好的了。

Android換膚知識點

換膚相應的API

我們先來看一下Android提供的一些基本的api,通過使用這些api可以在App內(nèi)部進行資源對象的替換。

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,我們通??梢允褂迷谫Y源文件中定義的 @+id  String類型,然后在編譯出的R.java中對應的資源文件生產(chǎn)的id(int類型),從而通過這個id(int類型)調(diào)用Resources提供的這些api獲取到對應的資源對象。這個在同一個app下沒有任何問題,但是在皮膚包中我們怎么獲取這個id值呢。

public class Resources{     /********部分代碼省略*******/     /** * 通過給的資源名稱返回一個資源的標識id。 *@paramname 描述資源的名稱 *@paramdefType 資源的類型 *@paramdefPackage 包名 * *@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這三個參數(shù)就可以在AssetManager中尋找相應的PackageName中有沒有Type類型并且id值都能與參數(shù)對應上的id,進行返回。然后我們可以通過這個id再調(diào)用Resource的獲取資源的api就可以得到相應的資源。

這里我們需要注意的一點是 getIdentifier(String name, String defType, String defPackage)  方法和 getString(int id)  方法所調(diào)用Resources對象的mAssets對象必須是同一個,并且包含有PackageName這個資源包。

AssetManager構(gòu)造

怎么構(gòu)造一個包含特定packageName資源的AssetManager對象實例呢?

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實例。但是不要忘記Java中還有反射機制可以創(chuàng)建類對象。

AssetManager assetManager = AssetManager.class.newInstance();

讓創(chuàng)建的assetManager包含特定的PackageName的資源信息,怎么辦?我們在AssetManager中找到相應的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()); }

使用資源包中的資源換膚

我們將上述所有的代碼組合在一起就可以實現(xiàn),使用資源包中的資源對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"));     /** * 獲取文本資源 */     int color = resources.getColor(resources.getIdentifier("night_color","color","com.tzx.skin"));      imageView.setImageDrawable(drawable);     textView.setText(text);  }

通過上述介紹,我們可以簡單的對當前頁面進行換膚了。但是想要做出一個一個成熟換膚框架那么僅僅這些還是不夠的,提高一下我們的思維高度,如果我們在View創(chuàng)建的時候就直接使用皮膚資源包中的資源文件,那么這無疑就使換膚更加的簡單已維護。

LayoutInflater.Factory

看過我前一篇 遇見LayoutInflater&Factory 文章的這部分可以省略掉.

很幸運Android給我們在View生產(chǎn)的時候做修改提供了法門。

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);     }     /***部分代碼省略****/ }

我們可以給當前的頁面的Window對象在創(chuàng)建的時候設置Factory,那么在Window中的View進行創(chuàng)建的時候就會先通過自己設置的Factory進行創(chuàng)建。Factory使用方式和相關(guān)注意事項請移位到  遇見LayoutInflater&Factory ,關(guān)于Factory的相關(guān)知識點盡在其中。

Android-Skin-Loader解析

初始化

初始化換膚框架,導入需要換膚的資源包(當前為一個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)造換膚對象

導入需要換膚的資源包,并構(gòu)造換膚的Resources實例。

/** * 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();                     //檢索程序外的一個安裝包文件                     PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);                     //獲取安裝包報名                     skinPackageName = mInfo.packageName;                     //構(gòu)建換膚的AssetManager實例                     AssetManager assetManager = AssetManager.class.newInstance();                     Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);                     addAssetPath.invoke(assetManager, skinPkgPath);                     //構(gòu)建換膚的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); }

定義基類

換膚頁面的基類的通用代碼實現(xiàn)基本換膚功能。

public class BaseFragmentActivityextends FragmentActivityimplements ISkinUpdate,IDynamicNewView{          /***部分代碼省略****/          //自定義LayoutInflater.Factory     private SkinInflaterFactory mSkinInflaterFactory;          @Override     protected 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的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();         //注冊皮膚管理對象         SkinManager.getInstance().attach(this);     }          @Override     protected void onDestroy(){         super.onDestroy();         //反注冊皮膚管理對象         SkinManager.getInstance().detach(this);     }     /***部分代碼省略****/ }

SkinInflaterFactory

  • SkinInflaterFactory進行View的創(chuàng)建并對View進行換膚。

構(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)建成功,對View進行換膚         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;     } }

對生產(chǎn)的View進行換膚

public class SkinInflaterFactoryimplements Factory{     //存儲當前Activity中的需要換膚的View     private 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);                     //構(gòu)造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 SkinManagerimplements ISkinLoader{     /***部分代碼省略****/     public int getColor(int resId){         int originColor = context.getResources().getColor(resId);         //是否沒有下載皮膚或者當前使用默認皮膚         if(mResources == null || isDefaultSkin){             return originColor;         }         //根據(jù)resId值獲取對應的xml的的@+id的String類型的值         String resName = context.getResources().getResourceEntryName(resId);         //更具resName在皮膚包的mResources中獲取對應的resId         int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);         int trueColor = 0;         try{             //根據(jù)resId獲取對應的資源value             trueColor = mResources.getColor(trueResId);         }catch(NotFoundException e){             e.printStackTrace();             trueColor = originColor;         }                  return trueColor;     }     public Drawable getDrawable(int resId){...} }

關(guān)于Android換膚的原理是什么問題的解答就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,如果你還有很多疑惑沒有解開,可以關(guān)注億速云行業(yè)資訊頻道了解更多相關(guān)知識。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI