文章目录

  • 换肤方案
    • Theme换肤
    • Resouce换肤
      • 2.拿到皮肤包Resource对象
      • 3.标记需要换肤的View
      • 4.缓存需要换肤的View
      • 5.切换时即时刷新页面
      • 6.制作皮肤包
    • UiModeManager换肤
  • 总结

换肤方案

据我所知目前Android换肤有两种类型,静态换肤和动态换肤;静态换肤就是将所有的皮肤方案放到项目中,而动态换肤则就是从网络加载皮肤包动态切换;
通常静态换肤是通过Theme实现,通过在项目中定义多套主题,使用setTheme方法切换的方式实现换肤;
动态换肤是通过替换系统的Resouce动态加载下载到本地的资源包实现换肤。
实际上静态换肤还有一种方式,使用系统自带的UiModeManager,只是它只能用来实现夜间模式。
下面我们对这个三种换肤方式进行讲解。

Theme换肤

这种方式是谷歌官方推荐的方式,很多google的app都是使用的这种方式,据说知乎也是使用的这种方式,这种方式的优点就是使用很简单,首先在res/color.xml下定义多套颜色资源(需要几套皮肤就定义几套,我们这里用两套):

<?xml version="1.0" encoding="utf-8"?>
<resources>//日间模式<color name="colorPrimary">#3F51B5</color><color name="colorPrimaryDark">#303F9F</color><color name="colorAccent">#FF4081</color>//夜间模式<color name="nightColorPrimary">#3b3b3b</color><color name="nightColorPrimaryDark">#383838</color><color name="nightColorAccent">#a72b55</color>
</resources>

然后在定义两套主题,分别引用不同的颜色资源:

<?xml version="1.0" encoding="utf-8"?>
<resources><!-- Base application theme. -->//日间模式主题<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"><item name="colorPrimary">@color/colorPrimary</item><item name="colorPrimaryDark">@color/colorPrimaryDark</item><item name="colorAccent">@color/colorAccent</item><!--自定义的属性 用作背景色--><item name="ColorBackground">@color/backgroundColor</item><!--文字颜色--><item name="android:textColor">@color/textColor</item></style>//夜间模式主题<style name="NightAppTheme" parent="Theme.AppCompat.Light.DarkActionBar"><item name="colorPrimary">@color/nightColorPrimary</item><item name="colorPrimaryDark">@color/nightColorPrimaryDark</item><item name="colorAccent">@color/nightColorAccent</item><!--自定义的属性 用作背景色--><item name="ColorBackground">@color/nightColorPrimary</item><!--文字颜色--><item name="android:textColor">@android:color/white</item></style>
</resources>

接着在布局文件中通过下面的方式引用资源文件

android:background="?attr/colorPrimary"

最后在Activity的setContentView方法前设置想要的主题就可以了。

    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);if () {setTheme(R.style.AppTheme);}setContentView(R.layout.activity_main2);}

这里有几个问题:
1.当我们在应用中切换主题时只有重新创建的Activity才会使用新的主题,所以这里就需要我们手动调用一下recreate()方法,这样就可以重新创建页面切换主题,但是这样做有两个弊端,一是屏幕会出现一下闪烁,二是页面重新创建之后要考虑数据的恢复。这个问题也是这个方案最大的弊端,我们可以不重新创建Activity而是在切换主题后手动修改已创建Activity的View的颜色等信息,但是我们不能在每个页面都写上修改页面View信息的方法,这样的工作量太大了,我们把切换主题的页面入口放到最底层,也就是说如果你想切换主题必须回到主页面,这样我们只需要在主页面添加这样的方法就可以了,这也算一个取巧的方法。

2.这个方案如果用在新项目上貌似没有什么不妥,但是如果一个老项目想用这个方案就很难受了,因为除了要定义多套资源外,我们还要把布局文件中的资源引用全部修改一遍,反正我是不想这么做。

Resouce换肤

动态换肤的一般步骤为:

  1. 下载并加载皮肤包
  2. 拿到皮肤包Resource对象
  3. 标记需要换肤的View
  4. 缓存需要换肤的View
  5. 切换时即时刷新页面
  6. 制作皮肤包

下面的代码参考一个动态换肤框架
Android-Skin-Loader

2.拿到皮肤包Resource对象

public Resources getSkinResources(Context context){/*** 插件apk路径*/String apkPath = Environment.getExternalStorageDirectory()+"/skin.apk";AssetManager assetManager = null;try {AssetManager assetManager = AssetManager.class.newInstance();AssetManager.class.getDeclaredMethod("addAssetPath", String.class).invoke(assetManager, apkPath);} catch (Throwable th) {th.printStackTrace();}return new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
}

