目錄
4.2.調用兼容AppCompatActivity代理類AppCompatDelegate實現xml佈局到View視圖的轉換
4.3.AppCompatActivity定義實現不同版本AppCompatDelegate實現類
4.4.setContentView()方法最終調用代理類AppCompatDelegateImplV7中的方法
4.5.在LayoutInflater中實現基於XmlPullParser解析xml佈局文件
4.6.xml解析調用createViewFromTag()通過控件名稱和屬性集合創建視圖
4.7.createViewFromTag()真正執行解析xml佈局文件以後創建View視圖
4.8.mFactory2來源(調試發現最終調用的是AppCompatActivity代理類的onCreateView()方法)
6.1SkinFactory實現Factory2接口,完成視圖的創建,視圖的緩存,全部視圖的換膚操作;
6.2SkinEngine加載插件apk的對應的Resources,(Resources用插件apk的資源)實現更換皮膚的各種方法(例如:getColor等)
實現換膚的方案:
a.靜態修改theme主題方式
設置多套皮膚的theme;
styles.xml
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="defaultcolor">@color/defalultcolor</item>
</style>
<style name="NewTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="defaultcolor">@color/newcolor</item>
</style>
</resources>
聲明屬性需要動態替換的樣式屬性
attrs.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="defaultcolor" format="reference|color" />
</resources>
爲控件設置屬性樣式?attr/defaultcolor
<TextView
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="這是第一個fragment"
android:textColor="?attr/defaultcolor"
android:textSize="36dp" />
動態設置主題
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if( 1 == 1){
setTheme(R.style.AppTheme);
}else {
setTheme(R.style.NewTheme);
}
setContentView(R.layout.activity_main);
}
缺點是設置主題在setContentView之前,設置以後需要重啓Activity;
b.動態修改樣式(應用內/插件化-皮膚apk),不需要重啓Activity
應用內
採用通過相同名稱+不同皮膚後綴來區分不同皮膚,如實現黑白皮膚,文本顏色item_text_color有一套默認皮膚,一套黑色皮膚定義資源item_text_color,item_text_color_black;
String resName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
resName+"_"+不同皮膚後綴
缺點:應用內多套皮膚時可能導致安裝包過大;
插件化-皮膚apk
皮膚apk和主apk有相同皮膚資源名稱,獲取皮膚apk下的資源名稱,需要和主apk下資源名稱一致,設置在皮膚apk下皮膚資源 ;
String resName = mOutResource.getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
mOutResource皮膚apk資源Resources;
動態修改視圖皮膚需要解決兩個問題:
1).緩存獲取全部需要換膚的視圖,換膚(資源)時執行換膚操作;
2).獲取Resources資源(插件apk資源Resources),動態設置皮膚;
1.什麼是一鍵換膚
所謂”一鍵換膚“就是通過一個接口調用,實現app範圍內所有資源文件替換,包括文本,顏色,圖片,動畫等;
2.界面上那些東西可以換膚
例如TextView文字顏色,字體大小,ImageView的background等等;
res目錄所有的資源幾乎都可以替換,具體如下:
動畫
背景圖片
字體
字體顏色
字體大小
音頻
視頻
3.利用Hook實現一鍵換膚
什麼是hook
如題,我是用hook實現一鍵換膚。那麼什麼是hook?
hook,鉤子. 安卓中的hook技術,其實是一個抽象概念:對系統源碼的代碼邏輯進行"劫持",插入自己的邏輯,然後放行。注意:hook可能頻繁使用java反射機制···
"一鍵換膚"中的hook思路
- "劫持"系統創建View的過程,我們自己來創建View
系統原本自己存在創建View的邏輯,我們要了解這部分代碼,以便爲我所用. - 收集我們需要換膚的View(用自定義view屬性來標記一個view是否支持一鍵換膚),保存到變量中
劫持了 系統創建view的邏輯之後,我們要把支持換膚的這些view保存起來 - 加載外部資源包,調用接口進行換膚
外部資源包,是.apk
後綴的一個文件,是通過gradle
打包形成的。裏面包含需要換膚的資源文件,但是必須保證,要換的資源文件,和原工程裏面的文件名完全相同
.
4.Android創建視圖源碼分析
自己定義Activity繼承自兼容AppCompatActivity
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
創建Activity經常在onCreate()方法中調用setContentView(R.layout.xxx);設置要顯示視圖,那麼如何將我們創建的xml佈局文件轉換爲要顯示View視圖呢?
源碼執行流程:
4.1.自定義Activity設置要顯示的佈局文件xml
setContentView(R.layout.activity_main);
4.2.調用兼容AppCompatActivity代理類AppCompatDelegate實現xml佈局到View視圖的轉換
由於Android版本比較分散,需要兼容各個版本Android系統,AppCompatActivity定義實現不同版本AppCompatDelegate實現類;
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
4.3.AppCompatActivity定義實現不同版本AppCompatDelegate實現類
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (sdk >= 23) {
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV7(context, window, callback);
}
}
4.4.setContentView()方法最終調用代理類AppCompatDelegateImplV7中的方法
通過如下實現將layout的xml佈局文件轉換爲View視圖添加到父視圖上contentParent;
LayoutInflater.from(mContext).inflate(resId, contentParent);
4.5.在LayoutInflater中實現基於XmlPullParser解析xml佈局文件
4.6.xml解析調用createViewFromTag()通過控件名稱和屬性集合創建視圖
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
final AttributeSet attrs = Xml.asAttributeSet(parser);
View result = root;
//找到跟節點
int type;
while ((type = parser.next()) != XmlPullParser.START_TAG &&
type != XmlPullParser.END_DOCUMENT) {
// Empty
}
if (type != XmlPullParser.START_TAG) {
throw new InflateException(parser.getPositionDescription()
+ ": No start tag found!");
}
//獲取跟節點名字
final String name = parser.getName();
//創建xml的layout佈局文件跟視圖
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//遞歸調用創建子視圖
rInflateChildren(parser, temp, attrs, true);
// 添加到xml佈局文件視圖到父視圖
if (root != null && attachToRoot) {
root.addView(temp, params);
}
//將xml佈局的跟視圖添加做爲父視圖
if (root == null || !attachToRoot) {
result = temp;
}
...部分源碼
return result;
}
}
//創建xml佈局文件的跟視圖
final View temp = createViewFromTag(root, name, inflaterContext, attrs);
//遞歸創建xml佈局文件的子視圖,最後調用createViewFromTag()方法創建視圖
rInflateChildren(parser, temp, attrs, true);
createViewFromTag(parent, name, context, attrs);
4.7.createViewFromTag()真正執行解析xml佈局文件以後創建View視圖
最終調用mFactory2實現視圖創建
view = mFactory2.onCreateView(parent, name, context, attrs);
if(view==null)
通過反射創建View(例如自定義View)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
boolean ignoreThemeAttr) {}
* @param parent:創建視圖的父View
* @param name:xml標籤使用的視圖名稱(例如:<TextView/>)
* @param context:上下文
* @param attrs:需要創建的xml標籤的屬性集;(例如:android:textColor="")
* @param ignoreThemeAttr:true忽略{android:theme}屬性爲被解析的視圖,false相反;
4.8.mFactory2來源(調試發現最終調用的是AppCompatActivity代理類的onCreateView()方法)
AppCompatActivity設置LayoutInflater工廠
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
final AppCompatDelegate delegate = getDelegate();
delegate.installViewFactory();
}
代理類AppCompatDelegateImplV7實現installViewFactory()方法
AppCompatDelegate(Activity代理類)
@Override
public void installViewFactory() {
LayoutInflater layoutInflater = LayoutInflater.from(mContext);
if (layoutInflater.getFactory() == null) {
LayoutInflaterCompat.setFactory(layoutInflater, this);
} else {
if (!(LayoutInflaterCompat.getFactory(layoutInflater)
instanceof AppCompatDelegateImplV7)) {
Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
+ " so we can not install AppCompat's");
}
}
}
LayoutInflaterCompat是LayoutInflater兼容類
//LayoutInflater不同版本的實現
static final LayoutInflaterCompatImpl IMPL;
static {
final int version = Build.VERSION.SDK_INT;
if (version >= 21) {
IMPL = new LayoutInflaterCompatImplV21();
} else if (version >= 11) {
IMPL = new LayoutInflaterCompatImplV11();
} else {
IMPL = new LayoutInflaterCompatImplBase();
}
}
public static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
IMPL.setFactory(inflater, factory);
}
static class LayoutInflaterCompatImplV11 extends LayoutInflaterCompatImplBase {
@Override
public void setFactory(LayoutInflater layoutInflater, LayoutInflaterFactory factory) {
LayoutInflaterCompatHC.setFactory(layoutInflater, factory);
}
}
FactoryWrapperHC實現了Factory2接口,包裹LayoutInflaterFactory接口,當調用Factroy2.
onCreateView方法時最後調用LayoutInflaterFactory(AppCompatDelegateImplV9).onCreateView()方法(class
AppCompatDelegateImplV9 extends AppCompatDelegateImplBase
implements MenuBuilder.Callback, LayoutInflaterFactory),
AppCompatDelegateImplV9實現LayoutInflaterFactory接口
static void setFactory(LayoutInflater inflater, LayoutInflaterFactory factory) {
final LayoutInflater.Factory2 factory2 = factory != null
? new FactoryWrapperHC(factory) : null;
//設置包裹LayoutInflaterFactory類做爲factory2
inflater.setFactory2(factory2);
final LayoutInflater.Factory f = inflater.getFactory();
if (f instanceof LayoutInflater.Factory2) {
// The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
// We will now try and force set the merged factory to mFactory2
forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
} else {
// Else, we will force set the original wrapped Factory2
forceSetFactory2(inflater, factory2);
}
}
static class FactoryWrapperHC extends LayoutInflaterCompatBase.FactoryWrapper
implements LayoutInflater.Factory2 {
FactoryWrapperHC(LayoutInflaterFactory delegateFactory) {
super(delegateFactory);
}
@Override
public View onCreateView(View parent, String name, Context context,
AttributeSet attributeSet) {
return mDelegateFactory.onCreateView(parent, name, context, attributeSet);
}
}
LayoutInfalter設置factory2
/**
* Like {@link #setFactory}, but allows you to set a {@link Factory2}
* interface.
*/
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);
}
}
LayoutInflaterCompat.setFactory(layoutInflater, this);
將代理類AppCompatDelegateImplV7的this設置爲mFactory2;
AppCompatDelegateImplV7實現了LayoutInflaterFactory接口,mFactory2調用onCreateView()方法最終調用實現Factory2接口FactoryWrapperHC類,FactoryWrapperHC包裹類LayoutInflaterFactory,最後調用LayoutInflaterFactory(AppCompatDelegateImplV7).onCreateView()方法創建視圖;
4.9.AppCompatViewInflater的createView()方法真正創建視圖(AppCompatDelegateImplV9.onCreateView()調用AppCompatViewInflater.createView())
@Override
public View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs) {
final boolean isPre21 = Build.VERSION.SDK_INT < 21;
if (mAppCompatViewInflater == null) {
mAppCompatViewInflater = new AppCompatViewInflater();
}
// We only want the View to inherit its context if we're running pre-v21
final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
true, /* Read read app:theme as a fallback at all times for legacy reasons */
VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
);
}
AppCompatViewInflater
public final View createView(View parent, final String name, @NonNull Context context,
@NonNull AttributeSet attrs, boolean inheritContext,
boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
final Context originalContext = context;
// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
// by using the parent's context
if (inheritContext && parent != null) {
context = parent.getContext();
}
if (readAndroidTheme || readAppTheme) {
// We then apply the theme on the context, if specified
context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
}
if (wrapContext) {
context = TintContextWrapper.wrap(context);
}
View view = null;
// We need to 'inject' our tint aware Views in place of the standard framework versions
switch (name) {
case "TextView":
view = new AppCompatTextView(context, attrs);
break;
case "ImageView":
view = new AppCompatImageView(context, attrs);
break;
case "Button":
view = new AppCompatButton(context, attrs);
break;
case "EditText":
view = new AppCompatEditText(context, attrs);
break;
case "Spinner":
view = new AppCompatSpinner(context, attrs);
break;
case "ImageButton":
view = new AppCompatImageButton(context, attrs);
break;
case "CheckBox":
view = new AppCompatCheckBox(context, attrs);
break;
case "RadioButton":
view = new AppCompatRadioButton(context, attrs);
break;
case "CheckedTextView":
view = new AppCompatCheckedTextView(context, attrs);
break;
case "AutoCompleteTextView":
view = new AppCompatAutoCompleteTextView(context, attrs);
break;
case "MultiAutoCompleteTextView":
view = new AppCompatMultiAutoCompleteTextView(context, attrs);
break;
case "RatingBar":
view = new AppCompatRatingBar(context, attrs);
break;
case "SeekBar":
view = new AppCompatSeekBar(context, attrs);
break;
}
if (view == null && originalContext != context) {
// If the original context does not equal our themed context, then we need to manually
// inflate it using the name so that android:theme takes effect.
view = createViewFromTag(context, name, attrs);
}
if (view != null) {
// If we have created a view, check it's android:onClick
checkOnClickListener(view, attrs);
}
return view;
}
創建Android的原生控件都是以AppCompat開頭的兼容類例如:AppCompatSeekBar;
整個流程分析完成,我們發現最終調用mFactory2.onCreateView()和反射的方式創建視圖;
就可以實現Factory2接口 ,給LayoutInflater重新設置實現自己邏輯Factory2(創建View視圖,緩存視圖和視圖屬性便於動態調用修改視圖屬性-文字顏色,字體大小,背景等)
5.Resources/AssetManager
調用AssetManager.addAssetPath("插件apk路徑")指定要加載的皮膚apk,創建執行插件apk的Resources以便獲取資源(圖片,顏色,尺寸等);
需要動態設置皮膚資源就需要獲取相關資源類(顏色,字體,背景等等),AssetManager.addAssetPath(String path)可以實現調用指定資源包;
Android中與資源相關的類主要有Resources、ResourcesImpl、ResourcesManager、Java層的AssetManager和Native層的AssetManager;
Resources:提供了大多數與應用開發直接相關的加載資源的方法,比如getColor(int resId)等。實現上是通過AssetManager來加載資源並進行解析。
AssetManager:Java層的AssetManager是對Native層AssetManager的封裝,爲上層提供了加載資源的方法。
加載資源包:一般而言,一個App至少會引用兩個資源包:系統資源包和App的資源包。一個AssetManager對象可加載多個資源包。App在創建AssetManager的時候,會先加載系統資源包,再加載App資源包。這樣,通過一個AssetManager對象就可以訪問系統資源包和App資源包的資源了。
AssetManager.addAssetPath(String path)是一個@hide方法,調用這個方法可以加載指定的資源包。
訪問資源:由於資源id包含了package id的信息,AssetManager通過解析資源id,即可知道從哪個資源包來加載資源。
6.實現關鍵類SkinFactory,SkinEngine
6.1SkinFactory實現Factory2接口,完成視圖的創建,視圖的緩存,全部視圖的換膚操作;
/**
* 自定義Factory2,目的是拿到需要換膚的View和View樣式
*/
public class SkinFactory implements LayoutInflater.Factory2 {
//預定義一個委託類,他負責按照系統原有邏輯來創建View
private AppCompatDelegate mDelegate;
//緩存要換膚的View
private List<SkinView> lisCacheSkinViews = new ArrayList<>();
/**
* 外部提供一個Set方法
* @param mDelegate
*/
public void setDelegate(AppCompatDelegate mDelegate) {
this.mDelegate = mDelegate;
}
/**
*
* @param parent
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//關鍵點1:執行系統代碼裏的創建View的過程,我們只是想加入自己的思想,並不是全盤接管
//系統創建出來的時候有可能爲空,你問爲啥?請全文搜索 “標記標記,因爲” 你會找到你要的答案
View view = mDelegate.createView(parent, name, context, attrs);
if(view == null){//萬一系統創建出來是空,那我們來補救
if(-1 == name.indexOf('.')){ //不包含,說明不帶包名,那麼我門幫他加上包名
view = createViewByPrefix(context, name, prefixs, attrs);
}else {
view = createViewByPrefix(context, name, null, attrs);
}
}
//關鍵點2:收集需要換膚的View
collectSkinView(context, attrs, view);
return view;
}
/**
* 收集換膚的控件
* 收集的方式是:通過自定義isSupport,從創建出來的很多View中,找到支持換膚的那些,保存到map中
* @param context
* @param attrs
* @param view
*/
private void collectSkinView(Context context, AttributeSet attrs, View view){
//獲取我們自己定義的屬性
TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.Skinable);
boolean isSupport = typedArray.getBoolean(R.styleable.Skinable_isSupport, false);
if(isSupport){//找到支持換膚的View
final int Len = attrs.getAttributeCount();
HashMap<String, String> attrMap = new HashMap<>();
for(int i=0; i< Len; i++){//遍歷所有屬性
String attrName = attrs.getAttributeName(i);
String attrValue = attrs.getAttributeValue(i);
attrMap.put(attrName, attrValue);//全部存起來
}
SkinView skinView = new SkinView();
skinView.view = view;
skinView.attrsMap = attrMap;
//將可換膚的View放到listCacheSkinView中
lisCacheSkinViews.add(skinView);
}
}
/**
* 公開對外的換膚接口
*/
public void changeSkin(){
for(SkinView skinView : lisCacheSkinViews){
skinView.changeSkin();
}
}
/**
* Factory2是繼承Factory的,所以這次主要重寫Factory2的onCreatteView邏輯,就不必理會Factory的重寫方法
* @param name
* @param context
* @param attrs
* @return
*/
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return null;
}
/**
* 所謂hook,要懂源碼,懂了之後再劫持系統邏輯,加入自己的邏輯。
* 那麼,既然懂了,系統的有些代碼,直接拿過來用,也無可厚非。
*/
//*******************************下面一大片,都是從源碼裏面抄過來的,並不是我自主設計******************************
// 你問我抄的哪裏的?到 AppCompatViewInflater類源碼裏面去搜索:view = createViewFromTag(context, name, attrs);
static final Class<?>[] mConstructorSignature = new Class[]{Context.class, AttributeSet.class};//
final Object[] mConstructorArgs = new Object[2];//View的構造函數的2個"實"參對象
private static final HashMap<String, Constructor<? extends View>> sConstructorMap = new HashMap<String, Constructor<? extends View>>();//用映射,將View的反射構造函數都存起來
static final String[] prefixs = new String[]{//安卓裏面控件的包名,就這麼3種,這個變量是爲了下面代碼裏,反射創建類的class而預備的
"android.widget.",
"android.view.",
"android.webkit."
};
private final View createViewByPrefix(Context context, String name, String[] prefixs, AttributeSet attrs){
Constructor<? extends View> constructor = sConstructorMap.get(name);
Class<? extends View> clazz = null;
if(constructor == null){
try {
if(prefixs != null && prefixs.length > 0){
for(String prefix : prefixs){
clazz = context.getClassLoader().loadClass(prefix != null ? (prefix+name) : name).asSubclass(View.class);
if(clazz != null) break;
}
}else {
if(clazz == null){
clazz = context.getClassLoader().loadClass(name).asSubclass(View.class);
}
}
if(clazz == null){
return null;
}
//拿到構造方法
constructor = clazz.getConstructor(mConstructorSignature);
} catch (ClassNotFoundException e) {
e.printStackTrace();
return null;
} catch (NoSuchMethodException e) {
e.printStackTrace();
return null;
}
constructor.setAccessible(true);
//然後緩存起來,下次再用,就直接從內存中去取
sConstructorMap.put(name, constructor);
}
Object[] args = mConstructorArgs;
try {
args[0] = context;
args[1] = attrs;
//通過反射創建View對象
//執行構造函數,拿到View對下
final View view = constructor.newInstance(args);
return view;
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}finally {
args[0] = null;
args[1] = null;
}
return null;
}
static class SkinView{
View view;
HashMap<String, String> attrsMap;
public void changeSkin(){
if (!TextUtils.isEmpty(attrsMap.get("background"))) {//屬性名,例如,這個background,text,textColor....
int bgId = Integer.parseInt(attrsMap.get("background").substring(1));//屬性值,R.id.XXX ,int類型,
// 這個值,在app的一次運行中,不會發生變化
String attrType = view.getResources().getResourceTypeName(bgId); // 屬性類別:比如 drawable ,color
if (TextUtils.equals(attrType, "drawable")) {//區分drawable和color
view.setBackgroundDrawable(SkinEngine.getInstance().getDrawable(bgId));//加載外部資源管理器,拿到外部資源的drawable
} else if (TextUtils.equals(attrType, "color")) {
view.setBackgroundColor(SkinEngine.getInstance().getColor(bgId));
}
}
if (view instanceof TextView) {
if (!TextUtils.isEmpty(attrsMap.get("textColor"))) {
int textColorId = Integer.parseInt(attrsMap.get("textColor").substring(1));
((TextView) view).setTextColor(SkinEngine.getInstance().getColor(textColorId));
}
if (!TextUtils.isEmpty(attrsMap.get("textSize"))) {
int textSizeId = Integer.valueOf(attrsMap.get("textSize").substring(1));
((TextView) view).setTextSize(SkinEngine.getInstance().getTextSize(textSizeId));
}
}
// //那麼如果是自定義組件呢
// if (view instanceof ZeroView) {
// //那麼這樣一個對象,要換膚,就要寫針對性的方法了,每一個控件需要用什麼樣的方式去換,尤其是那種,自定義的屬性,怎麼去set,
// // 這就對開發人員要求比較高了,而且這個換膚接口還要暴露給 自定義View的開發人員,他們去定義
// // ....
// }
}
}
}
6.2SkinEngine加載插件apk的對應的Resources,(Resources用插件apk的資源)實現更換皮膚的各種方法(例如:getColor等)
/**
* 單例
*/
public class SkinEngine {
private final static SkinEngine instance = new SkinEngine();
public static SkinEngine getInstance(){
return instance;
}
public void init(Context context){
mContext = context.getApplicationContext();
//使用application的目的是,如果萬一傳進來的是Activity對象
//那麼它被靜態對象instance所持有,這個Activity就無法釋放了
}
private Resources mOutResource; //資源管理器
private Context mContext; //上下文
private String mOutPkgName; //外部資源包packageName
//加載外部資源包
public void load(final String path){ //path 是外部傳入的apk文件名
File file = new File(path);
if(!file.exists()){
return;
}
//取得PackageManager引用
PackageManager mPm = mContext.getPackageManager();
//“檢索在包歸檔文件中定義的應用程序包的總體信息”,說人話,外界傳入了一個apk的文件路徑,這個方法,拿到這個apk的包信息,這個包信息包含什麼?
PackageInfo mInfo = mPm.getPackageArchiveInfo(path, PackageManager.GET_ACTIVITIES);
mOutPkgName = mInfo.packageName; //先把包名存起來
AssetManager assetManager;//資源管理器
//關鍵技術點3 通過反射獲取AssetManager 用來加載外面的資源包
try {
//反射創建AssetManager對象,爲何要反射?使用反射,是因爲他這個類內部的addAssetPath方法是hide狀態
assetManager = AssetManager.class.newInstance();
//addAssetPath方法可以加載外部的資源包
//爲什麼要反射執行這個方法?因爲它是hide的,不直接對外開放,只能反射調用
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path); //反射執行方法
mOutResource = new Resources(assetManager, //資源管理器
mContext.getResources().getDisplayMetrics(), //屏幕參數
mContext.getResources().getConfiguration()); //資源配置
//最終創建出一個 "外部資源包"mOutResource ,它的存在,就是要讓我們的app有能力加載外部的資源文件
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
/**
* 提供外部資源包裏面的顏色
* @param resId
* @return
*/
public int getColor(int resId) {
if (mOutResource == null) {
return resId;
}
String resName = mContext.getResources().getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "color", mOutPkgName);
if (outResId == 0) {
return resId;
}
return mOutResource.getColor(outResId);
}
/**
* 提供外部資源包裏的圖片資源
* @param resId
* @return
*/
public Drawable getDrawable(int resId) {//獲取圖片
if (mOutResource == null) {
return ContextCompat.getDrawable(mContext, resId);
}
String resName = mContext.getResources().getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "drawable", mOutPkgName);
if (outResId == 0) {
return ContextCompat.getDrawable(mContext, resId);
}
return mOutResource.getDrawable(outResId);
}
/**
* 提供外部資源包裏的字體大小
* @param resId
* @return
*/
public float getTextSize(int resId) {//獲取字體大小
if (mOutResource == null) {
return mContext.getResources().getDimension(resId);
}
String resName = mContext.getResources().getResourceEntryName(resId);
int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);
if (outResId == 0) {
return mContext.getResources().getDimension(resId);
}
return mOutResource.getDimension(outResId);
}
//..... 這裏還可以提供外部資源包裏的String,font等等等,只不過要手動寫代碼來實現getXX方法
}
注意事項:
通過資源id找到資源名稱,mContext是當前主app的上下文;
String resName = mContext.getResources().getResourceEntryName(resId);
mOutResource指的是插件apk的Resources獲取和主app相同名稱的資源替換顯示;
int outResId = mOutResource.getIdentifier(resName, "textSize", mOutPkgName);
6.3實例使用
初始化"換膚引擎"指定當前上下文
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
//初始化換膚引擎
SkinEngine.getInstance().init(this);
}
}
在基類Activity爲LayoutInflator注入Facotry2類,其他Activity實現基類;
/**
* Activity積類
*/
public class BaseActivity extends AppCompatActivity {
private boolean ifAllowChangeSkin=true;
private SkinFactory mSkinFactory;
private String mCurrentSkin;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
// TODO: 關鍵點1:hook(劫持)系統創建view的過程
if (ifAllowChangeSkin) {
mSkinFactory = new SkinFactory();
mSkinFactory.setDelegate(getDelegate());
LayoutInflater layoutInflater = LayoutInflater.from(this);
layoutInflater.setFactory2(mSkinFactory);//劫持系統源碼邏輯
}
super.onCreate(savedInstanceState);
}
}
在設置界面調用換膚操作, changeSkin("apk插件的名字"),具體apk插件存在哪和如何放入存儲卡加載插件apk,下面僅僅是插件apk下載完成以後實現的方法;
changeSkin("skinmodule-debug.apk");
protected void changeSkin(String path) {
if (ifAllowChangeSkin) {
File skinFile = new File(Environment.getExternalStorageDirectory(), path);
SkinEngine.getInstance().load(skinFile.getAbsolutePath());//加載外部資源包
mSkinFactory.changeSkin();//執行換膚操作
mCurrentSkin = path;
}
}
6.4主app和插件app設置需要換膚資源用相同的名稱
6.5自定義isSupport屬性設置View是否支持換膚
<resources><!--TODO: 關鍵技術點2 通過自定義屬性來標識哪些view支持換膚-->
<declare-styleable name="Skinable">
<!--TODO: isSupport=true標識當前控件支持換膚-->
<attr name="isSupport" format="boolean" />
</declare-styleable>
</resources>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
android:text="第一個"
android:id="@+id/tv_one"
android:gravity="center"
android:textSize="@dimen/tabsize"
app:isSupport="true"
/>
7.顯示效果
以下效果實現動態替換字體大小,背景顏色;
已經創建的Activity界面可能需要監聽器,監聽換膚操作執行換膚,新創建的頁面將使用插件apk皮膚資源;
8.注意事項
a.skinmodule創建module只需要管理assets和res下的資源即可,需要替換資源名稱要和app一致;
b.皮膚包 skin_plugin module的gradle sdk版本最好和app module的保持完全一致,否則無法保證不會出現奇葩問題;
c.skinmodule生成的具體保存位置自己定義,只需要創建插件Resources時執行插件路徑apk即可;
參考:
https://www.jianshu.com/p/4c8d46f58c4f
https://github.com/18598925736/HookSkinDemoFromHank