Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android開源換膚解析

Android開源換膚解析

編輯:關於Android編程

在上一篇Android小結博客中我們簡要的介紹了常見的換膚手法,換膚可以貫穿在整個Android開發中,所以必然可以做成一個框架集的形式。下面我們就來看看一些Android換膚開源組件。

一、Colorful基於Theme的換膚

Colorful是一個基於Theme實現的開源控件,作者是mr_simple。Colorful無需重啟Activity、無需自定義View,方便的實現日間、夜間模式。在上篇文章中,我們介紹了基於Theme的切換都需要重新OnCreate Activity。但是Colorful卻不需要重啟Activity,下面就讓我們一睹風采。

1、Colorful的使用:

基於Theme的換膚,就需要我們定義好我們的屬性和Theme屬性,這些都是必備的。使用Colorful:

Colorful mColorful;
// 構建Colorful對象來綁定View與屬性的對象關系
mColorful = new Colorful.Builder(this)
        .backgroundDrawable(R.id.root_view, R.attr.root_view_bg)
        // 設置view的背景圖片
        .backgroundColor(R.id.change_btn, R.attr.btn_bg)
        // 設置背景色
        .textColor(R.id.textview, R.attr.text_color)
        .setter(listViewSetter) // 手動設置setter
        .create(); // 設置文本顏色
mColorful.setTheme(R.style.NightTheme)

這就是簡單的使用。

2、源碼研究:

我們查看下Colorful這個控制類:

public final class Colorful {
    /**
     * Colorful Builder
     */
    Builder mBuilder;

    /**
     * private constructor
     * 
     * @param builder
     */
    private Colorful(Builder builder) {
        mBuilder = builder;
    }

    /**
     * 設置新的主題
     * 
     * @param newTheme
     */
    public void setTheme(int newTheme) {
        mBuilder.setTheme(newTheme);
    }

    /**
     * 
     * 構建Colorful的Builder對象
     * 
     * @author mrsimple
     * 
     */
    public static class Builder {
        /**
         * 存儲了視圖和屬性資源id的關系表
         */
        Set mElements = new HashSet();
        /**
         * 目標Activity
         */
        Activity mActivity;

        /**
         * @param activity
         */
        public Builder(Activity activity) {
            mActivity = activity;
        }

        /**
         * @param fragment
         */
        public Builder(Fragment fragment) {
            mActivity = fragment.getActivity();
        }

        private View findViewById(int viewId) {
            return mActivity.findViewById(viewId);
        }

        /**
         * 將View id與存儲該view背景色的屬性進行綁定
         * @param viewId
         *            控件id
         * @param colorId
         *            顏色屬性id
         * @return
         */
        public Builder backgroundColor(int viewId, int colorId) {
            mElements.add(new ViewBackgroundColorSetter(findViewById(viewId),
                    colorId));
            return this;
        }

        /**
         * 將View id與存儲該view背景Drawable的屬性進行綁定
         * @param viewId
         *            控件id
         * @param colorId
         *            Drawable屬性id
         * @return
         */
        public Builder backgroundDrawable(int viewId, int drawableId) {
            mElements.add(new ViewBackgroundDrawableSetter(
                    findViewById(viewId), drawableId));
            return this;
        }

        /**
         * 將TextView id與存儲該TextView文本顏色的屬性進行綁定
         * @param viewId
         *            TextView或者TextView子類控件的id
         * @param colorId
         *            顏色屬性id
         * @return
         */
        public Builder textColor(int viewId, int colorId) {
            TextView textView = (TextView) findViewById(viewId);
            mElements.add(new TextColorSetter(textView, colorId));
            return this;
        }

        /**
         * 用戶手動構造並且添加Setter
         * @param setter
         *            用戶自定義的Setter
         * @return
         */
        public Builder setter(ViewSetter setter) {
            mElements.add(setter);
            return this;
        }

        /**
         * 設置新的主題
         * @param newTheme
         */
        protected void setTheme(int newTheme) {
            mActivity.setTheme(newTheme);
            makeChange(newTheme);
        }

        /**
         * 修改各個視圖綁定的屬性
         */
        private void makeChange(int themeId) {
            Theme curTheme = mActivity.getTheme();
            for (ViewSetter setter : mElements) {
                setter.setValue(curTheme, themeId);
            }
        }

        /**
         * 創建Colorful對象
         * @return
         */
        public Colorful create() {
            return new Colorful(this);
        }
    }
}

(1)、從Color的結構定義中可以看到,Colorful的構造函數是一個私有的構造函數,即作者不希望我們直接創建一個Colorful的對象,這裡作者引入一個staitc Builder類,用於構造控件和屬性的對應關系。

