2.3 Android 換膚原理

Android 換膚原理

  • 制作皮膚包,皮膚包相當(dāng)于一個apk,不過只包含了資源文件
  • 獲取到皮膚包的Resource對象
  • 標(biāo)記需要換膚的View
  • 切換時刷新頁面

換膚用的Api

1.通過的Resource獲取皮膚包中資源(一般是圖片,顏色)的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);
    }
}
2. AssetManage用于構(gòu)造獲取皮膚包的Resource對象

創(chuàng)建一個包含皮膚包packageName的AssetManage對象實例

AssetManager assetManager = AssetManager.class.newInstance();
/**
 * 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();
}
3. 創(chuàng)建獲取換膚包的Resource實例了:
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());
}
4. 使用皮膚包中的資源,對app進行換膚
@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);

}

Android-Skin-Loader 換膚原理

1.load皮膚包
/** 
 * 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);
}
2. 換膚頁面的基類,設(shè)置布局文件加載器LayoutInflate.Fatory

用于在加載布局文件創(chuàng)建View的時候,統(tǒng)計需要換膚的View對象

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);
    }
   
}
3. SkinInflaterFactory 自定義布局文件加載器

在某個界面初始化在加載布局文件中的View的時候,通過自定義的布局文件解析器,創(chuàng)建View。

這里有一個判斷,每次創(chuàng)建View的時候,會判斷改View的skin:enable屬性,如果為false,則不支持換膚,直接返回null,將View的創(chuàng)建的流程交給Activity自己創(chuàng)建。只有shin:enable屬性為true的時候,才會自己創(chuàng)建View對象,并且在創(chuàng)建的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;
    }
}
4. 創(chuàng)建了View之后,解析布局文件中View定義的換膚屬性

解析View的屬性,在某些屬性(比如background,)支持換膚的時候,會將解析到的屬性保存在一個SkinItem的對象中,每解析一個View都會生成一個SkinItem對象,然后保存在一個ArrayList<SkinItem> 集合中,在點擊切換按鈕的時候,會遍歷該List,替換資源。

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();
            }
        }
    }
}
5. 在進行換膚的時候,替換資源

通過當(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){...}
}

這樣整個換膚的流程就走完了

?著作權(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)容