Android项目实战之:无需重启应用和当前页面实现一键换肤

前言

Android应用中每个页面都有自己的主题风格,而主题样式可以在Style.xml里面自定义。自然就可以在这里面做文章,并且便于管理,本篇我们主要讲解下开源换肤框架MultipleTheme的使用,助你轻松实现换肤需求。
GitHub地址:https://github.com/dersoncheng/MultipleTheme

使用步骤

1,首先在attrs.xml里面定义属性:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <attr name="main_bg" format="reference|color"/>
    <attr name="main_textcolor" format="reference|color"/>
    <attr name="second_bg" format="reference|color"/>
    <attr name="second_textcolor" format="reference|color"/>
</resources>

2,然后在style.xml里面设置相应的属性值:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <style name="theme_1" >
        <item name="main_bg">@color/bg_main_normal</item>
        <item name="main_textcolor">@color/textcolor_main_normal</item>
        <item name="second_bg">@color/bg_second_normal</item>
        <item name="second_textcolor">@color/textcolor_second_normal</item>
    </style>

    <style name="theme_2">
        <item name="main_bg">@color/bg_main_dark</item>
        <item name="main_textcolor">@color/textcolor_main_dark</item>
        <item name="second_bg">@color/bg_second_dark</item>
        <item name="second_textcolor">@color/textcolor_second_dark</item>
    </style>
</resources>

3,在color.xml中定义相应的颜色值:

<?xml version="1.0" encoding="utf-8"?>
<resources>
    <color name="bg_main_normal">#ffffff</color>
    <color name="textcolor_main_normal">#ff0000</color>
    <color name="bg_main_dark">#000000</color>
    <color name="textcolor_main_dark">#ffffff</color>
    <color name="bg_second_normal">#0000ff</color>
    <color name="textcolor_second_normal">#00ff00</color>
    <color name="bg_second_dark">#ffffff</color>
    <color name="textcolor_second_dark">#000000</color>
</resources>

经过以上三步我们就已经成功创建了两套主题,接下来关键是怎么应用这两套主题:
1,在BaseActivity的oncreate()创建Activity实例时,根据SharedPreference拿到当前应该显示什么主题,并setTheme(),然后布局文件各元素会自定获取Style.xml定义好的属性进行展示;

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

2,相应的布局文件如下:
针对切换主题模式时需要立即更新页面ui的页面,需要使用框架里的封装控件,修改相应属性的值为”?attr/xx”。

<derson.com.multipletheme.colorUi.widget.ColorRelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/main_bg"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin">

    <derson.com.multipletheme.colorUi.widget.ColorTextView
        android:textColor="?attr/main_textcolor"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/hello_world" />

    <derson.com.multipletheme.colorUi.widget.ColorButton
        android:id="@+id/btn"
        android:text="换肤"
        android:layout_centerInParent="true"
        android:textColor="?attr/main_textcolor"
        android:layout_width="100dip"
        android:layout_height="80dip" />

    <derson.com.multipletheme.colorUi.widget.ColorButton
        android:id="@+id/btn_2"
        android:layout_centerHorizontal="true"
        android:text="下一页"
        android:layout_below="@id/btn"
        android:layout_marginTop="30dip"
        android:textColor="?attr/main_textcolor"
        android:layout_width="100dip"
        android:layout_height="80dip" />

</derson.com.multipletheme.colorUi.widget.ColorRelativeLayout>

3,MainActivity代码,其中SharedPreferencesMgr是SharedPreferences保存数据的工具类:

public class MainActivity extends BaseActivity {

    ColorButton btn,btn_next;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn = (ColorButton)findViewById(R.id.btn);
        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // 根据主题标志设置相应主题
                if(SharedPreferencesMgr.getInt("theme", 0) == 1) {
                    SharedPreferencesMgr.setInt("theme", 0);
                    setTheme(R.style.theme_1);
                } else {
                    SharedPreferencesMgr.setInt("theme", 1);
                    setTheme(R.style.theme_2);
                }
                
                final View rootView = getWindow().getDecorView();
    rootView.setDrawingCacheEnabled(true);
    rootView.buildDrawingCache(true);
    final Bitmap localBitmap = Bitmap.createBitmap(rootView.getDrawingCache());
    rootView.setDrawingCacheEnabled(false);
    if (null != localBitmap && rootView instanceof ViewGroup) {
        final View localView = new View(getApplicationContext());
        localView.setBackgroundDrawable(new BitmapDrawable(getResources(), localBitmap));
        ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
        ((ViewGroup) rootView).addView(localView, params);
        localView.animate().alpha(0).setDuration(1000).setListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animation) {
                ColorUiUtil.changeTheme(rootView, getTheme());
            }

            @Override
            public void onAnimationEnd(Animator animation) {
                ((ViewGroup) rootView).removeView(localView);
                localBitmap.recycle();
            }

            @Override
            public void onAnimationCancel(Animator animation) {

            }

