思路總體結構java
下面全部的代碼位置,包括處理一些特殊問題的方案等等!android
https://github.com/xujiaji/ThemeSkinninggit
經過皮膚apk的全路徑,可知道其包名(須要用包名來獲取它的資源id)github
skinPkgPath
是apk的全路徑,經過mInfo.packageName
就能夠獲得包名PackageManager mPm = context.getPackageManager();
PackageInfo mInfo = mPm.getPackageArchiveInfo(skinPkgPath, PackageManager.GET_ACTIVITIES);
skinPackageName = mInfo.packageName;
複製代碼
經過反射添加路徑能夠建立皮膚apk的AssetManager對象web
skinPkgPath
是apk的全路徑,添加路徑的方法是AssetManager裏一個隱藏的方法經過反射能夠設置。assetManager
來訪問apk裏assets目錄的資源。AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, skinPkgPath);
複製代碼
建立皮膚apk的資源對象數組
Resources superRes = context.getResources();
Resources skinResource = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
複製代碼
當要經過資源id獲取顏色的時候緩存
int originColor = ContextCompat.getColor(context, resId);
context.getResources().getResourceEntryName(resId);
獲取資源id獲取它的名字mResources.getIdentifier(resName, "color", skinPackageName)
獲得皮膚apk中該資源id。(resName:就是資源名字;skinPackegeName就是皮膚apk的包名)mResources.getColor(trueResId)
經過getIdentifier
方法能夠經過名字來獲取id,好比將第二個參數修改成layout
、mipmap
、drawable
或string
就是經過資源名字獲取對應layout目錄
、mipmap目錄
、drawable目錄
或string文件
裏的資源id網絡
public int getColor(int resId) {
int originColor = ContextCompat.getColor(context, resId);
if (mResources == null || isDefaultSkin) {
return originColor;
}
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, "color", skinPackageName);
int trueColor;
if (trueResId == 0) {
trueColor = originColor;
} else {
trueColor = mResources.getColor(trueResId);
}
return trueColor;
}
複製代碼
當要經過資源id獲取圖片的時候app
drawable
目錄仍是mipmap
目錄進行了判斷public Drawable getDrawable(int resId) {
Drawable originDrawable = ContextCompat.getDrawable(context, resId);
if (mResources == null || isDefaultSkin) {
return originDrawable;
}
String resName = context.getResources().getResourceEntryName(resId);
int trueResId = mResources.getIdentifier(resName, "drawable", skinPackageName);
Drawable trueDrawable;
if (trueResId == 0) {
trueResId = mResources.getIdentifier(resName, "mipmap", skinPackageName);
}
if (trueResId == 0) {
trueDrawable = originDrawable;
} else {
if (android.os.Build.VERSION.SDK_INT < 22) {
trueDrawable = mResources.getDrawable(trueResId);
} else {
trueDrawable = mResources.getDrawable(trueResId, null);
}
}
return trueDrawable;
}
複製代碼
LayoutInflater.Factory2
接口來替換系統默認的那麼如何替換呢?框架
@Override
protected void onCreate(Bundle savedInstanceState) {
mSkinInflaterFactory = new SkinInflaterFactory(this);//自定義的Factory
LayoutInflaterCompat.setFactory2(getLayoutInflater(), mSkinInflaterFactory);
super.onCreate(savedInstanceState);
}
複製代碼
咱們使用的Activity通常是
AppCompatActivity
在裏面的onCreate方法中也有對其的設置和初始化,可是setFactory方法只能被調用一次,致使默認的一些初始化操做沒有被調用,這麼操做?
LayoutInflater.Factory2
接口的類,看onCreateView
方法中。在進行其餘操做前調用delegate.createView(parent, name, context, attrs)
處理系統的那一套邏輯。attrs.getAttributeBooleanValue
獲取當前view是不是可換膚的,第一個參數是xml名字空間,第二個參數是屬性名,第三個參數是默認值。這裏至關因而attrs.getAttributeBooleanValue("http://schemas.android.com/android/skin", "enable", false)
public class SkinInflaterFactory implements LayoutInflater.Factory2 {
private AppCompatActivity mAppCompatActivity;
public SkinInflaterFactory(AppCompatActivity appCompatActivity) {
this.mAppCompatActivity = appCompatActivity;
}
@Override
public View onCreateView(String s, Context context, AttributeSet attributeSet) {
return null;
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);//是不是可換膚的view
AppCompatDelegate delegate = mAppCompatActivity.getDelegate();
View view = delegate.createView(parent, name, context, attrs);//處理系統邏輯
if (view instanceof TextView && SkinConfig.isCanChangeFont()) {
TextViewRepository.add(mAppCompatActivity, (TextView) view);
}
if (isSkinEnable || SkinConfig.isGlobalSkinApply()) {
if (view == null) {
view = ViewProducer.createViewFromTag(context, name, attrs);
}
if (view == null) {
return null;
}
parseSkinAttr(context, attrs, view);
}
return view;
}
}
複製代碼
當內部的初始化操做完成後,若是判斷沒有建立好view,則須要咱們本身去建立view
ViewProducer.createViewFromTag(context, name, attrs)
來建立ViewProducer
,原理功能請看代碼註釋class ViewProducer {
//該處定義的是view構造方法的參數,也就是View兩個參數的構造方法:public View(Context context, AttributeSet attrs)
private static final Object[] mConstructorArgs = new Object[2];
//存放反射獲得的構造器
private static final Map<String, Constructor<? extends View>> sConstructorMap
= new ArrayMap<>();
//這是View兩個參數的構造器所對應的兩個參數
private static final Class<?>[] sConstructorSignature = new Class[]{
Context.class, AttributeSet.class};
//若是是系統的View或ViewGroup在xml中並非全路徑的,經過反射來實例化是須要全路徑的,這裏列出來它們可能出現的位置
private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
static View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view")) {//若是是view標籤,則獲取裏面的class屬性(該View的全名)
name = attrs.getAttributeValue(null, "class");
}
try {
//須要傳入構造器的兩個參數的值
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
if (-1 == name.indexOf('.')) {//若是不包含小點,則是內部View
for (int i = 0; i < sClassPrefixList.length; i++) {//因爲不知道View具體在哪一個路徑,因此經過循環全部路徑,直到能實例化或結束
final View view = createView(context, name, sClassPrefixList[i]);
if (view != null) {
return view;
}
}
return null;
} else {//不然就是自定義View
return createView(context, name, null);
}
} catch (Exception e) {
//若是拋出異常,則返回null,讓LayoutInflater本身去實例化
return null;
} finally {
// 清空當前數據,避免和下次數據混在一塊兒
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
private static View createView(Context context, String name, String prefix) throws ClassNotFoundException, InflateException {
//先從緩存中獲取當前類的構造器
Constructor<? extends View> constructor = sConstructorMap.get(name);
try {
if (constructor == null) {
// 若是緩存中沒有建立過,則嘗試去建立這個構造器。經過類加載器加載這個類,若是是系統內部View因爲不是全路徑的,則前面加上
Class<? extends View> clazz = context.getClassLoader().loadClass(
prefix != null ? (prefix + name) : name).asSubclass(View.class);
//獲取構造器
constructor = clazz.getConstructor(sConstructorSignature);
//將構造器放入緩存
sConstructorMap.put(name, constructor);
}
//設置爲無障礙(設置後即便是私有方法和成員變量均可訪問和修改,除了final修飾的)
constructor.setAccessible(true);
//實例化
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
// We do not want to catch these, lets return null and let the actual LayoutInflater
// try
return null;
}
}
}
複製代碼
view = ViewProducer.createViewFromTag(context, name, attrs);
刪除,換成下方代碼:LayoutInflater inflater = mAppCompatActivity.getLayoutInflater();
if (-1 == name.indexOf('.'))//若是爲系統內部的View則,經過循環這幾個地方來實例化View,道理跟上面ViewProducer裏面同樣
{
for (String prefix : sClassPrefixList)
{
try
{
view = inflater.createView(name, prefix, attrs);
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
if (view != null) break;
}
} else
{
try
{
view = inflater.createView(name, null, attrs);
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
}
複製代碼
sClassPrefixList
的定義private static final String[] sClassPrefixList = {
"android.widget.",
"android.view.",
"android.webkit."
};
複製代碼
最後是最終的攔截獲取須要換膚的View的部分,也就是上面
SkinInflaterFactory
類的onCreateView
最後調用的parseSkinAttr
方法
private Map<View, SkinItem> mSkinItemMap = new HashMap<>();
複製代碼
private void parseSkinAttr(Context context, AttributeSet attrs, View view) {
//保存須要換膚處理的xml屬性
List<SkinAttr> viewAttrs = new ArrayList<>();
//變量該view的全部屬性
for (int i = 0; i < attrs.getAttributeCount(); i++) {
String attrName = attrs.getAttributeName(i);//獲取屬性名
String attrValue = attrs.getAttributeValue(i);//獲取屬性值
//若是屬性是style,例如xml中設置:style="@style/test_style"
if ("style".equals(attrName)) {
//可換膚的屬性
int[] skinAttrs = new int[]{android.R.attr.textColor, android.R.attr.background};
//常常在自定義View時,構造方法中獲取屬性值的時候使用到。
//這裏經過傳入skinAttrs,TypeArray中將會包含這兩個屬性和值,若是style裏沒有那就沒有 - -
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, skinAttrs, 0, 0);
//獲取屬性對應資源的id,第一個參數這裏對應下標的就是上面skinAttrs數組裏定義的下標,第二個參數是沒有獲取到的默認值
int textColorId = a.getResourceId(0, -1);
int backgroundId = a.getResourceId(1, -1);
if (textColorId != -1) {//若是有顏色屬性
//<style name="test_style">
//<item name="android:textColor">@color/colorAccent</item>
//<item name="android:background">@color/colorPrimary</item>
//</style>
//以上邊的參照來看
//entryName就是colorAccent
String entryName = context.getResources().getResourceEntryName(textColorId);
//typeName就是color
String typeName = context.getResources().getResourceTypeName(textColorId);
//建立一換膚屬性實力類來保存這些信息
SkinAttr skinAttr = AttrFactory.get("textColor", textColorId, entryName, typeName);
if (skinAttr != null) {
viewAttrs.add(skinAttr);
}
}
if (backgroundId != -1) {//若是有背景屬性
String entryName = context.getResources().getResourceEntryName(backgroundId);
String typeName = context.getResources().getResourceTypeName(backgroundId);
SkinAttr skinAttr = AttrFactory.get("background", backgroundId, entryName, typeName);
if (skinAttr != null) {
viewAttrs.add(skinAttr);
}
}
a.recycle();
continue;
}
//判斷是不是支持的屬性,而且值是引用的,如:@color/red
if (AttrFactory.isSupportedAttr(attrName) && attrValue.startsWith("@")) {
try {
//去掉屬性值前面的「@」則爲id
int id = Integer.parseInt(attrValue.substring(1));
if (id == 0) {
continue;
}
//資源名字,如:text_color_selector
String entryName = context.getResources().getResourceEntryName(id);
//資源類型,如:color、drawable
String typeName = context.getResources().getResourceTypeName(id);
SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName);
if (mSkinAttr != null) {
viewAttrs.add(mSkinAttr);
}
} catch (NumberFormatException e) {
SkinL.e(TAG, e.toString());
}
}
}
//是否有須要換膚的屬性?
if (!SkinListUtils.isEmpty(viewAttrs)) {
SkinItem skinItem = new SkinItem();
skinItem.view = view;
skinItem.attrs = viewAttrs;
mSkinItemMap.put(skinItem.view, skinItem);
//是否換膚
if (SkinManager.getInstance().isExternalSkin() ||
SkinManager.getInstance().isNightMode()) {//若是當前皮膚來自於外部或者是處於夜間模式
skinItem.apply();//應用於這個view
}
}
}
複製代碼
SkinFileUtils
工具類調用getSkinDir
方法獲取皮膚的緩存目錄項目目錄\app\src
,建立一個和渠道相同名字的目錄。好比說有個red
渠道。
SkinInfaterFactory
的方法parseSkinAttr
中有這樣一句來進行過濾沒有帶@的屬性值:
context.getResources().getIdentifier(name, "mipmap", context.getPackageName())
來獲取圖片資源(參考這奇葩方式的庫)。但因爲這個屬性是須要換膚更換的屬性,因而沒辦法,專門爲這兩個屬性在SkinInfaterFactory
的parseSkinAttr
方法中寫了個判斷
Android-Skin-Loader
後的框架ThemeSkinning
)其餘一些幫助信息:
上面對應的代碼片斷都有對應路徑哦!
這篇文章的所有代碼,測試項目位置:https://github.com/xujiaji/ThemeSkinning
測試項目中的首頁底部導航測試和修改位置:https://github.com/xujiaji/FlycoTabLayout
下面這張Gif圖片是測試項目運行的效果圖: