根據(jù)上文中所分析的思路,我們來具體實現(xiàn):
- 首先铛绰,創(chuàng)建我們的activity逸雹,并且重寫
Factory2
的方法挽鞠。 - 然后,自定義我們的
CustomAppcompatInflater
繼承AppcompatInflater
乡翅。本來我們應(yīng)該接下來重寫他的onCreate()
方法的星澳,但是他是final疚顷,所以我們就自定義一個方法,功能和它一致就可以了禁偎,因為最后我們需要的是創(chuàng)建具體的控件的子類腿堤,所以這對我們沒什么影響。 - 最后如暖,創(chuàng)建我們具體需要改變顏色的控件
CustomButton
繼承MaterialButton
笆檀,上文也說了,一定注意繼承的是該控件的最終子類盒至,否者不會支持MaterialButton
酗洒。 - 該創(chuàng)建類就三個,有這三個類我們就可以實現(xiàn)加載布局過程中攔截
Button
的屬性進行自定義操作枷遂。 - 接下來我們仿照系統(tǒng)兼容包處理方法樱衷,在
acitivity
的重寫Factory2
方法中創(chuàng)建自定義的CustomAppcompatInflater
對象,然后返回自定義的方法酒唉,創(chuàng)建view
矩桂,自定義方法中拿著獲取到的控件name
和attributes
創(chuàng)建出我們的自定義控件,接下來上代碼:
attrs.xml
<!-- Button控件繼承TextView痪伦,此處parent語法通過侄榴,但無效果雹锣,不像style.xml -->
<declare-styleable name="CustomButton">
<attr name="android:background" />
<attr name="android:textColor" />
<!-- 字體屬性 -->
</declare-styleable>
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if (openChangeSkin() && !ignoreView(name)) {
if (viewInflater == null) {
viewInflater = new CustomAppCompatViewInflater(context);
}
viewInflater.setName(name);
viewInflater.setAttrs(attrs);
return viewInflater.autoMatch();
}
return super.onCreateView(parent, name, context, attrs);
}
/**
* 自定義控件加載器(可以考慮該類不被繼承)
*/
public final class CustomAppCompatViewInflater extends AppCompatViewInflater {
private String name; // 控件名
private Context context; // 上下文
private AttributeSet attrs; // 某控件對應(yīng)所有屬性
public CustomAppCompatViewInflater(@NonNull Context context) {
this.context = context;
}
public void setName(String name) {
this.name = name;
}
public void setAttrs(AttributeSet attrs) {
this.attrs = attrs;
}
/**
* @return 自動匹配控件名,并初始化控件對象
*/
public View autoMatch() {
View view = null;
switch (name) {
case BUTTON:
view = new CustomButton(context, attrs);
this.verifyNotNull(view, name);
break;
}
return view;
}
/**
* 校驗控件不為空(源碼方法癞蚕,由于private修飾蕊爵,只能復(fù)制過來了。為了代碼健壯桦山,可有可無)
*
* @param view 被校驗控件攒射,如:AppCompatTextView extends TextView(v7兼容包,兼容是重點:闼4衣ā!)
* @param name 控件名寇窑,如:"ImageView"
*/
private void verifyNotNull(View view, String name) {
if (view == null) {
throw new IllegalStateException(this.getClass().getName() + " asked to inflate view for <" + name + ">, but returned null");
}
}
}
/**
* 繼承TextView兼容包,9.0源碼中也是如此
* 參考:AppCompatViewInflater.java
* 86行 + 138行 + 206行
*/
public class CustomButton extends MaterialButton implements ViewsMatch {
private AttrsBean attrsBean;
public CustomButton(Context context) {
this(context, null);
}
public CustomButton(Context context, AttributeSet attrs) {
this(context, attrs, R.attr.buttonStyle);
}
public CustomButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
attrsBean = new AttrsBean();
// 根據(jù)自定義屬性箩张,匹配控件屬性的類型集合甩骏,如:background + textColor
TypedArray typedArray = context.obtainStyledAttributes(attrs,
R.styleable.CustomButton,
defStyleAttr, 0);
// 存儲到臨時JavaBean對象
attrsBean.saveViewResource(typedArray, R.styleable.CustomButton);
// 這一句回收非常重要!obtainStyledAttributes()有語法提示O瓤丁饮笛!
typedArray.recycle();
}
@Override
public void skinnableView() {
// 根據(jù)自定義屬性,獲取styleable中的background屬性
int key = R.styleable.CustomButton[R.styleable.CustomButton_android_background];
// 根據(jù)自定義屬性论熙,獲取styleable中的textColor屬性
key = R.styleable.CustomButton[R.styleable.CustomButton_android_textColor];
int textColorResourceId = attrsBean.getViewResource(key);
if (textColorResourceId > 0) {
if (SkinManager.getInstance().isDefaultSkin()) {
ColorStateList color = ContextCompat.getColorStateList(getContext(), textColorResourceId);
setTextColor(color);
} else {
ColorStateList color = SkinManager.getInstance().getColorStateList(textColorResourceId);
setTextColor(color);
}
}
}
}
以上代碼福青,在每次主題切換的時候遍歷控件調(diào)用skinnableView()方法,該方法中判斷是否存在主題資源文件脓诡,根據(jù)資源id獲取宿主或者主題包里面的對應(yīng)顏色值設(shè)置顏色无午。
加載主題包
直接上代碼,這個個人覺得沒太大必要細講祝谚,一看就能懂宪迟,而且代碼都注釋很清楚,值得注意的是里面三個位置的TODO注釋:
public class SkinManager {
private static SkinManager instance;
private Application application;
private Resources appResources; // 用于加載app內(nèi)置資源
private Resources skinResources; // 用于加載皮膚包資源
private String skinPackageName = ""; // 皮膚包資源所在包名(注:皮膚包不在app內(nèi)交惯,也不限包名)
private boolean isDefaultSkin = true; // 應(yīng)用默認皮膚(app內(nèi)置)
private static final String ADD_ASSET_PATH = "addAssetPath"; // 方法名
private Map<String, SkinCache> cacheSkin;
private SkinManager(Application application) {
this.application = application;
appResources = application.getResources();
cacheSkin = new HashMap<>();
}
/**
* 單例方法次泽,目的是初始化app內(nèi)置資源(越早越好,用戶的操作可能是:換膚后的第2次冷啟動)
*/
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}
public static SkinManager getInstance() {
return instance;
}
/**
* 加載皮膚包資源
*
* @param skinPath 皮膚包路徑席爽,為空則加載app內(nèi)置資源
*/
public void loaderSkinResources(String skinPath) {
// 優(yōu)化:如果沒有皮膚包或者沒做換膚動作意荤,方法不執(zhí)行直接返回!
if (TextUtils.isEmpty(skinPath)) {
isDefaultSkin = true;
return;
}
// 優(yōu)化:app冷啟動只锻、熱啟動可以取緩存對象
if (cacheSkin.containsKey(skinPath)) {
isDefaultSkin = false;
SkinCache skinCache = cacheSkin.get(skinPath);
if (null != skinCache) {
skinResources = skinCache.getSkinResources();
skinPackageName = skinCache.getSkinPackageName();
return;
}
}
try {
// 創(chuàng)建資源管理器(此處不能用:application.getAssets())
AssetManager assetManager = AssetManager.class.newInstance();
// 由于AssetManager中的addAssetPath和setApkAssets方法都被@hide玖像,目前只能通過反射去執(zhí)行方法
Method addAssetPath = assetManager.getClass().getDeclaredMethod(ADD_ASSET_PATH, String.class);
// 設(shè)置私有方法可訪問
addAssetPath.setAccessible(true);
// 執(zhí)行addAssetPath方法
addAssetPath.invoke(assetManager, skinPath);
//==============================================================================
// 如果還是擔心@hide限制,可以反射addAssetPathInternal()方法炬藤,參考源碼366行 + 387行
//==============================================================================
// 創(chuàng)建加載外部的皮膚包(net163.skin)文件Resources(注:依然是本應(yīng)用加載)
skinResources = new Resources(assetManager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
// 根據(jù)apk文件路徑(皮膚包也是apk文件)御铃,獲取該應(yīng)用的包名碴里。兼容5.0 - 9.0(親測)
skinPackageName = application.getPackageManager().getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES).packageName;
// 無法獲取皮膚包應(yīng)用的包名,則加載app內(nèi)置資源
isDefaultSkin = TextUtils.isEmpty(skinPackageName);
if (!isDefaultSkin) {
cacheSkin.put(skinPath, new SkinCache(skinResources, skinPackageName));
}
Log.e("skinPackageName >>> ", skinPackageName);
} catch (Exception e) {
e.printStackTrace();
// 發(fā)生異常上真,預(yù)判:通過skinPath獲取skinPacakageName失斠б浮!
isDefaultSkin = true;
}
}
/**
* 參考:resources.arsc資源映射表
* 通過ID值獲取資源 Name 和 Type
*
* @param resourceId 資源ID值
* @return 如果沒有皮膚包則加載app內(nèi)置資源ID睡互,反之加載皮膚包指定資源ID
*/
private int getSkinResourceIds(int resourceId) {
// 優(yōu)化:如果沒有皮膚包或者沒做換膚動作根竿,直接返回app內(nèi)置資源!
if (isDefaultSkin) return resourceId;
// 使用app內(nèi)置資源加載就珠,是因為內(nèi)置資源與皮膚包資源一一對應(yīng)(“netease_bg”, “drawable”)
String resourceName = appResources.getResourceEntryName(resourceId);
String resourceType = appResources.getResourceTypeName(resourceId);
// 動態(tài)獲取皮膚包內(nèi)的指定資源ID
// getResources().getIdentifier(“netease_bg”, “drawable”, “com.netease.skin.packages”);
int skinResourceId = skinResources.getIdentifier(resourceName, resourceType, skinPackageName);
// 源碼1924行:(0 is not a valid resource ID.)
// TODO: 2020/8/5 此處有問題,當我主題包中沒有該資源的時候會導(dǎo)致isDefault變成true寇壳,這就導(dǎo)致了遍歷view的時候無法改變主題,然而這里skinResourceId == 0是對每一個資源的判斷不能代表整個資源包
// isDefaultSkin = skinResourceId == 0;
// TODO: 2020/8/5 此處直接返回資源包獲取的id值妻怎,交給下一步操作判斷當前id應(yīng)該是獲取的宿主的還是資源包的
return skinResourceId;
}
public boolean isDefaultSkin() {
return isDefaultSkin;
}
//==============================================================================================
// TODO: 2020/8/5 此處根據(jù)獲取的資源包id來判斷是該加載宿主還是資源包
public int getColor(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return (ids == 0 || ids == resourceId) ? appResources.getColor(resourceId) : skinResources.getColor(ids);
}
public ColorStateList getColorStateList(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return (ids == 0 || ids == resourceId) ? appResources.getColorStateList(resourceId) : skinResources.getColorStateList(ids);
}
// mipmap和drawable統(tǒng)一用法(待測)
public Drawable getDrawableOrMipMap(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return (ids == 0 || ids == resourceId) ? appResources.getDrawable(resourceId) : skinResources.getDrawable(ids);
}
public String getString(int resourceId) {
int ids = getSkinResourceIds(resourceId);
return (ids == 0 || ids == resourceId) ? appResources.getString(resourceId) : skinResources.getString(ids);
}
// 返回值特殊情況:可能是color / drawable / mipmap
public Object getBackgroundOrSrc(int resourceId) {
// 需要獲取當前屬性的類型名Resources.getResourceTypeName(resourceId)再判斷
String resourceTypeName = appResources.getResourceTypeName(resourceId);
switch (resourceTypeName) {
case "color":
return getColor(resourceId);
case "mipmap": // drawable / mipmap
case "drawable":
return getDrawableOrMipMap(resourceId);
}
return null;
}
// 獲得字體
public Typeface getTypeface(int resourceId) {
// 通過資源ID獲取資源path壳炎,參考:resources.arsc資源映射表
String skinTypefacePath = getString(resourceId);
// 路徑為空,使用系統(tǒng)默認字體
if (TextUtils.isEmpty(skinTypefacePath)) return Typeface.DEFAULT;
return isDefaultSkin ? Typeface.createFromAsset(appResources.getAssets(), skinTypefacePath)
: Typeface.createFromAsset(skinResources.getAssets(), skinTypefacePath);
}
}
總結(jié):兩篇文章逼侦,大致分析了整個實現(xiàn)過程匿辩,最后總結(jié)梳理一下,首先是我們實現(xiàn)主體更好的切入點是Factory接口榛丢,整個接口是專門攔截控件的铲球,然后仿造系統(tǒng)兼容包的實現(xiàn)自定義Inflater,創(chuàng)建我們的兼容對象晰赞,通過我們兼容對象來自定義顏色等屬性稼病,這里的顏色獲取就是通過宿主或者主題資源包來獲得,主題資源包我們通過資源加載的方式通過宿主的上下文加載資源包的資源掖鱼,獲取對應(yīng)的屬性值然走,因為我們資源包的資源定義名稱和宿主保持一致。