Android换肤方案总结

这篇总结也是拖了很久了,欠下的技术债必须得偿还啦~

Android换肤在使用场景上可以区分为静态换肤/动态换肤、应用内换肤/插件式换肤。不同的换肤方案,适用于不同的业务场景。

setTheme可以较好的支持静态换肤,但如果要动态换肤则需要解决的核心问题有:

  • 外部资源的加载
  • 定位到需要换肤的View

第一个资源加载的问题可以通过构造AssetManager,反射调用其addAssetPath就可以完成。
第二个问题,就可以利用在onCreateView中,根据view的属性来定位。应用内换肤一般以资源前缀或后缀来区分不同资源,辅助以tag或侵入式等来区分换肤的view;进而保存需要换肤的view引用。

在个人的应用场景中,需要支持动态实时换肤,且以应用内换肤为主。最终在开源框架ChangeSkin的基础上进行了改造来满足需求。

下面会介绍不同的换肤方案及优缺点。

切换Theme

首先,在attr.xml中定义支持换肤的属性。一般是textColor,background等。如果系统支持的字体颜色,背景色较多,则需要增加很多的自定义属性。

<resources>
    <attr name="textColor" format="reference|color"/>
    <attr name="background" format="reference|color"/>
</resources>

其次,在不同主题的style中定义属性值。

<style name="AppTheme_Light" >
    <item name="textColor">@android:color/black</ item>
    <item name="android:background">@android:color/white</ item>
</style>
 
<style name="AppTheme_Night">
    <item name="textColor">@android:color/white</ item>
    <item name="android:background">@android:color/black</ item>
</style>

再次,View上应用style(?attr/nbackground表示使用style内的此属性作为view 的background).

<TextView
    android :id="@+id/textview"
    android :layout_width="wrap_content"
    android :layout_height="wrap_content"
    android :text="Hello World!"
    android :background="?attr/background"
    android :textColor="?attr/textColor"/>

最后,在Java代码内根据情况设置主题并刷新界面。如果要支持动态换肤,则必须要设置换肤的监听,全部页面实现一个回调。原因是setTheme方法要在setContentView之前调用才能生效。

//设置换肤回调
@Override
protected void onSkinChange() {
    TypedValue background = new TypedValue();
    TypedValue textColor = new TypedValue();
    Resources.Theme theme = getTheme();
    theme.resolveAttribute(R.attr.background, background, true);
    theme.resolveAttribute(R.attr.textColor, textColor, true);
    mTextView .setTextColor(textColor.data);
    mTextView.setBackgroundResource(background.resourceId);
    
    ...
}

方案优点:

  • 非侵入式,兼容性好,接口完善。

方案缺点:

  • 为了支持动态换肤,需要recreate页面或遍历view来实现。有性能消耗。
  • 如果应用设计无颜色规范,导致应用内颜色等定义较多,则需要定义较多自定义属性。

换肤框架AndroidChangeSkin

项目地址:https://github.com/hongyangAndroid/AndroidChangeSkin
该框架通过相同名称+不同皮肤后缀来区分不同皮肤,再通过View的Tag来标志夜间模式的Drawable/Color引用。
如实现黑白皮肤,文本颜色item_text_color有一套默认皮肤,一套黑色皮肤定义资源item_text_color,item_text_color_black。
方案优点:

  • 非侵入式, Android支持度高
  • 支持应用内换肤和插件换肤
  • 目前支持src,background,textColor,支持扩展

方案缺点:

  • 需要自定义Tag;部分View的Tag被其他逻辑占用
  • 使用方式繁琐
<TextView
    android :layout_width="wrap_content"
    android :layout_height="wrap_content"
    android :tag="skin:item_text_color:textColor"
    android :text="@string/hello_world"
    android :textColor="@color/item_text_color"/>

换肤框架ChangeSkin

项目地址:https://github.com/hongyangAndroid/ChangeSkin.
针对框架AndroidChangeSkin需要为每个支持换肤的view设置tag,鸿洋实现了侵入式换肤的方案。该框架也参考了AndroidSkinLoader。
该框架最主要的方法在于baseSkinactivity中,侵入了onCreateView方法,使用了视图兼容工厂。遍历反射创建的view,根据资源前缀来标志是否支持换肤,存储支持换肤的view引用,以实现动态换肤。

在应用的某些场景,需要动态获取当前皮肤某资源信息,如黑白皮肤的主题颜色,需要通过SkinManager获取ResourceManager对象,进而通过Resources的getColor方法获取颜色值。参数为资源名,为了便于开发,可通过getResourceName将resId转化为resName。

public static int getSkinColor(@ColorRes int resId){
        if (SkinManager.getInstance().isUseSkinPlugin()) {
            String resName = CoreApplication.getInstance().getResources().getResourceName(resId);
            return SkinManager.getInstance().getResourceManager().getColor(resName);
        }

        int retColor = CoreApplication.getInstance().getResources().getColor(resId);
        return retColor;
    }

方案优点:

  • 使用方便,对开发者无额外成本
  • 支持应用内换肤和插件换肤
  • 目前支持src,background,textColor,支持扩展

方案缺点:

  • 侵入式换肤,可能存在兼容性问题

换肤框架AndroidSkinLoader

该方案为侵入式方案,通过为LayoutInfalter去设置自定义Factory,对加载的View进行分析和提取。

具体可参考项目地址:https://github.com/fengjundev/Android-Skin-Loader

参考文章:
android 换肤(1)——插件式无缝换肤(解析鸿洋大神的换肤流程)
android 换肤(2)——插件式无缝换肤(解析鸿洋大神的换肤流程)
Android换肤技术总结
Android夜间模式调研总结

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 157,643评论 24 688
  • 很少跟人说起父辈,因为每每想起来心里除了遗憾,更多的则是心痛,可今天周哥提起这个话题,说到父辈们对家庭对后代的影响...
    兔小蝉阅读 233评论 0 0
  • 文档 https://www.openstack.org/software/ Installation Guide...
    writeme阅读 111评论 0 0