話說(shuō)什么是動(dòng)態(tài)換膚?這里舉個(gè)例子:在APP中可以下載某一個(gè)皮膚包,然后應(yīng)用起來(lái)整個(gè)APP的界面就發(fā)生了改變匀归,諸如某些圖片约啊,文字字體,文字顏色等等另玖。
那么這種功能是怎么實(shí)現(xiàn)的呢休蟹?其實(shí)初步分析一把,應(yīng)該就是在應(yīng)用了皮膚包之后這些換膚了的控件的某些布局屬性發(fā)生了變化日矫,比如width赂弓、height、src哪轿、background盈魁、textsize、textcolor等窃诉。話說(shuō)回來(lái)杨耙,在沒(méi)有實(shí)現(xiàn)換膚功能之前我們的APP對(duì)控件進(jìn)行屬性指定一般都是寫(xiě)在屬性文件中,比如android:textColor="@color/textColorDefault"飘痛,我們會(huì)在專門的color.xml文件中定義這個(gè)顏色屬性的具體value值珊膜,那么我們換膚時(shí)就應(yīng)該是去替換color.xml文件中定義的textColorDefault這個(gè)屬性值。
現(xiàn)在開(kāi)始分析Android默認(rèn)是在什么時(shí)候開(kāi)始加載視圖組件的宣脉。我們應(yīng)該會(huì)聯(lián)想到Activity的onCreate()方法里面我們都要去調(diào)用setContentView(int id) 來(lái)指定當(dāng)前Activity的布局文件车柠,就像這樣:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
按照流程我們找到了這里:
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);//這里實(shí)現(xiàn)view布局的加載
mOriginalWindowCallback.onContentChanged();
}
LayoutInflater的功能我們?cè)趂ragment中應(yīng)該很熟悉了,多說(shuō)一句,在自定義viewGroup的時(shí)候我們也可以仿照這樣的寫(xiě)法對(duì)自定義viewGroup指定默認(rèn)的布局文件了竹祷。
好接下來(lái)我們順藤摸瓜來(lái)到了LayoutInflater.java里面看看inflate是怎么實(shí)現(xiàn)的:
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
final String name = parser.getName();
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
return temp;
}
可以看到inflate會(huì)返回具體的View對(duì)象出去谈跛,那么我們的關(guān)注焦點(diǎn)就放在createViewFromTag中了。
/**
* Creates a view from a tag name using the supplied attribute set.
* <p>
* <strong>Note:</strong> Default visibility so the BridgeInflater can
* override it.
*
* @param parent the parent view, used to inflate layout params
* @param name the name of the XML tag used to define the view
* @param context the inflation context for the view, typically the
* {@code parent} or base layout inflater context
* @param attrs the attribute set for the XML tag used to define the view
* @param ignoreThemeAttr {@code true} to ignore the {@code android:theme}
* attribute (if set) for the view being inflated,
* {@code false} otherwise
*/
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
try {
View view;
if (mFactory2 != null) {
view = mFactory2.onCreateView(parent, name, context, attrs);
} else if (mFactory != null) {
view = mFactory.onCreateView(name, context, attrs);
} else {
view = null;
}
return view;
} catch (Exception e) {
}
}
這里我們先看看這幾個(gè)參數(shù)的意義塑陵,name指的是在layout.xml中給出的名稱感憾,例如:
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="skinSelect"
android:text="個(gè)性換膚"/>
這里拿到的name值就是“Button”。再如:
<com.dongnao.dnskin.widget.MyTabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:tabIndicatorColor="@color/tabSelectedTextColor"
app:tabTextColor="@color/tab_selector"/>
這里拿到的name值就是“com.dongnao.dnskin.widget.MyTabLayout”令花,或者:
<android.support.v4.view.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
這里拿到的name值就是“android.support.v4.view.ViewPager”阻桅。
因此我們要明確一點(diǎn),參數(shù)name可能是View控件的java全路徑名稱兼都,也有可能不是嫂沉,比如第一種情況的Button,但是這種情況只會(huì)出現(xiàn)在系統(tǒng)已有控件里面俯抖,它們的包名我們是可以大膽猜測(cè)出來(lái)的输瓜,無(wú)非就是在這么幾個(gè)包下面:
private static final String[]mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
OK瓦胎,分析完name參數(shù)芬萍,我們?cè)诳纯碅ttributeSet是個(gè)什么梗,其實(shí)源碼注釋已經(jīng)寫(xiě)得很清楚了搔啊,就是xml文件中對(duì)這個(gè)View給出的屬性描述柬祠。
參數(shù)分析完了,我們看看方法體是怎么實(shí)現(xiàn)的负芋。會(huì)發(fā)現(xiàn)生成View的時(shí)候會(huì)優(yōu)先從mFactory2中的onCreateView里面去獲取View對(duì)象漫蛔,獲取到了就直接返回。所以我們是不是可以自己去實(shí)現(xiàn)這個(gè)mFactory2來(lái)代替系統(tǒng)生成View對(duì)象旧蛾?因?yàn)樯蒝iew對(duì)象的工作由我們自己來(lái)完成的話我們就可以很輕松的獲取attrs參數(shù)莽龟,并且根據(jù)attrs對(duì)象知道在layout.xml中對(duì)這個(gè)View做了哪些屬性描述,比如說(shuō)拿到了background=“@drawable/bg_01”锨天,當(dāng)需要換膚的時(shí)候毯盈,我們就可以去皮膚包里面找到“@drawable/bg_01”這個(gè)資源,用來(lái)給這個(gè)View替換上去View.setBackground(...)病袄,那么我們的換膚功能不就實(shí)現(xiàn)了嗎搂赋?答案也確實(shí)是這樣做的。
接下來(lái)把精力放在怎么實(shí)現(xiàn)mFactory2上面益缠,并且設(shè)置進(jìn)入LayoutInflater中脑奠。前面我們知道Activity的setContentView()回去調(diào)用LayoutInflater.from(Context)拿到 LayoutInflater對(duì)象,代碼如下:
/**
* Obtains the LayoutInflater from the given context.
*/
public static LayoutInflater from(Context context) {
LayoutInflater LayoutInflater =
(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
if (LayoutInflater == null) {
throw new AssertionError("LayoutInflater not found.");
}
return LayoutInflater;
}
通過(guò)源碼的注釋也可以看到每一個(gè)Activity會(huì)有自己的LayoutInflater對(duì)象幅慌,此外LayoutInflater還暴露了mFactory2的set方法提供給我們:
public void setFactory2(Factory2 factory) {
if (mFactorySet) {
throw new IllegalStateException("A factory has already been set on this LayoutInflater");
}
if (factory == null) {
throw new NullPointerException("Given factory can not be null");
}
mFactorySet = true;
if (mFactory == null) {
mFactory = mFactory2 = factory;
} else {
mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
}
}
因此宋欺,這個(gè)setter就可以在每個(gè)Activity的onCreate之前進(jìn)行調(diào)用,達(dá)到我們想要的目的。到了這里迄靠,相信我們會(huì)想到使用ActivityLifecycleCallbacks回調(diào)來(lái)監(jiān)聽(tīng)Activity的各個(gè)生命周期回調(diào)秒咨,在onActivityCreated()進(jìn)行mFactory2的初始化并且調(diào)用setter。
接下來(lái)掌挚,我們定義一個(gè)單例類SkinManager.java :
public class SkinManager extends Observable {
Application application;
private static SkinManager instance;
/**
* 客戶端程序在application的onCreate()后調(diào)用.
* @param application
*/
public static void init(Application application) {
synchronized (SkinManager.class) {
if (null == instance) {
instance = new SkinManager(application);
}
}
}
public static SkinManager getInstance() {
return instance;
}
private SkinManager(Application application) {
this.application = application;
application.registerActivityLifecycleCallbacks(new SkinActivityLifecycleCallbacks());
}
SkinActivityLifecycleCallbacks.java如下:
public class SkinActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
Field field = LayoutInflater.class.getDeclaredField("mFactorySet");
field.setAccessible(true);
field.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
SkinLayoutFactory skinLayoutFactory = new SkinLayoutFactory(activity,typeface);
LayoutInflaterCompat.setFactory2(layoutInflater, skinLayoutFactory);
}
...
}
因?yàn)閟etFactory2()中有一個(gè)mFactorySet布爾類型的判斷雨席,我們使用了反射對(duì)mFactorySet置為true。
我們只需要在自定義application的onCreate()后面調(diào)用SkinManager.init()就完成了所有Activity的mFactory2設(shè)置吠式。
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SkinManager.init(this);
}
OK陡厘,到了這一步準(zhǔn)備工作就做完了,我們重心放入自定義Factory2的實(shí)現(xiàn)中來(lái)特占〔谥茫看看Factory2是個(gè)什么東西:
public interface Factory2 extends Factory {
/**
* Version of {@link #onCreateView(String, Context, AttributeSet)}
* that also supplies the parent that the view created view will be
* placed in.
*
* @param parent The parent that the created view will be placed
* in; <em>note that this may be null</em>.
* @param name Tag name to be inflated.
* @param context The context the view is being created in.
* @param attrs Inflation attributes as specified in XML file.
*
* @return View Newly created view. Return null for the default
* behavior.
*/
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
它是一個(gè)接口,聲明了一個(gè)創(chuàng)建View的函數(shù)等待實(shí)現(xiàn)類去實(shí)現(xiàn)是目。我們的實(shí)現(xiàn)類如下:
public class SkinLayoutFactory implements LayoutInflater.Factory2, Observer {
private static final HashMap<String, Constructor<? extends View>> sConstructorMap =
new HashMap<String, Constructor<? extends View>>();
private static final String[] mClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createViewFromTag(name, context, attrs);
return view;
}
private View createViewFromTag(String name, Context context, AttributeSet attrs) {
//包含了. 自定義控件
if (name.contains(".")) {
return createView(name,context,attrs);
}
for (String tag : mClassPrefixList) {
View v = createView(tag + name, context, attrs);
if (null == v)
continue;
return v;
}
return null;
}
private View createView(String name, Context context, AttributeSet attrs) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (null == constructor) {
try {
Class<? extends View> aClass = context.getClassLoader().loadClass(name).asSubclass(View.class);
constructor = aClass.getConstructor(Context.class, AttributeSet.class);
sConstructorMap.put(name, constructor);
} catch (Exception e) {
}
}
if (null != constructor) {
try {
return constructor.newInstance(context, attrs);
} catch (Exception e) {
}
}
return null;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
}
這里的實(shí)現(xiàn)其實(shí)也很簡(jiǎn)單谤饭,就是我們獲取View的java全名稱,然后通過(guò)反射機(jī)制獲取View的構(gòu)造方法進(jìn)行實(shí)例化再返回懊纳。當(dāng)然揉抵,我們這里還做了一個(gè)靜態(tài)的map來(lái)緩存View的構(gòu)造方法,可以優(yōu)化一定的性能嗤疯,畢竟反射多了總是不好的對(duì)吧(其實(shí)你仔細(xì)看了LayoutInflater的源碼冤今,它就是這么做的,我們這里借鑒一下)茂缚。
OK戏罢,按照前面給出的思路,我們自己構(gòu)建mFactory2脚囊,代替系統(tǒng)來(lái)創(chuàng)建View對(duì)象龟糕,接下來(lái)還差一個(gè)步驟,就是通過(guò)attrs參數(shù)知道這個(gè)View在xml中被哪些屬性描述了悔耘,我們需要一個(gè)機(jī)制來(lái)記錄這個(gè)View被描述過(guò)了的并且可能會(huì)被皮膚包替換資源的屬性名稱還有默認(rèn)的資源Id讲岁,在換膚時(shí)就去這些記錄里面查找View及它的換膚屬性的名稱和資源Id,拿到默認(rèn)資源Id后就可以知道這個(gè)資源的類型和名稱淮逊,比如@string/s_name催首、@color/co_default_bg,然后拿著資源類型和名稱去皮膚包中查找同類型同名稱的資源泄鹏,然后根據(jù)屬性名稱給這個(gè)View更改相應(yīng)的表現(xiàn)郎任。比如描述屬性名稱和資源類型名稱是textColor=“@color/default_tx_color”,在皮膚包中找到“@color/default_tx_color”這個(gè)資源备籽,給View.setTextColor(皮膚包中找到的資源)舶治,如果屬性名稱是background分井,那么就是View.setBackground(皮膚包中找到的資源),這樣就達(dá)到了換膚效果霉猛。
接下來(lái)就是怎么去實(shí)現(xiàn)這個(gè)記錄View及它的換膚屬性和資源名稱的機(jī)制了尺锚。
我們?cè)O(shè)計(jì)一套數(shù)據(jù)結(jié)構(gòu)來(lái)記錄這種關(guān)系。
其實(shí)可以在自定義Factory2返回View對(duì)象之前做這些工作惜浅,比如交給SkinAttribute對(duì)象去做瘫辩。SkinAttribute以及SkinView、SkinPair代碼如下:
public class SkinAttribute {
private static final List<String> mAttributes = new ArrayList<>();//支持換膚的屬性
static {
mAttributes.add("background");
mAttributes.add("src");
mAttributes.add("textColor");
mAttributes.add("drawableLeft");
mAttributes.add("drawableTop");
mAttributes.add("drawableRight");
mAttributes.add("drawableBottom");
}
private List<SkinView> skinViews = new ArrayList<>();
/**
* 篩選符合屬性的view
*
* @param view
* @param attrs
*/
public void load(View view, AttributeSet attrs) {
List<SkinPair> skinPairs = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//獲得屬性名
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
String attributeValue = attrs.getAttributeValue(i);
//寫(xiě)死了 不管了
if (attributeValue.startsWith("#")) {
continue;
}
//資源id
int resId = 0;
if (attributeValue.startsWith("?")) {//?開(kāi)頭, "?colorAccess" 對(duì)應(yīng)主題中的屬性名稱id
int attrId = Integer.parseInt(attributeValue.substring(1));//屬性id
//獲得主題style中對(duì)應(yīng)attr的資源id值
resId = SkinUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {//@開(kāi)頭 "@ID"
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
//可以替換的屬性
SkinPair skinPair = new SkinPair(attributeName, resId);
skinPairs.add(skinPair);
}
}
}
if (!skinPairs.isEmpty() || view instanceof TextView) {
SkinView skinView = new SkinView(view, skinPairs);
skinViews.add(skinView);
}
}
static class SkinView {
View view;
/**
* 當(dāng)前view支持換膚特性的屬性與id鍵值對(duì)列表
*/
List<SkinPair> skinPairs;
public SkinView(View view, List<SkinPair> skinPairs) {
this.view = view;
this.skinPairs = skinPairs;
}
}
static class SkinPair {
/**
* 屬性名稱,例如:background,src,textColor等
*/
String attributeName;
/**
* 資源ID值
*/
int resId;
public SkinPair(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
}
上述代碼中有一處需要清楚的是:String attributeValue = attrs.getAttributeValue() 返回的是一個(gè)字符串坛悉,例如textColor=“#ffffff”伐厌,那么attributeValue = “#ffffff”,textColor=“@color/default_color”裸影,那么attributeValue = “@12345678”挣轨,這里的“12345678”指的就是“@color/default_color”對(duì)應(yīng)的資源ID,類似的還有“@drawable/default_bg”等等轩猩。當(dāng)然卷扮,還有一種情況就是textColor=“?colorAccess”,雖然程序最終引用的資源是style.xml中定義的屬性值“colorAccent”指向的“@color/colorAccent”均践,
<style name="BaseTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorAccent">@color/colorAccent</item>
</style>
這個(gè)時(shí)候attributeValue = “?12121212”晤锹,但是“12121212”并不是“@color/colorAccent”的資源ID,而是style屬性“colorAccent”代表的ID浊猾,因此對(duì)待“?12121212”這樣的情況我們還需要再去style.xml中去查找真正引用的資源ID抖甘。具體做法如下:
public class SkinUtils {
public static int[] getResId(Context context, int[] attrs) {
int[] resIds = new int[attrs.length];
TypedArray typedArray = context.obtainStyledAttributes(attrs);
for (int i = 0; i < typedArray.length(); i++) {
resIds[i] = typedArray.getResourceId(i, 0);
}
typedArray.recycle();
return resIds;
}
}
到了這里热鞍,相信你也就知道了SkinLayoutFactory該怎么改造了:
public class SkinLayoutFactory implements LayoutInflater.Factory2{
......
private SkinAttribute skinAttribute;
public SkinLayoutFactory() {
skinAttribute = new SkinAttribute();
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
View view = createViewFromTag(name, context, attrs);
skinAttribute.load(view,attrs);//在返回view之前維護(hù)我們需要的 屬性-資源 關(guān)系數(shù)據(jù)結(jié)構(gòu)
return view;
}
......
}
OK葫慎,現(xiàn)在需要換膚控件的資源信息也采集到了,接下來(lái)就是怎么去實(shí)現(xiàn)換膚了薇宠。
換膚之前我們要清楚什么是皮膚包偷办,又該怎么把皮膚包加載到系統(tǒng)里面供我們獲取資源并使用。
皮膚包其實(shí)就是一個(gè)apk文件澄港,只不過(guò)內(nèi)部只包含資源文件椒涯,我們的皮膚包目錄結(jié)構(gòu)如下:
接下來(lái),我們?cè)撊绾伟岩粋€(gè)皮膚包加載進(jìn)項(xiàng)目中回梧,并且根據(jù)資源類型和名稱來(lái)獲取指定資源的Id呢废岂?
首先是將皮膚包加載進(jìn)入項(xiàng)目,我們會(huì)用到AssetManager這個(gè)工具:
/**
* 加載皮膚包 并 立即通知觀察者更新
*
* @param path 皮膚包路徑
*/
public void loadSkin(String path) {
try {
AssetManager assetManager = AssetManager.class.newInstance();
// 添加資源進(jìn)入資源管理器
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String
.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, path);
//app默認(rèn)資源
Resources resources = application.getResources();
//皮膚包資源
Resources skinResource = new Resources(assetManager, resources.getDisplayMetrics(),
resources.getConfiguration());
//獲取外部Apk(皮膚包) 包名
PackageManager mPm = application.getPackageManager();
PackageInfo info = mPm.getPackageArchiveInfo(path, PackageManager
.GET_ACTIVITIES);
String packageName = info.packageName;
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述代碼我們傳入皮膚包的路徑狱意,通過(guò)AssetManager獲取到Resource對(duì)象湖苞,其實(shí)到這一步就已經(jīng)將皮膚包的資源文件加載進(jìn)來(lái)了。
那么加載到了皮膚包的Resource對(duì)象详囤,我們?cè)撊绾瓮ㄟ^(guò)APP程序默認(rèn)的一個(gè)資源id去拿到在皮膚包中同類型同名稱的這個(gè)資源的id呢财骨?
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮膚包中不一定就是 當(dāng)前程序的 id
//獲取對(duì)應(yīng)id 在當(dāng)前的名稱 colorPrimary
//R.drawable.ic_launcher
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
String resType = mAppResources.getResourceTypeName(resId);//drawable
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
return skinId;
}
這個(gè)方法其實(shí)就是將默認(rèn)資源id轉(zhuǎn)化成皮膚包中對(duì)應(yīng)資源的id,獲取到了id我們就可以通過(guò)Resources.getXXX(int id)來(lái)拿到想要的資源了。
到了這個(gè)時(shí)候隆箩,相信我們都有了一個(gè)完整的換膚思路了:
①重寫(xiě)Factory2该贾,代替系統(tǒng)創(chuàng)建View對(duì)象,在這期間記錄下 需要換膚控件的 屬性名-資源ID 的集合捌臊。
②通過(guò)AssetManager加載外部的皮膚包資源Resource杨蛋,通過(guò)默認(rèn)的資源ID找到在皮膚包中對(duì)應(yīng)的資源ID,通過(guò)屬性名稱去動(dòng)態(tài)修改View的具體表現(xiàn)理澎。
③開(kāi)始換膚時(shí)六荒,我們可以使用觀察者模式來(lái)通知所有還未銷毀的Activity持有的 SkinLayoutFactory(作為觀察者),讓SkinLayoutFactory去遍歷其下面的所有 SkinView來(lái)完成應(yīng)用換膚資源的工作矾端。
/**
* 對(duì)當(dāng)前view進(jìn)行支持換膚的屬性進(jìn)行配置,應(yīng)用原生或者皮膚包的資源.
* @param typeface
*/
public void applySkin() {
for (SkinPair skinPair : skinPairs) {
Drawable left = null, top = null, right = null, bottom = null;
switch (skinPair.attributeName) {
case "background":
Object background = SkinResources.getInstance().getBackground(skinPair
.resId);
//Color
if (background instanceof Integer) {
view.setBackgroundColor((Integer) background);
} else {
ViewCompat.setBackground(view, (Drawable) background);
}
break;
case "src":
background = SkinResources.getInstance().getBackground(skinPair
.resId);
if (background instanceof Integer) {
((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
background));
} else {
((ImageView) view).setImageDrawable((Drawable) background);
}
break;
case "textColor":
((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList
(skinPair.resId));
break;
case "drawableLeft":
left = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableTop":
top = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableRight":
right = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
case "drawableBottom":
bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
}
}
上述代碼清晰的展示了通過(guò)屬性名稱來(lái)做出不同View展示調(diào)整的邏輯掏击。
我們?cè)賮?lái)看下SkinResources.java的代碼:
public class SkinResources {
private static SkinResources instance;
private Resources mSkinResources;
private String mSkinPkgName;
private boolean isDefaultSkin = true;
private Resources mAppResources;
private SkinResources(Context context) {
mAppResources = context.getResources();
}
public static void init(Context context) {
if (instance == null) {
synchronized (SkinResources.class) {
if (instance == null) {
instance = new SkinResources(context);
}
}
}
}
public static SkinResources getInstance() {
return instance;
}
public void reset() {
mSkinResources = null;
mSkinPkgName = "";
isDefaultSkin = true;
}
public void applySkin(Resources resources, String pkgName) {
mSkinResources = resources;
mSkinPkgName = pkgName;
//是否使用默認(rèn)皮膚
isDefaultSkin = TextUtils.isEmpty(pkgName) || resources == null;
}
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮膚包中不一定就是 當(dāng)前程序的 id
//獲取對(duì)應(yīng)id 在當(dāng)前的名稱 colorPrimary
//R.drawable.ic_launcher
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
String resType = mAppResources.getResourceTypeName(resId);//drawable
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
return skinId;
}
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
//如果有皮膚 isDefaultSkin false 沒(méi)有就是true
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
/**
* 可能是Color 也可能是drawable
*
* @return
*/
public Object getBackground(int resId) {
String resourceTypeName = mAppResources.getResourceTypeName(resId);
if (resourceTypeName.equals("color")) {
return getColor(resId);
} else {
// drawable
return getDrawable(resId);
}
}
public String getString(int resId) {
try {
if (isDefaultSkin) {
return mAppResources.getString(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getString(skinId);
}
return mSkinResources.getString(skinId);
} catch (Resources.NotFoundException e) {
}
return null;
}
}
OK,整個(gè)換膚原理基本就講完了秩铆,當(dāng)然還有字體的動(dòng)態(tài)全局及單個(gè)切換砚亭,自定義view的自定義屬性切換,某些控件加載時(shí)序問(wèn)題導(dǎo)致無(wú)法換膚等問(wèn)題殴玛,后面繼續(xù)補(bǔ)充捅膘。
附上源碼地址
鏈接:https://share.weiyun.com/51Q5YxV