1. 程式人生 > >android 換膚(2)——外掛式無縫換膚(解析鴻洋大神的換膚流程)

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換膚。
侵入式方法雖好可使用需謹慎。
不過這也是我們值得向大神學習的地方,一個新思路成就一種輪子。不管這個方法可取不可取,這種思想是非常值得學習的!