換膚分為動態(tài)換膚和靜態(tài)換膚
靜態(tài)換膚
這種換膚的方式,也就是我們所說的內(nèi)置換膚,就是在APP內(nèi)部放置多套相同的資源站蝠。進行資源的切換。
這種換膚的方式有很多缺點卢佣,比如, 靈活性差区赵,只能更換內(nèi)置的資源惭缰、apk體積太大,在我們的應(yīng)用Apk中等一般圖片文件能占到apk大小的一半左右。
當(dāng)然了,這種方式也并不是一無是處, 比如我們的應(yīng)用內(nèi)笼才,只是普通的 日夜間模式 的切換漱受,并不需要圖片等的更換,只是更換顏色,那這樣的方式就很實用骡送。
動態(tài)換膚
適用于大量皮膚昂羡,用戶選擇下載,像QQ摔踱、網(wǎng)易云音樂這種虐先。它是將皮膚包下載到本地,皮膚包其實是個APK派敷。
換膚包括替換圖片資源蛹批、布局顏色、字體篮愉、文字顏色腐芍、狀態(tài)欄和導(dǎo)航欄顏色。
動態(tài)換膚步驟包括:
- 采集需要換膚的控件
- 加載皮膚包
- 替換資源
采集換膚控件
android解析xml創(chuàng)建view的步驟:
- setContentView -> window.setContentView()(實現(xiàn)類是PhoneWindow)->mLayoutInflater.inflate() -> inflate .. ->createViewFromTag().
所以我們復(fù)寫了Factory的onCreateView之后试躏,就可以不通過系統(tǒng)層而是自己截獲從xml映射的View進行相關(guān)View創(chuàng)建的操作猪勇,包括對View的屬性進行設(shè)置(比如背景色,字體大小颠蕴,顏色等)以實現(xiàn)換膚的效果泣刹。如果onCreateView返回null的話,會將創(chuàng)建View的操作交給Activity默認(rèn)實現(xiàn)的Factory的onCreateView處理犀被。
1.使用ActivityLifecycleCallbacks椅您,盡可能少的去侵入代碼,在onActivityCreated中監(jiān)聽每個activity的創(chuàng)建寡键。
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
LayoutInflater layoutInflater = LayoutInflater.from(activity);
try {
//系統(tǒng)默認(rèn) LayoutInflater只能設(shè)置一次factory掀泳,所以利用反射解除限制
Field mFactorySet = LayoutInflater.class.getDeclaredField("mFactorySet");
mFactorySet.setAccessible(true);
mFactorySet.setBoolean(layoutInflater, false);
} catch (Exception e) {
e.printStackTrace();
}
//添加自定義創(chuàng)建View 工廠
SkinLayoutFactory factory = new SkinLayoutFactory(activity, skinTypeface);
layoutInflater.setFactory2(factory);
}
2.在 SkinLayoutFactory中將每個創(chuàng)建的view進行篩選采集
//根據(jù)tag反射獲取view
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
// 反射 classLoader
View view = createViewFromTag(name, context, attrs);
// 自定義View
if(null == view){
view = createView(name, context, attrs);
}
//篩選符合屬性View
skinAttribute.load(view, attrs);
return view;
}
3.將view封裝成對象
//view的參數(shù)對象
static class SkinPain {
String attributeName;
int resId;
public SkinPain(String attributeName, int resId) {
this.attributeName = attributeName;
this.resId = resId;
}
}
//view對象
static class SkinView {
View view;
List<SkinPain> skinPains;
public SkinView(View view, List<SkinPain> skinPains) {
this.view = view;
this.skinPains = skinPains;
}
}
將屬性符合的view保存起來
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");
mAttributes.add("skinTypeface");
}
private List<SkinView> skinViews = new ArrayList<>();
public void load(View view, AttributeSet attrs) {
List<SkinPain> skinPains = new ArrayList<>();
for (int i = 0; i < attrs.getAttributeCount(); i++) {
//獲取屬性名字
String attributeName = attrs.getAttributeName(i);
if (mAttributes.contains(attributeName)) {
//獲取屬性對應(yīng)的值
String attributeValue = attrs.getAttributeValue(i);
if (attributeValue.startsWith("#")) {
continue;
}
int resId;
//判斷前綴字符串 是否是"?"
//attributeValue = "?2130903043"
if (attributeValue.startsWith("?")) { //系統(tǒng)屬性值
//字符串的子字符串 從下標(biāo) 1 位置開始
int attrId = Integer.parseInt(attributeValue.substring(1));
resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
} else {
//@1234564
resId = Integer.parseInt(attributeValue.substring(1));
}
if (resId != 0) {
SkinPain skinPain = new SkinPain(attributeName, resId);
skinPains.add(skinPain);
}
}
}
//SkinViewSupport是自定義view實現(xiàn)的接口,用來區(qū)分是否需要換膚
if (!skinPains.isEmpty() || view instanceof TextView || view instanceof SkinViewSupport) {
SkinView skinView = new SkinView(view, skinPains);
skinView.applySkin(mTypeface);
skinViews.add(skinView);
}
}
...
}
加載皮膚包
加載皮膚包需要我們動態(tài)獲取網(wǎng)絡(luò)下載的皮膚包資源昌腰,問題是我們?nèi)绾渭虞d皮膚包中的資源
Android訪問資源使用的是Resources這個類,但是程序里面通過getContext獲取到的Resources實例實際上是對應(yīng)程序本來的資源的實例膀跌,也就是說這個實例只能加載app里面的資源遭商,想要加載皮膚包里面的就不行了
自己構(gòu)造一個Resources(這個Resources指向的資源就是我們的皮膚包)
看看Resources的構(gòu)造方法,可以看到主要是需要一個AssetManager
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
構(gòu)造一個指向皮膚包的AssetManager捅伤,但是這個AssetManager是不能直接new出來的劫流,這里就使用反射來實例化了
AssetManager assetManager = AssetManager.class.newInstance();
AssetManager有一個addAssetPath方法可以指定資源的位置,可惜這個也只能用反射來調(diào)用
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, filePath);
再來看看Resources的其他兩個參數(shù),一個是DisplayMetrics祠汇,一個是Configuration仍秤,這兩的就可以直接使用app原來的Resources里面的就可以。
具體代碼如下:
public void loadSkin(String path) {
if(TextUtils.isEmpty(path)){
// 記錄使用默認(rèn)皮膚
SkinPreference.getInstance().setSkin("");
//清空資源管理器可很, 皮膚資源屬性等
SkinResources.getInstance().reset();
} else {
try {
//反射創(chuàng)建AssetManager
AssetManager manager = AssetManager.class.newInstance();
// 資料路徑設(shè)置
Method addAssetPath = manager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(manager, path);
Resources appResources = this.application.getResources();
Resources skinResources = new Resources(manager,
appResources.getDisplayMetrics(), appResources.getConfiguration());
//記錄當(dāng)前皮膚包
SkinPreference.getInstance().setSkin(path);
//獲取外部Apk(皮膚笔Α) 包名
PackageManager packageManager = this.application.getPackageManager();
PackageInfo packageArchiveInfo = packageManager.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
String packageName = packageArchiveInfo.packageName;
SkinResources.getInstance().applySkin(skinResources,packageName);
} catch (Exception e) {
e.printStackTrace();
}
}
setChanged();
//通知觀者者,進行替換資源
notifyObservers();
}
替換資源
換膚的核心操作就是替換資源我抠,這里采用觀察者模式苇本,被觀察者是我們的換膚管理類SkinManager,觀察者是我們之前緩存的每個頁面的LayoutInflater.Factory2
@Override
public void update(Observable o, Object arg) {
//狀態(tài)欄
SkinThemeUtils.updataStatusBarColor(activity);
//字體
Typeface skinTypeface = SkinThemeUtils.getSkinTypeface(activity);
skinAttribute.setTypeface(skinTypeface);
//更換皮膚
skinAttribute.applySkin();
}
applySkin()在去遍歷每個factory緩存的需要換膚的view菜拓,調(diào)用他們的換膚方法
public void applySkin() {
for (SkinView mSkinView : skinViews) {
mSkinView.applySkin(mTypeface);
}
}
applySkin方法如下:
public void applySkin(Typeface typeface) {
//換字體
if(view instanceof TextView){
((TextView) view).setTypeface(typeface);
}
//自定義view換膚
if(view instanceof SkinViewSupport){
((SkinViewSupport)view).applySkin();
}
for (SkinPain skinPair : skinPains) {
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;
case "skinTypeface" :
applyTypeface(SkinResources.getInstance().getTypeface(skinPair.resId));
break;
default:
break;
}
if (null != left || null != right || null != top || null != bottom) {
((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right,
bottom);
}
}
}
這里能看到換膚的實現(xiàn)方式就是根據(jù)原始資源Id來獲取皮膚包的資源Id瓣窄,從而加載資源。因此我們要保證app和皮膚包的資源名稱一致
public Drawable getDrawable(int resId) {
//如果有皮膚 isDefaultSkin false 沒有就是true
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);//查找對應(yīng)的資源id
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
//獲取皮膚包中對應(yīng)資源的id
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮膚包中的資源id不一定就是 當(dāng)前程序的 id
//獲取對應(yīng)id 在當(dāng)前的名稱 例如colorPrimary
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher /colorPrimaryDark
String resType = mAppResources.getResourceTypeName(resId);//drawable
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);//使用皮膚包的Resource
return skinId;
}
皮膚包的生成
其實很簡單纳鼎,就是我們重新建立一個項目(這個項目里面的資源名字和需要換膚的項目的資源名字是對應(yīng)的就可以)俺夕,記住我們是通過名字去獲取資源,不是id
- 新建工程project
- 將換膚的資源文件添加到res文件下贱鄙,無java文件
- 直接運行build.gradle劝贸,生成apk文件(注意,運行時Run/Redebug configurations 中Launch Options選擇launch nothing)贰逾,否則build 會報 no default Activty的錯誤悬荣。
- 將apk文件重命名,如black.apk重命名為black.skin防止用戶點擊安裝