            @Override
            public void onAnimationRepeat(Animator animation) {

            }
        }).start();
                    }
                } else {
                    ColorUiUtil.changeTheme(rootView, getTheme());
                }
            }
        });
        btn_next = (ColorButton)findViewById(R.id.btn_2);
        btn_next.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this, SecondActivity.class));
            }
        });
    }
    
}

上面得到DecorView的视图,这里会复制当前RootView(根视图)覆盖到当前视图上面,然后调用动画改变其透明度。这里注册动画监听函数(AnimatorListener),在onAnimatorStart[动画开始执行]会根据改变的主题样式去同步改变各控件样式,在onAnimatorEnd[动画执行完毕]会删掉复制的RootView;

这里同步改变各控件主题样式的触发操作主要由ColorUiUtil完成:

public class ColorUiUtil {
    /**
     * 切换应用主题
     *
     * @param rootView
     */
    public static void changeTheme(View rootView, Resources.Theme theme) {
        if (rootView instanceof ColorUiInterface) {
            ((ColorUiInterface) rootView).setTheme(theme);
            if (rootView instanceof ViewGroup) {
                int count = ((ViewGroup) rootView).getChildCount();
                for (int i = 0; i < count; i++) {
                    changeTheme(((ViewGroup) rootView).getChildAt(i), theme);
                }
            }
            if (rootView instanceof AbsListView) {
                try {
                    Field localField = AbsListView.class.getDeclaredField("mRecycler");
                    localField.setAccessible(true);
                    Method localMethod = Class.forName("android.widget.AbsListView$RecycleBin").getDeclaredMethod("clear", new Class[0]);
                    localMethod.setAccessible(true);
                    localMethod.invoke(localField.get(rootView), new Object[0]);
                } catch (NoSuchFieldException e1) {
                    e1.printStackTrace();
                } catch (ClassNotFoundException e2) {
                    e2.printStackTrace();
                } catch (NoSuchMethodException e3) {
                    e3.printStackTrace();
                } catch (IllegalAccessException e4) {
                    e4.printStackTrace();
                } catch (InvocationTargetException e5) {
                    e5.printStackTrace();
                }
            }
        } else {
            if (rootView instanceof ViewGroup) {
                int count = ((ViewGroup) rootView).getChildCount();
                for (int i = 0; i < count; i++) {
                    changeTheme(((ViewGroup) rootView).getChildAt(i), theme);
                }
            }
            if (rootView instanceof AbsListView) {
                try {
                    Field localField = AbsListView.class.getDeclaredField("mRecycler");
                    localField.setAccessible(true);
                    Method localMethod = Class.forName("android.widget.AbsListView$RecycleBin").getDeclaredMethod("clear", new Class[0]);
                    localMethod.setAccessible(true);
                    localMethod.invoke(localField.get(rootView), new Object[0]);
                } catch (NoSuchFieldException e1) {
                    e1.printStackTrace();
                } catch (ClassNotFoundException e2) {
                    e2.printStackTrace();
                } catch (NoSuchMethodException e3) {
                    e3.printStackTrace();
                } catch (IllegalAccessException e4) {
                    e4.printStackTrace();
                } catch (InvocationTargetException e5) {
                    e5.printStackTrace();
                }
            }
        }
    }


}

最重要的一点是需要动态变换主题的组件都要使用自定义组件,个别没有的根据规则可以自己实现:
Android项目实战之:无需重启应用和当前页面实现一键换肤
这个框架对换肤操作的确管用,但是需要自己去设置对应的自定义组件,感觉代码量颇大,可定制型不是很强,而且功能解耦不是很清晰。

如果你用的控件它的框架中并没有,那就照着作者写的另写一个吧。比如我用的design包下的TabLayout,那么就复制一份作者写的控件,修改为extends TabLayout:

public class ColorTabLayout extends TabLayout implements ColorUiInterface {

    private int attr_background = -1;

    public ColorTabLayout(Context context) {
        super(context);
    }

    public ColorTabLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        this.attr_background = ViewAttributeUtil.getBackgroundAttibute(attrs);
    }

    public ColorTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        this.attr_background = ViewAttributeUtil.getBackgroundAttibute(attrs);
    }

    @Override
    public View getView() {
        return this;
    }

    @Override
    public void setTheme(Resources.Theme themeId) {
        if(attr_background != -1) {
            ViewAttributeUtil.applyBackgroundDrawable(this, themeId, attr_background);
        }
    }
}

然后该怎么用怎么用:

<com.monkey.zhuishu.changeTheme.widget.ColorTabLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="@dimen/base_title_height"
    android:layout_below="@+id/titleBar"
    android:background="?attr/activity_bg"
    app:tabIndicatorColor="?attr/indicator_color"
    app:tabSelectedTextColor="@color/tabSelectedText"
    app:tabTextColor="@color/tabUnSelectedText">

</com.monkey.zhuishu.changeTheme.widget.ColorTabLayout>