Android換膚原理和Android-Skin-Loader框架解析

換皮膚啦

前言

Android換膚技術(shù)已經(jīng)是很久之前就已經(jīng)被成熟使用的技術(shù)了,然而我最近才在學(xué)習(xí)和接觸熱修復(fù)的時候才看到。在看了一些換膚的方法之后,并且對市面上比較認可的Android-Skin-Loader換膚框架的源碼進行了分析總結(jié)。再次記錄一下祭奠自己逝去的時間。

換膚介紹

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

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

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

換膚方式二:加載資源包

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

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

Android換膚知識點

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

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

這里我們需要注意的一點是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 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的構(gòu)造函數(shù)來看有{@hide}的朱姐,所以在其他類里面是直接創(chuàng)建AssetManager實例。但是不要忘記Java中還有反射機制可以創(chuàng)建類對象。

AssetManager assetManager = AssetManager.class.newInstance();

讓創(chuàng)建的assetManager包含特定的PackageName的資源信息,怎么辦?我們在AssetManager中找到相應(yīng)的api可以調(diào)用。

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;
        }
    }
}

同樣改方法也不支持外部調(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"));
    /**
     * 獲取Color資源
     */
    int color = resources.getColor(resources.getIdentifier("night_color","color","com.tzx.skin"));

    imageView.setImageDrawable(drawable);
    textView.setText(text);

}

通過上述介紹,我們可以簡單的對當(dāng)前頁面進行換膚了。但是想要做出一個一個成熟換膚框架那么僅僅這些還是不夠的,提高一下我們的思維高度,如果我們在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 Factory2 extends Factory {
        public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
    }
    /***部分代碼省略****/
}

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

Android-Skin-Loader解析

初始化

  • 初始化換膚框架,導(dǎo)入需要換膚的資源包(當(dāng)前為一個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();
    }
}

構(gòu)造換膚對象

  • 導(dǎo)入需要換膚的資源包,并構(gòu)造換膚的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();
            }
        };

        @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());
                    //存儲當(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);
}

定義基類

  • 換膚頁面的基類的通用代碼實現(xiàn)基本換膚功能。
public class BaseFragmentActivity extends FragmentActivity implements ISkinUpdate, IDynamicNewView{
    
    /***部分代碼省略****/
    
    //自定義LayoutInflater.Factory
    private SkinInflaterFactory mSkinInflaterFactory;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        try {
            //設(shè)置LayoutInflater的mFactorySet為true,表示還未設(shè)置mFactory,否則會拋出異常。
            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();
        //注冊皮膚管理對象
        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 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;
        }
        //創(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 SkinInflaterFactory implements Factory {
    //存儲當(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實例,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);
            //是否是使用外部皮膚進行換膚
            if(SkinManager.getInstance().isExternalSkin()){
                skinItem.apply();
            }
        }
    }
}

資源獲取

通過當(dāng)前的資源id,找到對應(yīng)的資源name。再從皮膚包中找到該資源name所對應(yīng)的資源id。

public class SkinManager implements ISkinLoader{
    /***部分代碼省略****/
    public int getColor(int resId){
        int originColor = context.getResources().getColor(resId);
        //是否沒有下載皮膚或者當(dāng)前使用默認皮膚
        if(mResources == null || isDefaultSkin){
            return originColor;
        }
        //根據(jù)resId值獲取對應(yīng)的xml的的@+id的String類型的值
        String resName = context.getResources().getResourceEntryName(resId);
        //更具resName在皮膚包的mResources中獲取對應(yīng)的resId
        int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
        int trueColor = 0;
        try{
            //根據(jù)resId獲取對應(yīng)的資源value
            trueColor = mResources.getColor(trueResId);
        }catch(NotFoundException e){
            e.printStackTrace();
            trueColor = originColor;
        }
        
        return trueColor;
    }
    public Drawable getDrawable(int resId){...}
}

其他

除此之外再增加以下對于皮膚的管理api(下載、監(jiān)聽回調(diào)、應(yīng)用、取消、異常處理、擴展模塊等等)。

總結(jié)

換膚就是這么簡單!!~!

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容