最近发现BiliBili客户端有个非常个性的功能, 提供了夜间模式/少女粉/姨妈红/胖次蓝/基佬紫等主题配色供用户切换选择, 不但丰富了用户了的视觉体验, 而且一定程度的增加了产品的收入, 是的你没看错,BiliBili客户端的很多配色主题都是要花费硬币的. 这么酷炫又实用的功能当然不能错过了!
上图:
切换主题这个功能也算是一个骨灰级的东西了, 就拿最熟悉的QQ来说很久之前就有了切换皮肤的功能, 当我们这里讨论的只是配色而不是皮肤, 本篇的目的在于用最精简的代码来实现切换整个应用配色的功能.
动手之前在github上扒到了哔哩哔哩关于他们主题的开源框架MagicaSakura, ‘魔法小樱’浓浓的二次元气息啊, 这些都不是问题, 问题是这个Sakura有点肿.. 为什么这么说呢? 通过源码发现这个框架是对所有用到的View全部重定义一遍, 并实现了切换配色的interface, 这个会造成所有View都要因为配色问题重新定义一个副本, 并在所有用到的地方使用这个新的View.
MagicaSakura具体的实现这里不做讨论, 一是因为肿, 二是这样的轮子重复造起来也没意思.
所以接下来本篇会记录一个更加简洁的方式,来实现开篇的效果.
其实,google已经为我们提供了一个切换主题的方法setTheme, 不过如果想要这个方法来实现我们的功能, 可能会存在一些缺陷. 这个方法的作用是在activity内的view在初始化之前指定运行时的主题, 意思在这个方法调用之前的Activity如果已经初始化完成那么是没有用的, 只有在Activity在创建完成并且contentview没有初始化的时候调用才会生效.
那么这个方法不能用吗? 其实不然, activity有一个recreate方法, 会让当前界面重新构建一次, 这时候再构建的时候指定theme就可以变更主题了. 不过这个方法调用时会造成界面严重的闪烁, 用户体验非常不好, 而且存在于Activity栈中的activity也仍然是旧的样式. 只是简单的调用recreate方法并不能实现我们要的效果.
那么如何让recreate能够平滑过度并且对之前已经存在的activity进行重建将会是接下来要记录的重点
本篇的代码片段摘录自我的一个kotlin写的项目, 代码皆为kotlin实现, 鉴于kotlin可读性比较高就不再翻译了
首先定一个类ColorTheme做为我们切换主题的工具类, 同时实现ActivityLifecycleCallbacks接口.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| object ColorTheme : Application.ActivityLifecycleCallbacks { var activityList = arrayListOf<Activity>() val ThemeChacheKey = "themeCacheData" var style: Int = 0 fun setStyle(activity: Activity, @StyleRes style: Int, data: Bundle? = null) { ColorTheme.style = style Observable.from(activityList) .skipLast(1) .subscribe { it.recreate() } val intent = Intent(activity, activity.javaClass) intent.putExtra(ThemeChacheKey, data) activity.startActivity(intent) activity.overridePendingTransition(0, 0); activity.finish() } override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) { activity?.let { activityList.add(activity) val styleRes = ColorTheme.style activity.setTheme(styleRes) } } override fun onActivityDestroyed(activity: Activity?) { activity?.let { activityList.remove(activity) } } ...
|
上面这段代码实现了对当前界面应用新theme的处理以及对已经打开的activity应用新theme的处理.
通过对调用的activity再次创建,并finish掉当前的activity可用规避recreate方法的严重闪烁问题,需要注意的是对通过的intent传递并回显界面中的临时数据.
栈中的数据则是通过对活动activity的记录,并依次调用这些activity的recreate方法进行重建,因为这些activity是被压入栈中不显示的,所以使用recreate方法并不会出现闪烁问题. 需要注意的是需要在可能被调用recreate的activity中复写onActivitySaveInstanceState方法用来处理activity被销毁终极时的数据回显问题.
给这个类加上初始化方法
1 2 3 4
| fun init(application: Application, style: Int = 0) { application.registerActivityLifecycleCallbacks(this) this.style = style }
|
我们定义这个类之后在我们的Application的onCreate里初始化即可
1 2 3 4 5 6 7 8 9
| class CoderApplication : Application() { private val TAG = PluginApplication::class.java.simpleName override fun onCreate() { super.onCreate() ColorTheme.init(this,R.style.Theme_Default) ... }
|
需要切换主题配色的地方如此调用即可:
1
| ColorTheme.setStyle(activity, style)
|
开发UI的地方需要注意, 所有用到需要主题配色的地方都需要使用?attr/来指定颜色, 并在每个主题的style中指定attr的值:
UI指定颜色
1 2 3 4 5 6
| ... <TextView android:background="?attr/colorPrimary" android:textColor="?attr/colorPrimaryText" android:text="测试文本" /> ...
|
两个主题配色
1 2 3 4 5 6 7 8 9
| <style name="Theme_Blue" parent="BaseTheme"> <item name="colorPrimary">#0000FF</item> <item name="colorPrimaryText">#FFFFFF</item> ... <style name="Theme_Red" parent="BaseTheme"> <item name="colorPrimary">#FF0000</item> <item name="colorPrimaryText">#FFFFFF</item> ...
|
为了更方便的使用, 我把上述的实现做了gradle依赖, 使用方法:
gradle
/build.gradle
1 2 3 4 5 6
| repositories { ... maven { url "https://jitpack.io" } }
|
/app/build.gradle
1 2 3 4
| dependencies { ... compile 'com.github.NightFarmer:Themer:1.0.0' }
|
本工程使用kotlin编译, kotlin和java同为jvm平台语言, 所以java开发同样可以正常使用.
github: https://github.com/NightFarmer/Themer