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

[TOC]

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

現在要給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博客之星活動,如果文章對你有幫助,希望可以投我一票,謝謝支持!點擊投票

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,985評論 25 708
  • ¥開啟¥ 【iAPP實現進入界面執行逐一顯】 〖2017-08-25 15:22:14〗 《//首先開一個線程,因...
    小菜c閱讀 6,508評論 0 17
  • 1.什么是Activity?問的不太多,說點有深度的 四大組件之一,一般的,一個用戶交互界面對應一個activit...
    JoonyLee閱讀 5,754評論 2 51
  • 她看車窗外有綠樹和天,夏天天暗得晚,流云泛著橘色,像是一道漩渦在遙遠的天邊轉著,中間的眼兒放著極大的光芒,也是橘色...
    貽我彤管閱讀 246評論 0 0
  • 空間結構非線性整體穩定性分析時,需要分兩種情況進行考慮:一種是考慮幾何非線性和初始幾何缺陷,按彈性材料進行全過程分...
    千山萬水閱讀 8,021評論 1 9