构造一个AssetManager对象并调用addAssetPath方法设置资源文件的路径,由于addAssetPath方法是hide注解的,我们不能直接调用,所以我们通过反射来调用这个方法,最后使用我们构造的AssetManager对象和原来的DisplayMetrics、Configuration构造一个Resources对象。
拿到Resoures对象通过下面的方法获取需要的资源:

getIdentifier(String name, String defType, String defPackage)

第一个参数是资源的名称,比如R.color.red,其中red就是name
第二个参数是资源类型,比如R.String.appname,其中String就是类型
第三个参数是资源所在的包名,这是打皮肤包设置的,一般是自己应用的包名

3.标记需要换肤的View

在布局文件中自定义一个属性,例如:

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"xmlns:skin="http://schemas.android.com/android/skin"android:layout_width="match_parent"android:layout_height="match_parent"skin:enable="true" android:background="@color/color_app_bg" ><TextViewandroid:id="@+id/detail_text"android:layout_width="wrap_content"android:layout_height="wrap_content"skin:enable="true"  /></RelativeLayout>

skin:enable=“true” 就是我们自定义的属性,名字什么的都无所谓。

4.缓存需要换肤的View

这里需要用到一个工具LayoutInflaterFactory,它可以拦截替换View的创建过程,具体的介绍看一下这篇文章Android技能树 — LayoutInflater Factory小结,我们在view创建之前(一般是setContentView方法之前)设置自定义的LayoutInflaterFactory就可以拿到并缓存被标记的View。

public class SkinInflaterFactory implements LayoutInflater.Factory {//缓存View
private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {// if this is NOT enable to be skined , simplly skip itboolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);if (!isSkinEnable) {return null;}//创建viewView view = createView(context, name, attrs);if (view == null) {return null;}//缓存viewparseSkinAttr(context, attrs, view);return view;}
}
    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);getLayoutInflater().setFactory(new SkinInflaterFactory());setContentView(R.layout.activity_main2);}

如果Acitivity继承的是AppCompatActivity,可以直接调用:

    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);LayoutInflaterCompat.setFactory(LayoutInflater.from(this),new SkinInflaterFactory());setContentView(R.layout.activity_main2);}

在Android3.0之后新增了LayoutInflater.Factory2接口,我们最好使用新的接口,使用起来是类似的,只是把Factory改成Factory2:

public class SkinInflaterFactory implements LayoutInflater.Factory2 {//缓存View
private List<SkinItem> mSkinItems = new ArrayList<SkinItem>();@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {// if this is NOT enable to be skined , simplly skip itboolean isSkinEnable = attrs.getAttributeBooleanValue(SkinConfig.NAMESPACE, SkinConfig.ATTR_SKIN_ENABLE, false);if (!isSkinEnable) {return null;}//创建viewView view = createView(context, name, attrs);if (view == null) {return null;}//缓存viewparseSkinAttr(context, attrs, view);return view;}
}
    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);getLayoutInflater().setFactory2(new SkinInflaterFactory());setContentView(R.layout.activity_main2);}

如果Acitivity继承的是AppCompatActivity,可以直接调用:

    @Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);LayoutInflaterCompat.setFactory2(LayoutInflater.from(this),new SkinInflaterFactory());setContentView(R.layout.activity_main2);}

如果skin:enbale不为true则直接返回null交给系统默认去创建。而如果为true,则自己去创建这个View,并将这个VIew的所有属性比如id, width height,textColor,background等与支持换肤的属性进行对比。比如我们支持换background textColor listSelector等, android:background="@color/hall_back_color" 这个属性,在进行换肤的时候,如果皮肤包里存在hall_back_color这个值的设置,就将这个颜色值替换为皮肤包里的颜色值,以完成换肤的需求。同时,也会将这个需要换肤的View保存起来。

如果在切换换肤之后,进入一个新的页面,就在进入这个页面Activity的 InlfaterFacory的onCreateView里根据skin:enable=“true” 这个标记,进行判断。为true则进行换肤操作。而对于切换换肤操作时,已经存在的页面,就对这几个存在页面保存好的需要换肤的View进行换肤操作。
这里并没有把所有方法都贴出来,想继续深入的可以去看框架的源码。

5.切换时即时刷新页面

每个Activity的SkinInflaterFactory中都有着一个缓存View的集合,使用观察者模式在换肤成功之后通知到每个Activity去刷新View。

6.制作皮肤包

  1. 新建工程project
  2. 将换肤的资源文件添加到res文件下,无java文件
  3. 直接运行build.gradle,生成apk文件(注意,运行时Run/Redebug configurations 中Launch Options选择launch nothing),否则build 会报 no default Activty的错误。
  4. 将apk文件重命名如black.apk,重命名为black.skin防止用户点击安装

UiModeManager换肤

UiModeManager是在API8添加的,它用来管理界面显示模式的服务,我们实现夜间模式主要用到他的setNightMode方法,我们看一下这个方法的注释:

