關於三種『應用內主題切換』開源項目的一點思考

這裏討論的只是白天、夜晚主題切換這種場景,不涉及外部資源加載。

現在要給App添加夜晚主題,所以就需要選擇一種應用內部更換主題的實現方案,目前來說,比較常見的幾種方式如下:

Theme

設置Theme來切換不同主題。

優點:利用系統自帶的機制實現,根據標誌位setTheme()即可。

缺點:在主題切換界面不重啓的情況下,不能自動完成界面主題的刷新。

遍歷View

對主題的更換,使用遍歷View,然後單獨設置更改後的屬性即可。

優點:可以即時更新界面,不需要重啓Activity

缺點:需要單獨添加標誌位,來標記需要更換主題的View,需要增加額外工作,另外就是標記的添加,有可能影響原來的代碼邏輯。

開源項目

關於Theme的解決方案就不說了,就是在style文件中定義不同的主題即可。

目前開源的幾個應用內換膚項目,基本採用的都是遍歷View,然後更換屬性來完成,下面我們簡單分析一下實現機制。

MultipleTheme

這個項目的實現方案比較好理解,採用的是Theme+遍歷更新View的思路。

public class BaseActivity extends Activity{

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if(SharedPreferencesMgr.getInt("theme", 0) == 1) {
            setTheme(R.style.theme_2);
        } else {
            setTheme(R.style.theme_1);
        }
    }
}

首先在基類裏面,根據當前本地保存的標誌位來設置Theme,這樣就能保證新打開的Activity的主題都是正確的。

其次,在主動更換主題的時候,需要調用下面的方法

 ColorUiUtil.changeTheme(rootView, getTheme());

而這個方法的實質,就是遍歷rootView裏面所有的View,如果View實現了ColorUiInterface接口,則調用setTheme()來更換View的對應屬性

public interface ColorUiInterface {

    public View getView();

    public void setTheme(Resources.Theme themeId);
}

爲此,作者實現了一系列的自定義類,來實現ColorUiInterface接口,所以如果你要用的話,需要把所有更換主題的View替換,這顯然是一種成本非常高的方案。

而且就目前來說,Demo裏面存在BUG,點擊切換皮膚之後,Button的字體顏色換了,但是背景顏色卻消失了,同時這個項目已經4個月沒有維護。

所以,由上述可以得出結論:此項目不可商用,推薦指數:★

Colorful

Colorful與上面一種方案總體思想是相通的,但是在具體實現細節上各有特色。

首先在需要更換主題View的篩選上,上面的方案用的是是否實現某接口來識別,而在Colorful中則是需要用戶手動綁定,建立需要更換的View與屬性之間關係,雖然在編碼上面需要花費一些時間,但是這樣就不需要替換所有的View,在總體上是優於前一種方案。

ViewGroupSetter listViewSetter = new ViewGroupSetter(mNewsListView);
        // 綁定ListView的Item View中的news_title視圖,在換膚時修改它的text_color屬性
        listViewSetter.childViewTextColor(R.id.news_title, R.attr.text_color);

        // 構建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(); // 設置文本顏色

在綁定View與屬性之後,可以調用下面方法完成更換主題

private void changeThemeWithColorful() {
        if (!isNight) {
            mColorful.setTheme(R.style.NightTheme);
        } else {
            mColorful.setTheme(R.style.DayTheme);
        }
        isNight = !isNight;
    }

在這之後,做的事情就和MultipleTheme沒有太大差別了,首先更改Activity的Theme,但是因爲onCreate()已調用,所以這個時候Theme改變了,但是界面是沒有變化的,就需要手動去遍歷更新所有需要改變的View的屬性。

        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);
            }
        }

獲取Theme對應的顏色使用

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

獲取Theme對應屬性的

綜上所述,使用這個方案,對佈局代碼的修改較小,而且由於是手動指定View,所以不需要遍歷,效率上會好一些。但是需要在Activity中添加綁定代碼,如果要改變的View比較多的話,代碼量會比較多。推薦指數:★★★

AndroidChangeSkin

AndroidChangeSkin這個庫不單單可以完成應用內資源的替換,還可以完成外部apk資源包的主題加載,但是這裏只討論使用內部資源的情況。

首先我們看一下AndroidChangeSkin是怎麼實現變換主題View的標記的呢?

通過android:tag。

比如,你想替換ImageView的src屬性,那就可以下面這樣,在運行時,會通過解析tag字符串,將『skin:left_menu_icon:src』拆分,skin代表需要換膚,left_menu_icon代表需要替換的資源名稱,src代表了要更換的屬性名稱。

<ImageView
                android:src="@drawable/left_menu_icon"
                android:tag="skin:left_menu_icon:src" />

要更換TextView文字顏色則需要這樣。

 <TextView
                android:tag="skin:menu_item_text_color:textColor"
                android:text="恢復默認"
                android:textColor="@color/menu_item_text_color" />

