現(xiàn)在的很多應(yīng)用都有換膚的功能伞梯,例如QQ玫氢。這類應(yīng)用都是在線下載皮膚包,然后在不重啟的情況下直接完成換膚
示例
原理
- Activity setContentView內(nèi)部調(diào)用
關(guān)于setContentView的所有方法,這里調(diào)用了getWindow()返回了Window谜诫,這個(gè)Window在activity的attach方法中被賦值為PhoneWindow
Activity.java源碼:
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
public void setContentView(View view) {
getWindow().setContentView(view);
initWindowDecorActionBar();
}
public void setContentView(View view, ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
initWindowDecorActionBar();
}
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback) {
...
mWindow = new PhoneWindow(this, window, activityConfigCallback);
...
}
- PhoneWindow setContentView內(nèi)部調(diào)用
可以看到實(shí)際調(diào)用了LayoutInflater.inflate方法
PhoneWindow.java源碼:
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
@Override
public void setContentView(View view) {
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
- LayoutInflater.inflate內(nèi)部調(diào)用
由源碼可知漾峡,view由Factory2和Factory創(chuàng)建,如果我們hook了Factory2那不是視圖的創(chuàng)建可以由我們說了算
LayoutInflater.java源碼:
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
return inflate(resource, root, root != null);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
final Resources res = getContext().getResources();
if (DEBUG) {
Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
+ Integer.toHexString(resource) + ")");
}
final XmlResourceParser parser = res.getLayout(resource);
try {
return inflate(parser, root, attachToRoot);
} finally {
parser.close();
}
}
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
synchronized (mConstructorArgs) {
...
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
result = temp;
return result;
...
}
}
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {
...
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;
}
public void setFactory2(Factory2 factory) {
//由此處可知設(shè)置Factory2只能設(shè)置一次喻旷,所以我們設(shè)置時(shí)需要將mFactorySet改成false
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);
}
}
- Factory2
LayoutInflater.java源碼:
public interface Factory2 extends Factory {
public View onCreateView(View parent, String name, Context context, AttributeSet attrs);
}
可以看到參數(shù)里面有AttributeSet生逸,我們可以通過AttributeSet篩選需要做處理的屬性,記錄view和對(duì)應(yīng)的屬性且预,然后在換膚時(shí)替換屬性對(duì)應(yīng)的資源槽袄,就可以達(dá)到換膚的目的了,
具體處理邏輯較為復(fù)雜锋谐,可以通過后面提供的源碼查看
SkinPeeler庫
SkinPeeler庫是基于上面的原理完成的換皮庫遍尺,使用方法:
- 導(dǎo)入庫
//root build.gradle
allprojects {
repositories {
...
maven { url 'https://www.jitpack.io' }
}
}
//app build.gradle
dependencies {
implementation 'com.github.ray-tianfeng:skin-peeler:v1.0.0'
}
- 使用
換膚 SkinPeeler.getInstance().skin(String skinPath);
傳入制作好的皮膚包,即可完成換膚還原 SkinPeeler.getInstance().restore();
不使用皮膚換膚監(jiān)聽 SkinPeeler.getInstance().addSkinChangeListener(Activity
mActivity, SkinPeeler.OnSkinChangeListener mOnSkinChangeListener);
皮膚切換監(jiān)聽涮拗,完成換皮時(shí)回調(diào)-
自定義屬性適配器
- 實(shí)現(xiàn)BaseAttrADT.java
//支持的屬性集合,例如:background乾戏、src、textColor public List<String> getAttrName(); /** * 應(yīng)用皮膚 * @param targetView 目標(biāo)視圖 * @param skinResources 皮膚Resources * @param skinPackageName 皮膚包包名 * @param attrName 屬性名稱 * @param oldValueName 舊值方便通過{@link com.zlong.skinpeeler.utils.IdUtils} 查找皮膚包資源屬性和名稱 */ public void applySkin(View targetView, Resources skinResources, String skinPackageName, String attrName, String oldValueName) throws Exception; /** * 恢復(fù)原始皮膚 * @param targetView 目標(biāo)視圖 * @param resources 原始 Resources * @param attrName 屬性名稱 * @param oldValueName 舊值 */ public void restore(View targetView, Resources resources, String attrName, String oldValueName);
- 添加屬適配器至管理器
SkinPeeler.getInstance().addAttrADT(BaseAttrADT attrADT)
- 常用工具類
IdUtils:資源Id查找工具類三热,通過IdUtils.findResById(int id)
,查找原包中id對(duì)應(yīng)的類型鼓择、名稱
-
自定義屬性注意事項(xiàng)
- ID
原包中R.xx.xx對(duì)應(yīng)的資源id不可在皮膚包中使用,必須使用皮膚包中對(duì)應(yīng)資源的id就漾,因?yàn)樵械馁Y源對(duì)應(yīng)的id呐能,和皮膚包中同一資源對(duì)應(yīng)的id不同 - 資源查找
applySkin提供了皮膚包的Resources,那我們可以通過皮膚包資源id獲取對(duì)應(yīng)的資源抑堡,
我們把原包中的資源id通過IdUtils.findResById
查找資源對(duì)應(yīng)的名稱和類型摆出,然后通過Resources.getIdentifier(String name, String defType, String defPackage)
查找資源在皮膚包中對(duì)應(yīng)的id朗徊,最后獲取資源就行了
- ID
通過第二步我們可以得到資源的id,但是我們不能直接把皮膚包的資源id直接設(shè)置到view上懊蒸,因?yàn)樵つw對(duì)應(yīng)的Resources荣倾,肯定沒有皮膚包對(duì)應(yīng)的資源id。
在代碼中也不能直接設(shè)置資源id骑丸,因?yàn)閾Q膚后,直接設(shè)置資源id妒貌,系統(tǒng)直接通過原始Resources查找的資源通危。需要通過上面的資源查找,直接查找對(duì)應(yīng)的資源灌曙,設(shè)置到對(duì)應(yīng)的view上
庫內(nèi)置了AutoAttrADT.java可以對(duì)照著來實(shí)現(xiàn)自定義屬性
實(shí)現(xiàn)屬性
庫已經(jīng)通過AutoAttrADT.java實(shí)現(xiàn)了常用屬性的適配
background菊碟、src、textColor在刺、drawableLeft逆害、drawableTop、drawableRight蚣驼、drawableBottom-
皮膚包制作
- 創(chuàng)建Module
- 將apply plugin: 'com.android.library'修改為apply plugin:
'com.android.application'魄幕,因?yàn)檫@樣可以生成對(duì)應(yīng)的資源id - 將原項(xiàng)目中res目錄下的所有資源復(fù)制到皮膚包中,layout可以在完成制作后刪除
- 替換換膚時(shí)需要修改的資源
- 通過build->build bundles->build apk將皮膚包打包
- 在對(duì)應(yīng)module的build/outputs/debug
下有一個(gè)打包好的皮膚包apk颖杏,可以將后綴修改skin纯陨,或者直接使用。修改后綴為了防止用戶安裝和刪除留储。
-
庫使用注意事項(xiàng)
- 需要文件讀取權(quán)限翼抠,如果在6.0及以上,需要做權(quán)限處理
- 包名只能是androidManifest中的packageName获讳,不能在gradle使用applicationId阴颖,因?yàn)镮dUtils通過包名查找R類的。
- 所有的資源盡量先定義后使用(R.string.xx, R.color.xx, R.drawable.xx)
- 沉浸式菜單欄適配丐膝,先定義菜單欄顏色量愧,然后在String中定義圖標(biāo)顯示模式,設(shè)置監(jiān)聽尤误。皮膚變化時(shí)侠畔,在回調(diào)中更改狀態(tài)欄顏色及圖標(biāo)顏色
擴(kuò)展1
在上面的實(shí)現(xiàn)過程中有使用到AttributeSet,這個(gè)就是當(dāng)前view的屬性集合损晤,我們是不是可以自定義一個(gè)屬性(圓角背景)软棺。然后在onCreateView解析到此屬性時(shí),
通過java代碼創(chuàng)建一個(gè)drawable尤勋,設(shè)置給view喘落,注意此處自定義的屬性只能在xml中使用茵宪,因?yàn)閂iew不包含這個(gè)自定義的屬性的。!