On API 22 and below, changes to the night mode
* are only effective when the {@link Configuration#UI_MODE_TYPE_CAR car}
* or {@link Configuration#UI_MODE_TYPE_DESK desk} mode is enabled on a
* device. Starting in API 23, changes to night mode are always effective.
*/
public void setNightMode(@NightMode int mode)

在API22及其以下的Android版本,只有在设置了UI_MODE_TYPE_CAR或者UI_MODE_TYPE_DESK之后,设置night模式才会有效。(翻译的我都不懂了),简单说就是如果你想设置night也就是夜间模式,必须先设置UI_MODE_TYPE_CAR或者UI_MODE_TYPE_DESK其中一个,第一个是驾驶模式,第二个我也搞不懂是什么,反正设置了这两个flag之后系统UI会有变动我们不能设置。那怎么办呢?当然有办法。
分析了源码之后发现setNightMode最终是通过设置Configuration的uiMode属性来实现的夜间模式,那不就简单了,我们自己也可以设置呀,这样就可以跳过驾驶模式了:

public static void updateNightMode(boolean on) {DisplayMetrics dm = sRes.getDisplayMetrics();Configuration config = sRes.getConfiguration();config.uiMode &= ~Configuration.UI_MODE_NIGHT_MASK;config.uiMode |= on ? Configuration.UI_MODE_NIGHT_YES : Configuration.UI_MODE_NIGHT_NO;sRes.updateConfiguration(config, dm);
}

调用这个方法就可以实现夜间模式,哦,不对,我们还没放资源文件呢 ,这里就非常简单了,是我最喜欢的一种换肤方式,直接在res下创建-night结尾的资源文件夹,然后放入你想修改的资源就可以了,比如我想修改color.xml下的colorPrimary,先创建values-night文件夹,然后将values下的color.xml拷贝过去,再修改values-night文件夹下的colorPrimary,这样切换到夜间模式就会直接使用values-night下的colorPrimary了。注意这里还有一个问题,我们切换到夜间模式时已经创建的Activity不会改变,还是要重新创建。
我封装了一个简单的类用来实现这种方式:

public class NightModeHelper {private static final String TAG = "NightModeHelper";private static final String PREF_KEY = "nightModeState";public static void updateConfig(Context context) {int currentMode = (context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK);SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context);updateConfig(context, mPrefs.getInt(PREF_KEY, currentMode));}private static void updateConfig(Context context, int newNightMode) {if (context == null) {return;}Resources res = context.getResources();Configuration conf = res.getConfiguration();int currentNightMode = conf.uiMode & Configuration.UI_MODE_NIGHT_MASK;if (currentNightMode != newNightMode) {Configuration config = new Configuration(conf);DisplayMetrics metrics = res.getDisplayMetrics();config.uiMode = newNightMode | (config.uiMode & ~Configuration.UI_MODE_NIGHT_MASK);res.updateConfiguration(config, metrics);
//        if (!(Build.VERSION.SDK_INT >= 26)) {//            ResourcesFlusher.flush(res);
//        }} else {Log.d(TAG, "applyNightMode() | Skipping. Night mode has not changed: " + newNightMode);}}public static int getCurrentMode(Context context) {SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context);return mPrefs.getInt(PREF_KEY, context.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK);}/*** 手动设置模式* @param context* @param mode {@link Configuration#UI_MODE_NIGHT_YES} {@link Configuration#UI_MODE_NIGHT_NO}*/public static void setMode(Context context, int mode) {SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context);mPrefs.edit().putInt(PREF_KEY, mode).apply();}/*** 切换模式** @param context*/public static void toggle(Context context) {SharedPreferences mPrefs = PreferenceManager.getDefaultSharedPreferences(context);if (getCurrentMode(context) == Configuration.UI_MODE_NIGHT_YES) {mPrefs.edit().putInt(PREF_KEY, Configuration.UI_MODE_NIGHT_NO).apply();} else {mPrefs.edit().putInt(PREF_KEY, Configuration.UI_MODE_NIGHT_YES).apply();}}
}

注意这里我并没有做Activity的重新创建工作。

如果你的Acitivty是继承AppCompatActivity的,那么可以使用AppCompatActivity封装的代码进行夜间模式切换:

getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);

或者:

AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);

这两行代码的效果是一样的,并且他们会自动重新创建Activity。

总结

  1. 静态换肤的优势是简单稳定,但是将所有主题写在应用里会增大安装包的体积,并且不利于扩展。
  2. 动态换肤虽然不会占用安装包的体积,并且可以随意扩展,但是他毕竟侵入了系统,有潜在的风险,而且实现起来也有一点麻烦,但是利大于弊,所以目前大多数app都是使用的这种方式。
  3. 如果只是想实现夜间模式,那么第三种方案我认为是最好的,而且在老项目上实现这个功能也不会很繁琐。
  4. 对于静态换肤的重新创建Activity问题我建议重启App,这样可以省去很多多余的操作。

Android换肤总结相关推荐

  1. Android 换肤demo,轻量快捷接入集成,判断是否夜间模式

    true为黑夜模式 //检查当前系统是否已开启暗黑模式 public static boolean getDarkModeStatus(Context context) {int mode = con ...

  2. android换肤哪个简单,Android换肤

    这是一个Android换肤的库,代码量极少,支持换肤的情况还比较多,提供了以下功能: 无需重启,一键换肤效率高 支持App内多套皮肤换肤 支持插件式动态换肤 支持Activity,Fragment,以 ...

  3. android换肤动画,Android换肤(二) — 插件式换肤

    ###前言 上节我们讲到了`Android-skin-support`库的应用内换肤,大家感兴趣的可以参看文章: [Android换肤(一) - 应用内换肤](http://www.demodashi ...

  4. Android-skin-support 一款用心去做的Android 换肤框架

    介绍 Github地址: https://github.com/ximsfei/Android-skin-support Android-skin-support: 一款用心去做的Android 换肤 ...

  5. Android 换肤之旅——主题切换

    随着手机应用的成熟发展,市面上的应用已不在以简单的实现功能为目标了,它们反而会更加注重用户体验.我们常说的换肤(主题)功能--针对用户的喜好来提供一个可选的主题也是提高用户体验的方式之一.换肤功能不仅 ...

  6. Android 换肤方案详解(一)

    引言 在我们的开发中,也许有些项目会有换肤的需求,这个时候会比较头疼怎么做才能做到一键换肤呢?大家肯定是希望只要一行代码就能调用最好.下面我们先分析一下换肤的本质是什么? 原理 换肤,其本质无非就是更 ...

  7. Android 换肤原理分析

    当了解了一些知识,应该用文字记录它,再抽个时间再看它,永远记住它 Android 换肤的理论知识和文章已经很多了,这里记录一下自己对这块的理解.本文效果如下: 工程:一键换肤的快乐 一.换肤的由来 首 ...

  8. android 换肤框架搭建及使用 (3 完结篇)

    本系列计划3篇: Android 换肤之资源(Resources)加载(一) setContentView() / LayoutInflater源码分析(二) 换肤框架搭建(三) - 本篇 tips: ...

  9. android换肤动画,Android-换肤ThemeSkinning使用

    1.png 2.png 3.png 1.集成步骤: 1.添加依赖 compile 'com.solid.skin:skinlibrary:1.4.3' 参考源码版本 2.让你的 Application ...

最新文章

  1. 计算机科学与技术专业认证研讨,信息学院召开计算机科学与技术专业IEET认证暨人才培养方案修订研讨会...
  2. 【7】青龙面板系列教程之任务消息定时推送
  3. Iterator迭代器并发修改异常问题解决
  4. false sharing
  5. linux(CentOS)下安装mongodb
  6. c语言 char c1,c2; for (c1='0',C语言-5循环结构(PPT)复习课程.ppt
  7. 数据库函数依赖及范式
  8. 【C语言】创建一个函数,并调用比较三个数的大小
  9. 转帖:一份不错的游戏程序书单(比较全面,但都是英文的。。)
  10. java 产生无重复的随机数,Java程序生成无重复的随机数
  11. C 语言中 bzero()函数的使用
  12. 最小二乘支持向量机分类器(LSSVM)及Python实现
  13. 中华石杉Java面试突击第一季笔记二(分布式搜索引擎)
  14. CASS11.0.0.6安装以及教程
  15. Unity 3D 热更新之基于 Asset Bundle Browser 的 AssetBundle包
  16. FXCG: 日内交易与波段交易有什么区别
  17. maven 编译出现错误
  18. 黑马程序员—[Android就业薪资] Android31期,毕业18个工作日,就业率71.95%,薪资9946元
  19. 机器学习之梯度下降法(GD)、随机梯度下降法(SGD)和随机平均梯度下降法(SAGD)
  20. 中国一汽发布L4级智能驾驶技术,解放商用车率先使用;科大讯飞今日将发翻译机2.0版本;阿里巴巴已以50亿控股全球第二大WiFi芯片商乐鑫信息科技

热门文章

  1. eclipse 下载sts插件及离线安装教程
  2. LabVIEW灰度图像操作与运算(基础篇—2)
  3. GUET七星瓢虫2022年考核题目c语言部分复现
  4. MindCV提交ISSUE规范
  5. nat123内网穿透详解
  6. STM32单片机LED显示屏驱动原理与实现
  7. 如何根据已有的下载路径下载文件并重命名文件
  8. 特征选择-嵌入式选择
  9. LPC 使用usb Vcom和linux主机进行通信收到乱码
  10. 使用Xpath语法爬取豆瓣读书Top250