(2)、Builder類源碼結構解析: 在Builder源碼類中,我們可以看到裡面包含一個

/**
 * 存儲了視圖和屬性資源id的關系表
 */
Set mElements = new HashSet();

這就是用於存儲我們的控件和屬性值,然後通過

  1. public Builder backgroundColor(int viewId, int colorId):將View id與存儲該view背景色的屬性進行綁定
  2. public Builder backgroundDrawable(int viewId, int drawableId):將View id與存儲該view背景Drawable的屬性進行綁定
  3. public Builder textColor(int viewId, int colorId):將TextView id與存儲該TextView文本顏色的屬性進行綁定

通過這三個方法綁定控件的id和控件綁定的自定義屬性id,然後在方法的內部,構建ViewSetter對象存儲到Set集合。

當我們建立好這種控件和屬性的對應關系,調用setTheme(id)進行Theme的主題設置,然後該方法調用

/**
 * 修改各個視圖綁定的屬性
 */
private void makeChange(int themeId) {
    Theme curTheme = mActivity.getTheme();
    for (ViewSetter setter : mElements) {
        setter.setValue(curTheme, themeId);
    }
} 

我們可以看到該方法裡面是遍歷Set集合,然後調用ViewSetter的setValue設置值進行修改。總體的思路設計就是這樣。

(3)、上面我們看到了ViewSetter類。該類封裝了我們的控件id和屬性id。

/**
 * ViewSetter,用於通過{@see #mAttrResId}
 * 設置View的某個屬性值,例如背景Drawable、背景色、文本顏色等。如需修改其他屬性,可以自行擴展ViewSetter.
 * 
 * @author mrsimple
 * 
 */
public abstract class ViewSetter {

    /**
     * 目標View
     */
    protected View mView;
    /**
     * 目標view id,有時在初始化時還未構建該視圖,比如ListView的Item View中的某個控件
     */
    protected int mViewId;
    /**
     * 目標View要的特定屬性id
     */
    protected int mAttrResId;

    public ViewSetter(View targetView, int resId) {
        mView = targetView;
        mAttrResId = resId;
    }

    public ViewSetter(int viewId, int resId) {
        mViewId = viewId;
        mAttrResId = resId;
    }

    /**
     * 
     * @param newTheme
     * @param themeId
     */
    public abstract void setValue(Theme newTheme, int themeId);

    /**
     * 獲取視圖的Id
     * 
     * @return
     */
    protected int getViewId() {
        return mView != null ? mView.getId() : -1;
    }

    protected boolean isViewNotFound() {
        return mView == null;
    }

    /**
     * 
     * @param newTheme
     * @param resId
     * @return
     */
    protected int getColor(Theme newTheme) {
        TypedValue typedValue = new TypedValue();
        newTheme.resolveAttribute(mAttrResId, typedValue, true);
        return typedValue.data;
    }
}

這裡主要就是看getColor方法,通過resolveAttibute方法獲取Theme裡面的屬性值,然後設置到具體的控件上。這樣就直接設置上了,實現了換膚,並且不需要重啟Activity。比如最直接的例子,TextColorSetter:

public class TextColorSetter extends ViewSetter {

    public TextColorSetter(TextView textView, int resId) {
        super(textView, resId);
    }

    public TextColorSetter(int viewId, int resId) {
        super(viewId, resId);
    }

    @Override
    public void setValue(Theme newTheme, int themeId) {
        if (mView == null) {
            return;
        }
        ((TextView) mView).setTextColor(getColor(newTheme));
    }
}

這裡的setValue方法就是直接設置值。

3、總評

總體來說,Colorful基於Theme依然沒有避免掉Theme的弊病,原理是將Thme中的屬性值獲取出來,設置到對應的控件上。而且現在只有setColor方法,即只支持獲取Color的屬性值設置,其他的圖片之類的還沒有豐富。總體功能還不是很完善。

二、Android-Skin-Loader

Android-Skin-Loader是一個比較優秀的換膚框架,地址android-sgik-loader,該工程的核心

Android-Skin-Loader ├── android-skin-loader-lib // 皮膚加載庫 ├── android-skin-loader-sample // 皮膚庫應用實例 ├── android-skin-loader-skin // 皮膚包生成demo └── skin-package // 皮膚包輸出目錄

通過這個框架可以對控件進行隨意換膚。

在這個框架中,使用了自定義的Factory對象來完成換膚的操作,這點熟悉LayoutInflater的都知道,這個用於初始化我們的View,具體的我們可以參照Android 探究 LayoutInflater setFactory文章學習下。 換膚需要解決的核心問題有兩個: (1)、外部資源的加載 (2)、定位到需要換膚的View 第一個資源加載的問題可以通過構造AssetManager,反射調用其addAssetPath就可以完成。