通過這種方式標記View的好處是,不需要代碼中手動標記,也不需要用接口標記,但是同時也有一個弊端,那就是view.setTag()方法就不能夠使用了,因爲這個框架需要這個標誌位進行區分。

AndroidChangeSkin的應用內換膚使用的是添加後綴的方式,比如上面的android:textColor="@color/menu_item_text_color",如果要更換主題,需要預先定義好主體顏色,不同主題後綴不同,像下面這樣就有三種主題,默認主題,red主題,green主題。

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="menu_item_text_color">#ffffffff</color>

    <!--應用內換膚資源-->
    <color name="menu_item_text_color_red">#ff0000</color>

    <color name="menu_item_text_color_green">#00ff00</color>

</resources

AndroidChangeSkin的使用是比較舒服的,首先是在xml文件裏面設置好tag屬性,然後在Activity裏面註冊需要主題的Activity即可

 @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        SkinManager.getInstance().register(this);
        setContentView(R.layout.activity_main);
    }

其實上面方式執行的時候,就對當前所有的View進行了一次遍歷,然後根據當前的主題後綴,設置了對應的資源。

有人可能會問了,在setContentView()之前,怎麼可能遍歷View呢?實際上內部是這樣處理的

 public void register(final Activity activity) {
        mActivities.add(activity);
        activity.findViewById(android.R.id.content).post(new Runnable() {
            @Override
            public void run() {
                apply(activity);
            }
        });
    }

也就是說,這個方法只是把apply(activity)添加到了消息隊列中,等整個界面加載完畢,消息隊列開始輪詢的時候,這個消息纔會被處理,這樣就能夠在界面加載完之後,立刻遍歷設置對應屬性,是一種懶加載策略,而且時機選擇的恰到好處。

但是這裏就出現了一個問題,就是每次進入界面都需要遍歷所有的View,在性能上肯定不是最優,但是使用這種方案,遍歷貌似是不可避免的操作。

因爲AndroidChangeSkin內部會通過SP來保存當前的主題,所以每次切換完主題,退出再進入的時候,會顯示已經切換好的主題,這一點也是通過上面的register()完成的。

在onDestory()的時候,不要忘記反註冊,防止內存泄露

@Override
    protected void onDestroy() {
        super.onDestroy();
        SkinManager.getInstance().unregister(this);
    }

綜上所述,AndroidChangeSkin使用簡單,也很好理解,但是存在兩個問題:一個是tag被廢掉了,如果你的代碼裏面用到了tag,那麼就要好好想一下了;(雖然在標籤中使用了tag,但是在後面會將tag更換到其他key對應的tag中,一般不會影響代碼中tag的使用,除非你的tag對應的key和標記爲tag的key完全一樣,這樣的概率是非常小的)另外一個就是無論是否需要切換主題,每次進入Activity的時候,都會遍歷一次View,對於view比較多的界面,會有性能上的影響。

下面代碼會將原先的tag替換爲skin_tag_id作爲key對應的tag中,默認無參tag不受影響,多謝AndroidChangeSkin作者鴻洋指出!

private static void changeViewTag(View view) {
        Object tag = view.getTag(R.id.skin_tag_id);
        if (tag == null) {
            tag = view.getTag();
            view.setTag(R.id.skin_tag_id, tag);
            view.setTag(null);
        }
    }

所以,推薦指數:★★★★★

我的思考

從上面這幾個開源項目來看,實現思路中,主要有兩個要解決的問題:

  1. 如何標記要更換主題的View
  2. 如何在Activity不銷燬的狀況下,更新當前界面

對於第一個問題,可以實現接口、手動指定、tag區分,後兩種一個在效率上會好一些,一個在使用上方便一些,所以各有優點。

而對於第二個問題,則基本都一樣,遍歷標記的View集合,然後設置對應屬性。

前兩種方案,在設置屬性的時候,用的是theme,不同Theme對應的資源不同,而後一種則是直接使用的資源名稱,通過添加後綴的方式,來實現不同的資源加載。

同時,前兩種方案需要自定義attrs,然後xml中引用,但是在預覽中是看不到預覽效果的,因爲attrs對應的資源id未指定,所以在開發時多少有些不方便,而後一種實現則沒有這個問題。

所以,我個人比較喜歡AndroidChangeSkin的實現。但是怎麼避免每次進入都需要遍歷View帶來的性能損耗呢?

我的想法是,在切換主題開關的界面使用AndroidChangeSkin,這樣切換之後可以實時更新,但是在其他新開啓的界面,使用Theme,通過本地標誌位來setTheme(),這樣既能完成需求,又不會造成額外的性能損耗。

參考文章

關於我

博主正在參加2015CSDN博客之星活動,如果文章對你有幫助,希望可以投我一票,謝謝支持!點擊投票

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章