android 換膚(2)——外掛式無縫換膚(解析鴻洋大神的換膚流程)
上一篇我說到tag式換膚的流程。
小結:
你只需要在每一個需要換膚的activity中註冊SkinManager就可以換膚了,並且在需要換膚的資源中xml或者程式碼中都要設定tag,且這個tag是嚴格按格式來的。
這就是非侵入是換膚:對程式的影響最小,但是很麻煩,你要為每一個需要換膚的資源配置tag。
所以,侵入式換膚解決的就是這個問題。你在也不用註冊SkinManager了,也不用在每個屬性中都加入格式繁瑣的tag了。
對於侵入式來說,你只需要規範的命名你的資源id就ok了。
接下來我們看看侵入式的換膚。
侵入式換膚的SkinManager沒有了註冊和反註冊方法,其他的差不多。這裡就不貼出來了,如果想要了解的,在(1)中已經給出了鴻洋大神的專案地址,自己可以去看。
而侵入式換膚呼叫的SkinAttrSupport中的getSkin方法並不再是“getSkinTags(String tag)”了。而是“getSkinAttrs(AttributeSet attrs, Context context)”。
可以看到,我們只需要傳入view的資源集AttributeSet和上下文就ok。已經脫離了tag。
該方法我在(1)中也貼出來了,這裡就不再貼出來了。
侵入式換膚最主要的方法在於baseSkinactivity中:
這是鴻洋大神的BaseSkinActivity。侵入了onCreateView方法,使用了檢視相容工廠。
這裡面如何侵入的,我只能看看,解釋的可能也有出入。
**
* 如果需要換膚的activity就要繼承該activity
* 該activity實現了ISkinListener介面,可以為換膚提供一個標識
* 實現LayoutInflaterFactory是換膚的主要方法
*/
public class BaseSkinActivity extends AppCompatActivity implements ISkinListener, LayoutInflaterFactory {
//建構函式
private static final Class<?>[] sConstructorSignature = new Class[]{Context.class, AttributeSet.class};
//建構函式的集合
private static final Map<String, Constructor<? extends View>> sConstructorMap = new ArrayMap<>();
//建構函式引數,長度為2
private final Object[] mConstructorArgs = new Object[2];
//供反射使用的方法例項,建立view的方法
private Method sCreateViewMethod;
//該方法參照onCreateView,class中傳入的引數是和onCreateView的引數一樣。
private static final Class<?>[] sCreateViewSignature = new Class[]{View.class, String.class, Context.class, AttributeSet.class};
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//這一段程式碼在通過反射得到該activity建立的時候的view例項,app相容委託例項,即當前activity的例項
AppCompatDelegate delegate = getDelegate();
View view = null;
try {
if (sCreateViewMethod == null)
//反射createView方法,並傳入相應的引數
sCreateViewMethod = delegate.getClass().getMethod("createView", sCreateViewSignature);
//傳入該方法所在類的類例項,以及引數
view = (View) sCreateViewMethod.invoke(delegate, parent, name, context, attrs);
} catch (InvocationTargetException e) {
//目標呼叫異常
e.printStackTrace();
} catch (IllegalAccessException e) {
//非法訪問異常
e.printStackTrace();
} catch (NoSuchMethodException e) {
//沒有這樣的方法異常
e.printStackTrace();
}
//通過傳入該activity的屬性和上下文,得到這個activity所持有的所有屬性例項
List<SkinAttr> skinAttrList = SkinAttrSupport.getSkinAttrs(attrs, context);
//如果這個activity裡面沒有使用資源則直接返回view
if (skinAttrList.isEmpty()) {
return view;
}
//如果通過反射沒有得到view則手動建立一個
if (view == null) {
//將該activity的引數傳入
view = createViewFromTag(context, name, attrs);
}
//給當前activity注入面板
injectSkin(view, skinAttrList);
return view;
}
//通過標籤來建立一個view
private View createViewFromTag(Context context, String name, AttributeSet attrs) {
if (name.equals("view"))
//根據屬性名稱來查詢並返回一個指定的屬性值
name = attrs.getAttributeValue(null, "class");
try {
mConstructorArgs[0] = context;
mConstructorArgs[1] = attrs;
if (-1 == name.indexOf('.'))//如果屬性值中不包含.則手動建立一個字首;若包含傳null
return createView(context, name, "android.widget.");
else
return createView(context, name, null);
} catch (Exception e) {
return null;
} finally {
// 最後將上下文清空,不然會造成記憶體洩漏
mConstructorArgs[0] = null;
mConstructorArgs[1] = null;
}
}
//建立檢視
private 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(!TextUtils.isEmpty(prefix) ? (prefix + name) : name).asSubclass(View.class);
//獲得該view建構函式集合
constructor = clazz.getConstructor(sConstructorSignature);
//將建構函式傳入map集合
sConstructorMap.put(name, constructor);
}
//使用反射機制可以打破封裝性,導致了java物件的屬性不安全。設定可訪問性
constructor.setAccessible(true);
//傳入構造方法作為引數,獲得物件並返回
return constructor.newInstance(mConstructorArgs);
} catch (Exception e) {
return null;
}
}
//傳入當前activity的view和所有的屬性資源,注入面板
private void injectSkin(View view, List<SkinAttr> skinAttrList) {
if (skinAttrList.size() > 0) {
//得到activity中儲存的面板view集合,如果是第一次來獲取
List<SkinView> skinViews = SkinManager.getInstance().getSkinViews(this);
if (skinViews == null)
skinViews = new ArrayList<>();
//把所有的屬性資源和當前activity傳入skinView中,並新增到skinview的集合中
skinViews.add(new SkinView(view, skinAttrList));
SkinManager.getInstance().addSkinView(this, skinViews);
//如果可以執行換膚,則執行;
if (SkinManager.getInstance().needChangeSkin()) {
//換膚
SkinManager.getInstance().apply(this);
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
//將該類新增進檢視相容工廠
LayoutInflaterCompat.setFactory(LayoutInflater.from(this), this);
super.onCreate(savedInstanceState);
//在activity建立的時候新增換膚的監聽
SkinManager.getInstance().addChangedListener(this);
}
/**
* 當程式要求app修改面板的時候就會呼叫這個方法
* 該方法裡面寫實時的修改面板的方法
*/
@Override
public void onSkinChanged() {
//因為在onCreateView方法中已經將引數聲明瞭,所以這裡直接呼叫
SkinManager.getInstance().apply(this);
}
/**
* 當activity被finish的時候我們從換膚集合中移除這個activity,下一次換膚就不會對該activity產生作用了
*/
@Override
protected void onDestroy() {
super.onDestroy();
SkinManager.getInstance().removeChangedListener(this);
}
}
經過我苦逼的註釋後,我發現大神的程式碼果真非同凡響。侵入式方法雖然減少了很多工作量,但是帶來了一個很重要的問題。
如果我們在侵入onCreateView方法的時候出錯了,,,,,那我們就很那解決問題了。
不要認為這個方法不會出bug,鴻洋大神在微博上都說過還要測試bug。。。
而且侵入式方法的程式碼並不美觀,可讀性並不高。如果讓我選擇,我還是寧願麻煩一點,選擇tag換膚,這樣程式碼清晰,邏輯易懂,修改也相對容易了,
當然,可能以後會有更好的方法來代替tag換膚。
侵入式方法雖好可使用需謹慎。
不過這也是我們值得向大神學習的地方,一個新思路成就一種輪子。不管這個方法可取不可取,這種思想是非常值得學習的!