第二個問題,就可以利用在onCreateView中,根據view的屬性來定位,例如你可以讓需要換膚的view添加一個自定義的屬性skin_enabled=true(最開始有打印屬性),並且利用一些手段拿到構造到的view,就能在View構造階段定位的需要換膚的View。

在Android-Skin-Loader中,你需要集成封裝的BaseActivity或BaseFragment來進行實現,我們看一下BaseActivity的源碼。

/**
 * Base Activity for development
 * 
 * 
NOTICE:
* You should extends from this if you what to do skin change * * @author fengjun */ public class BaseActivity extends Activity implements ISkinUpdate, IDynamicNewView{ /** * Whether response to skin changing after create */ private boolean isResponseOnSkinChanging = true; private SkinInflaterFactory mSkinInflaterFactory; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mSkinInflaterFactory = new SkinInflaterFactory(); getLayoutInflater().setFactory(mSkinInflaterFactory); } @Override protected void onResume() { super.onResume(); SkinManager.getInstance().attach(this); } @Override protected void onDestroy() { super.onDestroy(); SkinManager.getInstance().detach(this); mSkinInflaterFactory.clean(); } /** * dynamic add a skin view * * @param view * @param attrName * @param attrValueResId */ protected void dynamicAddSkinEnableView(View view, String attrName, int attrValueResId){ mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, attrName, attrValueResId); } protected void dynamicAddSkinEnableView(View view, List pDAttrs){ mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs); } final protected void enableResponseOnSkinChanging(boolean enable){ isResponseOnSkinChanging = enable; } @Override public void onThemeUpdate() { if(!isResponseOnSkinChanging){ return; } mSkinInflaterFactory.applySkin(); } @Override public void dynamicAddView(View view, List pDAttrs) { mSkinInflaterFactory.dynamicAddSkinEnableView(this, view, pDAttrs); } }

通過源碼,我們可以知道,我們需要換膚的界面需要集成BaseActivity才行。在BaseActivity中封裝了自定義的SkinInflaterFactory對象,同時實現了ISkinUpdate、IDynamicNewView兩個接口,用於回調換膚的過程以及添加換膚的View。在BaseActivity中封裝了dynamicAddSkinEnableView、enableResponseOnSkinChanging兩個方法,用於添加添加換膚的View以及設置是否可用換膚。方法實現就是通過SkinInflaterFactory中封裝的方法完成需要換膚View的封裝,然後實現換膚。

在Android-Skin-Loader中的核心類就是SkinInflaterFactory類,該類定義了實現換膚的方法。下面就看看該類的源碼實現:

