热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

Android实现日夜间模式的深入理解

相信Android的日间夜间模式切换相信大家在平时使用APP的过程中都遇到过,比如知乎、简书中就有相关的模式切换。实现日间夜间模式切换的方案也有许多种,趁着今天有空来讲一下日间夜间模式切换的几种实现方案,也可以做一个横向的对比来看看哪种方案最好。

在本篇文章中给出了三种实现日间/夜间模式切换的方案,三种方案综合起来可能导致文章的篇幅过长,请耐心阅读。

    1、使用 setTheme 的方法让 Activity 重新设置主题;

    2、设置 Android Support Library 中的 UiMode 来支持日间/夜间模式的切换;

    3、通过资源 id 映射,回调自定义 ThemeChangeListener 接口来处理日间/夜间模式的切换。

一、使用 setTheme 方法

我们先来看看使用 setTheme 方法来实现日间/夜间模式切换的方案。这种方案的思路很简单,就是在用户选择夜间模式时,Activity 设置成夜间模式的主题,之后再让 Activity 调用 recreate() 方法重新创建一遍就行了。

那就动手吧,在 colors.xml 中定义两组颜色,分别表示日间和夜间的主题色:

<&#63;xml version="1.0" encoding="utf-8"&#63;>

 #3F51B5
 #303F9F
 #FF4081

 #3b3b3b
 #383838
 #a72b55

之后在 styles.xml 中定义两组主题,也就是日间主题和夜间主题:



 
 

 

在主题中的 mainBackground 属性是我们自定义的属性,用来表示背景色:

<&#63;xml version="1.0" encoding="utf-8"&#63;>

 

接下来就是看一下布局 activity_main.xml:

<&#63;xml version="1.0" encoding="utf-8"&#63;>


 

android:background 属性中,我们使用 "&#63;attr/mainBackground" 来表示,这样就代表着 RelativeLayout 的背景色会去引用在主题中事先定义好的 mainBackground 属性的值。这样就实现了日间/夜间模式切换的换色了。

最后就是 MainActivity 的代码:

public class MainActivity extends AppCompatActivity {

 // 默认是日间模式
 private int theme = R.style.AppTheme;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
 // 判断是否有主题存储
  if(savedInstanceState != null){
   theme = savedInstanceState.getInt("theme");
   setTheme(theme);
  }
  setContentView(R.layout.activity_main);

  Button btn_theme = (Button) findViewById(R.id.btn_theme);
  btn_theme.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    theme = (theme == R.style.AppTheme) &#63; R.style.NightAppTheme : R.style.AppTheme;
    MainActivity.this.recreate();
   }
  });
 }

 @Override
 protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  outState.putInt("theme", theme);
 }

 @Override
 protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  theme = savedInstanceState.getInt("theme");
 }
}

MainActivity 中有几点要注意一下:

     1、调用 recreate() 方法后 Activity 的生命周期会调用 onSaveInstanceState(Bundle outState) 来备份相关的数据,之后也会调用 onRestoreInstanceState(Bundle savedInstanceState) 来还原相关的数据,因此我们把 theme 的值保存进去,以便 Activity 重新创建后使用。

     2、我们在 onCreate(Bundle savedInstanceState) 方法中还原得到了 theme 值后,setTheme() 方法一定要在 setContentView() 方法之前调用,否则的话就看不到效果了。

     3、recreate() 方法是在 API 11 中添加进来的,所以在 Android 2.X 中使用会抛异常。

贴完上面的代码之后,我们来看一下该方案实现的效果图:

二、使用 Android Support Library 中的 UiMode 方法

使用 UiMode 的方法也很简单,我们需要把 colors.xml 定义为日间/夜间两种。之后根据不同的模式会去选择不同的 colors.xml 。在 Activity 调用 recreate() 之后,就实现了切换日/夜间模式的功能。

说了这么多,直接上代码。下面是 values/colors.xml :

<&#63;xml version="1.0" encoding="utf-8"&#63;>

 #3F51B5
 #303F9F
 #FF4081
 #FF000000
 #FFFFFF

除了 values/colors.xml 之外,我们还要创建一个 values-night/colors.xml 文件,用来设置夜间模式的颜色,其中 的 name 必须要和 values/colors.xml 中的相对应:

<&#63;xml version="1.0" encoding="utf-8"&#63;>

 #3b3b3b
 #383838
 #a72b55
 #FFFFFF
 #3b3b3b

在 styles.xml 中去引用我们在 colors.xml 中定义好的颜色:



 
 

activity_main.xml 布局的内容和上面 setTheme() 方法中的相差无几,这里就不贴出来了。之后的事情就变得很简单了,在 MyApplication 中先选择一个默认的 Mode :

public class MyApplication extends Application {

 @Override
 public void onCreate() {
  super.onCreate();
  // 默认设置为日间模式
  AppCompatDelegate.setDefaultNightMode(
    AppCompatDelegate.MODE_NIGHT_NO);
 }

}

要注意的是,这里的 Mode 有四种类型可以选择:

    1、MODE_NIGHT_NO: 使用亮色(light)主题,不使用夜间模式;

    2、MODE_NIGHT_YES:使用暗色(dark)主题,使用夜间模式;

    3、MODE_NIGHT_AUTO:根据当前时间自动切换 亮色(light)/暗色(dark)主题;

    4、MODE_NIGHT_FOLLOW_SYSTEM(默认选项):设置为跟随系统,通常为 MODE_NIGHT_NO

当用户点击按钮切换日/夜间时,重新去设置相应的 Mode :

public class MainActivity extends AppCompatActivity {

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);

  Button btn_theme = (Button) findViewById(R.id.btn_theme);
  btn_theme.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
    getDelegate().setLocalNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO
      &#63; AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
    // 同样需要调用recreate方法使之生效
    recreate();
   }
  });
 }

}

我们来看一下 UiMode 方案实现的效果图:

就前两种方法而言,配置比较简单,最后的实现效果也都基本上是一样的。但是缺点就是需要调用 recreate() 使之生效。而让 Activity 重新创建就必须涉及到一些状态的保存。这就增加了一些难度。所以,我们一起来看看第三种解决方法。

通过资源 id 映射,回调接口

第三种方法的思路就是根据设置的主题去动态地获取资源 id 的映射,然后使用回调接口的方式让 UI 去设置相关的属性值。我们在这里先规定一下:夜间模式的资源在命名上都要加上后缀 “_night” ,比如日间模式的背景色命名为 color_background ,那么相对应的夜间模式的背景资源就要命名为 color_background_night 。好了,下面就是我们的 Demo 所需要用到的 colors.xml :

<&#63;xml version="1.0" encoding="utf-8"&#63;>

 
 #3F51B5
 #3b3b3b
 #303F9F
 #383838
 #FF4081
 #a72b55
 #FF000000
 #FFFFFF
 #FFFFFF
 #3b3b3b
 

可以看到每一项 color 都会有对应的 “_night” 与之匹配。

看到这里,肯定有人会问,为什么要设置对应的 “_night” ?到底是通过什么方式来设置日/夜间模式的呢?下面就由 ThemeManager 来为你解答:

public class ThemeManager {

 // 默认是日间模式
 private static ThemeMode mThemeMode = ThemeMode.DAY;
 // 主题模式监听器
 private static List mThemeChangeListenerList = new LinkedList<>();
 // 夜间资源的缓存,key : 资源类型, 值
 private static HashMap> sCachedNightResrouces = new HashMap<>();
 // 夜间模式资源的后缀,比如日件模式资源名为:R.color.activity_bg, 那么夜间模式就为 :R.color.activity_bg_night
 private static final String RESOURCE_SUFFIX = "_night";

 /**
  * 主题模式,分为日间模式和夜间模式
  */
 public enum ThemeMode {
  DAY, NIGHT
 }

 /**
  * 设置主题模式
  *
  * @param themeMode
  */
 public static void setThemeMode(ThemeMode themeMode) {
  if (mThemeMode != themeMode) {
   mThemeMode = themeMode;
   if (mThemeChangeListenerList.size() > 0) {
    for (OnThemeChangeListener listener : mThemeChangeListenerList) {
     listener.onThemeChanged();
    }
   }
  }
 }

 /**
  * 根据传入的日间模式的resId得到相应主题的resId,注意:必须是日间模式的resId
  *
  * @param dayResId 日间模式的resId
  * @return 相应主题的resId,若为日间模式,则得到dayResId;反之夜间模式得到nightResId
  */
 public static int getCurrentThemeRes(Context context, int dayResId) {
  if (getThemeMode() == ThemeMode.DAY) {
   return dayResId;
  }
  // 资源名
  String entryName = context.getResources().getResourceEntryName(dayResId);
  // 资源类型
  String typeName = context.getResources().getResourceTypeName(dayResId);
  HashMap cachedRes = sCachedNightResrouces.get(typeName);
  // 先从缓存中去取,如果有直接返回该id
  if (cachedRes == null) {
   cachedRes = new HashMap<>();
  }
  Integer resId = cachedRes.get(entryName + RESOURCE_SUFFIX);
  if (resId != null && resId != 0) {
   return resId;
  } else {
   //如果缓存中没有再根据资源id去动态获取
   try {
    // 通过资源名,资源类型,包名得到资源int值
    int nightResId = context.getResources().getIdentifier(entryName + RESOURCE_SUFFIX, typeName, context.getPackageName());
    // 放入缓存中
    cachedRes.put(entryName + RESOURCE_SUFFIX, nightResId);
    sCachedNightResrouces.put(typeName, cachedRes);
    return nightResId;
   } catch (Resources.NotFoundException e) {
    e.printStackTrace();
   }
  }
  return 0;
 }

 /**
  * 注册ThemeChangeListener
  *
  * @param listener
  */
 public static void registerThemeChangeListener(OnThemeChangeListener listener) {
  if (!mThemeChangeListenerList.contains(listener)) {
   mThemeChangeListenerList.add(listener);
  }
 }

 /**
  * 反注册ThemeChangeListener
  *
  * @param listener
  */
 public static void unregisterThemeChangeListener(OnThemeChangeListener listener) {
  if (mThemeChangeListenerList.contains(listener)) {
   mThemeChangeListenerList.remove(listener);
  }
 }

 /**
  * 得到主题模式
  *
  * @return
  */
 public static ThemeMode getThemeMode() {
  return mThemeMode;
 }

 /**
  * 主题模式切换监听器
  */
 public interface OnThemeChangeListener {
  /**
   * 主题切换时回调
   */
  void onThemeChanged();
 }
}

上面 ThemeManager 的代码基本上都有注释,想要看懂并不困难。其中最核心的就是 getCurrentThemeRes 方法了。在这里解释一下 getCurrentThemeRes 的逻辑。参数中的 dayResId 是日间模式的资源id,如果当前主题是日间模式的话,就直接返回 dayResId 。反之当前主题为夜间模式的话,先根据 dayResId 得到资源名称和资源类型。比如现在有一个资源为 R.color.colorPrimary ,那么资源名称就是 colorPrimary ,资源类型就是 color 。然后根据资源类型和资源名称去获取缓存。如果没有缓存,那么就要动态获取资源了。这里使用方法的是

context.getResources().getIdentifier(String name, String defType, String defPackage)

name 参数就是资源名称,不过要注意的是这里的资源名称还要加上后缀 “_night” ,也就是上面在 colors.xml 中定义的名称;
defType 参数就是资源的类型了。比如 color,drawable等;

defPackage 就是资源文件的包名,也就是当前 APP 的包名。

有了上面的这个方法,就可以通过 R.color.colorPrimary 资源找到对应的 R.color.colorPrimary_night 资源了。最后还要把找到的夜间模式资源加入到缓存中。这样的话以后就直接去缓存中读取,而不用再次去动态查找资源 id 了。

ThemeManager 中剩下的代码应该都是比较简单的,相信大家都可以看得懂了。

现在我们来看看 MainActivity 的代码:

public class MainActivity extends AppCompatActivity implements ThemeManager.OnThemeChangeListener {

 private TextView tv;
 private Button btn_theme;
 private RelativeLayout relativeLayout;
 private ActionBar supportActionBar;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  ThemeManager.registerThemeChangeListener(this);
  supportActiOnBar= getSupportActionBar();
  btn_theme = (Button) findViewById(R.id.btn_theme);
  relativeLayout = (RelativeLayout) findViewById(R.id.relativeLayout);
  tv = (TextView) findViewById(R.id.tv);
  btn_theme.setOnClickListener(new View.OnClickListener() {
   @Override
   public void onClick(View v) {
    ThemeManager.setThemeMode(ThemeManager.getThemeMode() == ThemeManager.ThemeMode.DAY
      &#63; ThemeManager.ThemeMode.NIGHT : ThemeManager.ThemeMode.DAY);
   }
  });
 }

 public void initTheme() {
  tv.setTextColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
  btn_theme.setTextColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.textColor)));
  relativeLayout.setBackgroundColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.backgroundColor)));
  // 设置标题栏颜色
  if(supportActionBar != null){
   supportActionBar.setBackgroundDrawable(new ColorDrawable(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary))));
  }
  // 设置状态栏颜色
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
   Window window = getWindow();
   window.setStatusBarColor(getResources().getColor(ThemeManager.getCurrentThemeRes(MainActivity.this, R.color.colorPrimary)));
  }
 }

 @Override
 public void onThemeChanged() {
  initTheme();
 }

 @Override
 protected void onDestroy() {
  super.onDestroy();
  ThemeManager.unregisterThemeChangeListener(this);
 }

}

在 MainActivity 中实现了 OnThemeChangeListener 接口,这样就可以在主题改变的时候执行回调方法。然后在 initTheme() 中去重新设置 UI 的相关颜色属性值。还有别忘了要在 onDestroy() 中移除 ThemeChangeListener 。

最后就来看看第三种方法的效果吧:

也许有人会说和前两种方法的效果没什么差异啊,但是仔细看就会发现前面两种方法在切换模式的瞬间会有短暂黑屏现象存在,而第三种方法没有。这是因为前两种方法都要调用 recreate() 。而第三种方法不需要 Activity 重新创建,使用回调的方法来实现。

三个方法对比

到了这里,按照套路应该是要总结的时候了。那么就根据上面给的三种方法来一个简单的对比吧:

setTheme 方法:可以配置多套主题,比较容易上手。除了日/夜间模式之外,还可以有其他五颜六色的主题。但是需要调用 recreate() ,切换瞬间会有黑屏闪现的现象;

UiMode 方法:优点就是 Android Support Library 中已经支持,简单规范。但是也需要调用 recreate() ,存在黑屏闪现的现象;

动态获取资源 id ,回调接口:该方法使用起来比前两个方法复杂,另外在回调的方法中需要设置每一项 UI 相关的属性值。但是不需要调用 recreate() ,没有黑屏闪现的现象。

总结

以上就是这篇文章的全部内容了,希望能对各位Android开发者们有所帮助。


推荐阅读
  • OpenCV 2.4.9 源码解析:级联分类器的错误率与尺寸分析 ... [详细]
  • 本文深入探讨了 AdoDataSet RecordSet 的序列化与反序列化技术,详细解析了将 RecordSet 转换为 XML 格式的方法。通过使用 Variant 类型变量和 TStringStream 流对象,实现数据集的高效转换与存储。该方法不仅提高了数据传输的灵活性,还增强了数据处理的兼容性和可扩展性。 ... [详细]
  • Maven入门指南深入解析了Maven的核心概念与基本功能。本文详细介绍了Maven在项目构建、文档生成、报告生成、依赖管理、源代码控制(SCM)、项目发布与分发以及邮件列表管理等方面的应用。Maven通过采用标准目录结构和“约定优于配置”的原则,简化了项目的配置与管理,提高了开发效率。 ... [详细]
  • 利用IDEA高效构建Maven Spring MVC项目环境
    本文详细介绍了如何使用IntelliJ IDEA高效搭建Maven Spring MVC项目环境。首先,通过创建一个新的Maven项目,设置好GroupId、ArtifactId和Version等基本信息。接着,配置项目的依赖和插件,确保Spring MVC框架能够顺利集成。最后,通过IDEA的内置工具完成项目的初始化和测试,为后续开发打下坚实基础。 ... [详细]
  • 深入解析Android中图像资源的内存占用问题及其优化策略
    在Android开发过程中,图像资源的内存占用是一个值得关注的问题。本文将探讨图像内存占用与哪些因素相关,包括设备性能的影响,并提供一系列优化策略,帮助开发者有效管理图像资源,提升应用性能。 ... [详细]
  • 本文详细解析了如何利用Appium与Python在真实设备上执行测试示例的方法。首先,需要开启手机的USB调试功能;其次,通过数据线将手机连接至计算机并授权USB调试权限。最后,在命令行工具中验证设备连接状态,确保一切准备就绪,以便顺利进行测试。 ... [详细]
  • 将 Eclipse 中的 Java Web 项目迁移至 IntelliJ IDEA 并配置 Tomcat 环境
    为了适应更高效的工作流程,本文详细介绍了如何将基于Eclipse构建的Java Web项目迁移到IntelliJ IDEA,并在新环境中配置Tomcat服务器,以确保项目的顺利运行。此过程不仅涉及项目文件的转移,还包括解决可能遇到的兼容性问题和环境配置挑战。通过本文的指导,开发者可以轻松实现从Eclipse到IntelliJ IDEA的过渡,提升开发效率。 ... [详细]
  • 本文深入探讨了ASP.NET Web API与RESTful架构的设计与实现。ASP.NET Web API 是一个强大的框架,能够简化HTTP服务的开发,使其能够广泛支持各种客户端设备。通过详细分析其核心原理和最佳实践,本文为开发者提供了构建高效、可扩展且易于维护的Web服务的指导。此外,还讨论了如何利用RESTful原则优化API设计,确保系统的灵活性和互操作性。 ... [详细]
  • Joomla!软件介绍【Joomla!概括介绍】国外相当知名的内容管理系统。【Joomla!基本介绍】Joomla!是一套在国外相当知名的内容管理系统(ContentManagem ... [详细]
  • 设置与优化Windows Server 2003网络环境配置
    安装好了系统并部署了多台客户机,接下来配置网络,是所有的计算机能够联通 ... [详细]
  • 通过一张截图深入解析字节跳动的 Java 开发实力
    在与一位来自字节跳动的朋友交流时了解到,根据他们近期招聘Java工程师的经验,大多数候选人往往在工作3年后会遇到一个难以跨越的瓶颈期。这是因为在职业生涯的这个阶段,许多工程师的技术深度和广度已经达到了一定的水平,但要进一步提升则需要更多的挑战和学习机会。字节跳动作为一家技术驱动的公司,通过严格的面试流程和实际项目经验,能够更好地评估候选人的技术水平和发展潜力。 ... [详细]
  • 出现情况:在服务器开启一段时间后,再调用数据库连接时就会报这个错,或者网络不稳定是进行数据库查询时也会报这个异常。 异常截图: ... [详细]
  • Maven在不同环境下的自动化构建与部署策略:测试与生产环境的定制化打包方案
    环境配置 在你的pom.xml文件中添加如下配置:PROD ... [详细]
  • PHP中如何使用hidef代替define优化效率?本文主要介绍了PHP中使用hidef扩展代替define提高性能,本文着重测试hidef的性能,同时提供了实例。希望对大家有所帮 ... [详细]
  • 初探SpringMVC框架:首日入门指南
    2019独角兽企业重金招聘Python工程师标准1.搭建环境2.如何完成Controller和Viewer的映射3.如何把值传递给Controller4.Controller ... [详细]
author-avatar
丙尔金开发_448
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有