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

819 阅读3分钟

[TOC]

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

总览

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

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

UIMode实现

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

    daynight_1

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

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

    //设置所有的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中新建日夜间两种颜色

    <?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文件,添加自定义属性

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

    <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. 在布局文件中使用自定义属性颜色

    <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>
    
  5. 在Activity中配置

    1. 只在当前Activity中生效

      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生效

      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使用和原理分析

添加如下依赖:

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中进行初始化操作

@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()方法

@NonNull
@Override
public AppCompatDelegate getDelegate() {
    return SkinAppCompatDelegateImpl.get(this, this);
}

通过如下方法触发

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

详细用法查看README

对比

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