/**
 * Supply {@link SkinInflaterFactory} to be called when inflating from a LayoutInflater.
 * 
 * 
Use this to collect the {skin:enable="true|false"} views availabled in our XML layout files. * * @author fengjun */ public class SkinInflaterFactory implements Factory { private static final boolean DEBUG = true; /** * Store the view item that need skin changing in the activity */ private List mSkinItems = new ArrayList(); @Override public View onCreateView(String name, Context context, AttributeSet attrs) { // if this is NOT enable to be skined , simplly skip it boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false); if (!isSkinEnable){ return null; } View view = createView(context, name, attrs); if (view == null){ return null; } parseSkinAttr(context, attrs, view); return view; } /** * Invoke low-level function for instantiating a view by name. This attempts to * instantiate a view class of the given name found in this * LayoutInflater's ClassLoader. * * @param context * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * * @return View The newly instantiated view, or null. */ private View createView(Context context, String name, AttributeSet attrs) { View view = null; try { if (-1 == name.indexOf('.')){ if ("View".equals(name)) { view = LayoutInflater.from(context).createView(name, "android.view.", attrs); } if (view == null) { view = LayoutInflater.from(context).createView(name, "android.widget.", attrs); } if (view == null) { view = LayoutInflater.from(context).createView(name, "android.webkit.", attrs); } }else { view = LayoutInflater.from(context).createView(name, null, attrs); } L.i("about to create " + name); } catch (Exception e) { L.e("error while create 【" + name + "】 : " + e.getMessage()); view = null; } return view; } /** * Collect skin able tag such as background , textColor and so on * * @param context * @param attrs * @param view */ private void parseSkinAttr(Context context, AttributeSet attrs, View view) { List viewAttrs = new ArrayList(); for (int i = 0; i < attrs.getAttributeCount(); i++){ String attrName = attrs.getAttributeName(i); String attrValue = attrs.getAttributeValue(i); if(!AttrFactory.isSupportedAttr(attrName)){ continue; } if(attrValue.startsWith("@")){ try { int id = Integer.parseInt(attrValue.substring(1)); String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); if (mSkinAttr != null) { viewAttrs.add(mSkinAttr); } } catch (NumberFormatException e) { e.printStackTrace(); } catch (NotFoundException e) { e.printStackTrace(); } } } if(!ListUtils.isEmpty(viewAttrs)){ SkinItem skinItem = new SkinItem(); skinItem.view = view; skinItem.attrs = viewAttrs; mSkinItems.add(skinItem); if(SkinManager.getInstance().isExternalSkin()){ skinItem.apply(); } } } public void applySkin(){ if(ListUtils.isEmpty(mSkinItems)){ return; } for(SkinItem si : mSkinItems){ if(si.view == null){ continue; } si.apply(); } } public void dynamicAddSkinEnableView(Context context, View view, List pDAttrs){ List viewAttrs = new ArrayList(); SkinItem skinItem = new SkinItem(); skinItem.view = view; for(DynamicAttr dAttr : pDAttrs){ int id = dAttr.refResId; String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); SkinAttr mSkinAttr = AttrFactory.get(dAttr.attrName, id, entryName, typeName); viewAttrs.add(mSkinAttr); } skinItem.attrs = viewAttrs; addSkinView(skinItem); } public void dynamicAddSkinEnableView(Context context, View view, String attrName, int attrValueResId){ int id = attrValueResId; String entryName = context.getResources().getResourceEntryName(id); String typeName = context.getResources().getResourceTypeName(id); SkinAttr mSkinAttr = AttrFactory.get(attrName, id, entryName, typeName); SkinItem skinItem = new SkinItem(); skinItem.view = view; List viewAttrs = new ArrayList(); viewAttrs.add(mSkinAttr); skinItem.attrs = viewAttrs; addSkinView(skinItem); } public void addSkinView(SkinItem item){ mSkinItems.add(item); } public void clean(){ if(ListUtils.isEmpty(mSkinItems)){ return; } for(SkinItem si : mSkinItems){ if(si.view == null){ continue; } si.clean(); } } }

通過自定義的Factory實現自定義的View的解析,通過自定義屬性skin:enable="true|false"來進行設置控件是否能夠進行換膚。我們可以看到onCreateView的實現。

boolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);

在這裡通過AttributeSet來獲取控件的自定義屬性(skin:enable)的值。如果isSkinEnable為false,即不是換膚標識的View,直接return null。如果是標識位換膚的控件,則通過createView()方法創建View。然後根據name的類型進行判斷創建View的種類。最後通過parseSkinAttr()方法遍歷View的屬性進行搜集。在這裡補充下一個屬性:private List< SkinItem> mSkinItems,這個成員變量用於記錄存儲的View。這裡使用了SkinItem對象,該對象用於封裝View以及對應的屬性。

public class SkinItem {

    public View view;

    public List attrs;

    public SkinItem(){
        attrs = new ArrayList();
    }

    public void apply(){
        if(ListUtils.isEmpty(attrs)){
            return;
        }
        for(SkinAttr at : attrs){
            at.apply(view);
        }
    }

    public void clean(){
        if(ListUtils.isEmpty(attrs)){
            return;
        }
        for(SkinAttr at : attrs){
            at = null;
        }
    }

    @Override
    public String toString() {
        return "SkinItem [view=" + view.getClass().getSimpleName() + ", attrs=" + attrs + "]";
    }
}

這裡SkinItem封裝了View以及對應的attrs屬性,同時定義了apply()、clean()兩個方法,用於啟動和清除。然後通過parseSkinAttr()方法遍歷View的屬性,通過SkinItem進行存儲。在SkinInflaterFactory類中,通過dynamicAddSkinEnableView、addSkinView來添加View視圖。然後通過調用applySkin()方法進行換膚。

public void applySkin(){
    if(ListUtils.isEmpty(mSkinItems)){
        return;
    }

    for(SkinItem si : mSkinItems){
        if(si.view == null){
            continue;
        }
        si.apply();
    }
}

在applySkin()中通過遍歷SkinItem來完成換膚,skinItem.apply方法的本質又是什麼呢?

public void apply(){
    if(ListUtils.isEmpty(attrs)){
        return;
    }
    for(SkinAttr at : attrs){
        at.apply(view);
    }
}

在這個方法中又出現了一個SkinAttr類,然後本質就是針對屬性進行設置。在這裡,有BackgroundAttr、DividerAttr等幾個子類,進行實現。

總體來說,Android-Skin-Loader就是通過自定義Factory來實現換膚的操作。

通過上面的例子,我們了解了整體的換膚實現方式,主要就是采用資源的實現方式。以及針對View進行換膚操作的設置。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved