logo头像

我有一个梦想

日夜间及换肤(一)-常用技巧

本文于 851 天之前发表,文中内容可能已经过时。

[TOC]

原地址:日夜间及换肤(一)-常用技巧

总览

实现日夜间的方式有多种,基本可以整理如下:

  1. 设置UiMode来设置
  2. 对Activity设置主题来变换
  3. 动态设置资源,控制view刷新

UIMode实现

  1. 在value的同级目录下新建一个values-night的目录,其中新建colors.xml文件,themes.xml文件等需要日夜间切换

    daynight-1

  1. 比如colors文件,需要将color所对应的名字保持一致,只进行颜色上的更改,达到日夜间不同的效果

    1
    2
    3
    4
    5
    6
    7
    //normal
    <color name="background">#FFFFFFFF</color> //白色
    <color name="text_color">#FF000000</color> //黑色

    //night
    <color name="background">#FF000000</color> //黑色
    <color name="text_color">#FFFFFFFF</color> //白色
  1. 在需要触发切换的地方调用如下代码

    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
    //设置所有的Activity生效
    if (AppCompatDelegate.getDefaultNightMode() == AppCompatDelegate.MODE_NIGHT_YES) {
    //切换白天
    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
    } else {
    //切换夜间
    AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
    }

    //设置所在的Activity生效
    if(delegate.localNightMode == AppCompatDelegate.MODE_NIGHT_YES){
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    //切换白天
    delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_NO
    }
    }else{
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
    //切换夜间
    delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES
    }
    }

    //五种参数类型
    //跟随系统
    MODE_NIGHT_FOLLOW_SYSTEM
    //跟随时间自动设置
    MODE_NIGHT_AUTO_TIME
    //日间
    MODE_NIGHT_NO
    //夜间
    MODE_NIGHT_YES
    //根据系统电量决定暗色和亮色展示
    MODE_NIGHT_AUTO_BATTERY

系统会根据日夜间自动去取*-night或者正常的文件夹相对应的配置,配合持久化存储进行日夜间状态存储,二次打开加载上一次设置的模式

设置Activity主题实现

  1. 在colors.xml中新建日夜间两种颜色

    1
    2
    3
    4
    5
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <color name="background_day">#FFFFFFFF</color>
    <color name="background_night">#FF000000</color>
    </resources>
  2. 在res/values目录下新建attrs.xml文件,添加自定义属性

    1
    2
    3
    4
    <?xml version="1.0" encoding="utf-8"?>
    <resources>
    <attr name="custom_bg" format="color|reference"/>
    </resources>
  3. 在res/values目录下新建themes.xml文件,添加两种主题如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    <resources xmlns:tools="http://schemas.android.com/tools">
    <style name="Theme.MyDay" parent="Theme.AppCompat">
    <item name="custom_bg">@color/background_day</item>
    </style>

    <style name="Theme.MyNight" parent="Theme.AppCompat">
    <item name="custom_bg">@color/background_night</item>
    </style>
    </resources>
  4. 在布局文件中使用自定义属性颜色

    1
    2
    3
    4
    5
    6
    7
    <androidx.core.widget.NestedScrollView 
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="?attr/custom_bg"
    tools:context=".MainActivity">
    ...
    </androidx.core.widget.NestedScrollView>
  1. 在Activity中配置

    1. 只在当前Activity中生效

      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
      35
      36
      37
      38
      class MainActivity : AppCompatActivity() {

      private var myTheme = R.style.Theme_MyDay

      private lateinit var binding:ActivityMainBinding
      override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      // 判断是否有主题存储
      if(savedInstanceState != null){
      myTheme = savedInstanceState.getInt("theme")
      setTheme(myTheme)
      }
      binding = ActivityMainBinding.inflate(layoutInflater)
      setContentView(binding.root)
      initView()
      }

      private fun initView() {
      binding.button2.setOnClickListener {
      myTheme = if(myTheme == R.style.Theme_MyDay){
      R.style.Theme_MyNight
      }else{
      R.style.Theme_MyDay
      }
      recreate()
      }
      }

      override fun onSaveInstanceState(outState: Bundle) {
      super.onSaveInstanceState(outState)
      outState.putInt("theme", myTheme)
      }

      override fun onRestoreInstanceState(savedInstanceState: Bundle) {
      super.onRestoreInstanceState(savedInstanceState)
      myTheme = savedInstanceState.getInt("theme")
      }
      }

      因为setContentView()时进行主题加载和布局渲染,所以需要在setContentView()之前调用setTheme方法,如需动态替换日夜间模式,同样需要recreate()才能生效,因为牵扯到Activity的重建,所以我们第一反应是通过onSaveInstance存储主题,在onCreate中进行加载,当然也可以使用其他方式。

    2. 在所有Activity生效

      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
      35
      class App : Application() {
      companion object{
      var myTheme = R.style.Theme_MyDay
      }

      var activityLifecycle = object : ActivityLifecycleCallbacks {
      override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
      activity.setTheme(myTheme)
      }

      override fun onActivityStarted(activity: Activity) {
      }

      override fun onActivityResumed(activity: Activity) {
      }

      override fun onActivityPaused(activity: Activity) {
      }

      override fun onActivityStopped(activity: Activity) {
      }

      override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
      }

      override fun onActivityDestroyed(activity: Activity) {
      }

      }

      override fun onCreate() {
      super.onCreate()
      registerActivityLifecycleCallbacks(activityLifecycle)
      }
      }

      在Application中进行设置,用静态对象存储主题,监听应用下所有Activity的创建,在onActivityCreate()时,进行主题设置,这样对Activity来说是无感的,只需要设置主题即可。

      当然也可以做一个持久化存储,这里只是提供一个思路

动态设置资源,控制view刷新

比较代表性的就是Android-skin-support这个库

android-skin-support使用和原理分析

添加如下依赖:

1
2
3
4
5
implementation 'skin.support:skin-support:4.0.5'                   // skin-support
implementation 'skin.support:skin-support-appcompat:4.0.5' // skin-support 基础控件支持
implementation 'skin.support:skin-support-design:4.0.5' // skin-support-design material design 控件支持[可选]
implementation 'skin.support:skin-support-cardview:4.0.5' // skin-support-cardview CardView 控件支持[可选]
implementation 'skin.support:skin-support-constraint-layout:4.0.5' // skin-support-constraint-layout ConstraintLayout 控件支持[可选]

在Application的onCreate中进行初始化操作

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onCreate() {
super.onCreate();
SkinCompatManager.withoutActivity(this)
.addInflater(new SkinAppCompatViewInflater()) // 基础控件换肤初始化
.addInflater(new SkinMaterialViewInflater()) // material design 控件换肤初始化[可选]
.addInflater(new SkinConstraintViewInflater()) // ConstraintLayout 控件换肤初始化[可选]
.addInflater(new SkinCardViewInflater()) // CardView v7 控件换肤初始化[可选]
.setSkinStatusBarColorEnable(false) // 关闭状态栏换肤,默认打开[可选]
.setSkinWindowBackgroundEnable(false) // 关闭windowBackground换肤,默认打开[可选]
.loadSkin();
}

如果项目中使用的Activity继承自AppCompatActivity,需要重载getDelegate()方法

1
2
3
4
5
@NonNull
@Override
public AppCompatDelegate getDelegate() {
return SkinAppCompatDelegateImpl.get(this, this);
}

通过如下方法触发

1
SkinCompatManager.getInstance().loadSkin("night", SkinCompatManager.SKIN_LOADER_STRATEGY_BUILD_IN); // 后缀加载

详细用法查看README

对比

  1. UIMode和theme替换都需要重建Activity,所以会导致闪屏,动态替换会更加流畅
  2. recreate()是在API 11添加进来的,所以会在Android 2.x中使用抛出异常
  3. theme换肤起来会更加方便