日/夜间模式的切换

谢谢各位大佬的欣赏!

不求关注、不求喜欢、只求能够帮助各位学习,提升自己!

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

我给出了三种实现日间/夜间模式切换的方案:

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

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

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

三种方案综合起来可能导致文章的篇幅过长,请耐心阅读。

:2:

使用 setTheme 方法

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

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

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

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

@color/colorPrimary@color/colorPrimaryDark@color/colorAccent@android:color/black@android:color/white@color/nightColorPrimary@color/nightColorPrimaryDark@color/nightColorAccent@android:color/white@color/nightColorPrimaryDark

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

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

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

最后就是 MainActivity 的代码:

publicclassMainActivityextendsAppCompatActivity{// 默认是日间模式privateinttheme = R.style.AppTheme;@OverrideprotectedvoidonCreate(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(newView.OnClickListener() {@OverridepublicvoidonClick(View v){                theme = (theme == R.style.AppTheme) ? R.style.NightAppTheme : R.style.AppTheme;                MainActivity.this.recreate();            }        });    }@OverrideprotectedvoidonSaveInstanceState(Bundle outState){super.onSaveInstanceState(outState);        outState.putInt("theme", theme);    }@OverrideprotectedvoidonRestoreInstanceState(Bundle savedInstanceState){super.onRestoreInstanceState(savedInstanceState);        theme = savedInstanceState.getInt("theme");    }}

在 MainActivity 中有几点要注意一下:

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

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

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

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

setTheme()效果图gif

使用 Android Support Library 中的 UiMode 方法

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

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

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

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

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

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

@color/colorPrimary@color/colorPrimaryDark@color/colorAccent@color/textColor@color/backgroundColor

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

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

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

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

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

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

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

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

publicclassMainActivityextendsAppCompatActivity{@OverrideprotectedvoidonCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Button btn_theme = (Button) findViewById(R.id.btn_theme);        btn_theme.setOnClickListener(newView.OnClickListener() {@OverridepublicvoidonClick(View v){intcurrentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;                getDelegate().setLocalNightMode(currentNightMode == Configuration.UI_MODE_NIGHT_NO                        ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);// 同样需要调用recreate方法使之生效recreate();            }        });    }}

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

UiMode的效果图gif

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

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

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

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

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

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

publicclassThemeManager{// 默认是日间模式privatestaticThemeMode mThemeMode = ThemeMode.DAY;// 主题模式监听器privatestaticList mThemeChangeListenerList =newLinkedList<>();// 夜间资源的缓存,key : 资源类型, 值privatestaticHashMap> sCachedNightResrouces =newHashMap<>();// 夜间模式资源的后缀,比如日件模式资源名为:R.color.activity_bg, 那么夜间模式就为 :R.color.activity_bg_nightprivatestaticfinalString RESOURCE_SUFFIX ="_night";/**

    * 主题模式,分为日间模式和夜间模式

    */publicenumThemeMode {        DAY, NIGHT    }/**    * 设置主题模式    *    *@paramthemeMode    */publicstaticvoidsetThemeMode(ThemeMode themeMode){if(mThemeMode != themeMode) {            mThemeMode = themeMode;if(mThemeChangeListenerList.size() >0) {for(OnThemeChangeListener listener : mThemeChangeListenerList) {                    listener.onThemeChanged();                }            }        }    }/**    * 根据传入的日间模式的resId得到相应主题的resId,注意:必须是日间模式的resId    *    *@paramdayResId 日间模式的resId    *@return相应主题的resId,若为日间模式,则得到dayResId;反之夜间模式得到nightResId    */publicstaticintgetCurrentThemeRes(Context context,intdayResId){if(getThemeMode() == ThemeMode.DAY) {returndayResId;        }// 资源名String entryName = context.getResources().getResourceEntryName(dayResId);// 资源类型String typeName = context.getResources().getResourceTypeName(dayResId);        HashMap cachedRes = sCachedNightResrouces.get(typeName);// 先从缓存中去取,如果有直接返回该idif(cachedRes ==null) {            cachedRes =newHashMap<>();        }        Integer resId = cachedRes.get(entryName + RESOURCE_SUFFIX);if(resId !=null&& resId !=0) {returnresId;        }else{//如果缓存中没有再根据资源id去动态获取try{// 通过资源名,资源类型,包名得到资源int值intnightResId = context.getResources().getIdentifier(entryName + RESOURCE_SUFFIX, typeName, context.getPackageName());// 放入缓存中cachedRes.put(entryName + RESOURCE_SUFFIX, nightResId);                sCachedNightResrouces.put(typeName, cachedRes);returnnightResId;            }catch(Resources.NotFoundException e) {                e.printStackTrace();            }        }return0;    }/**    * 注册ThemeChangeListener    *    *@paramlistener    */publicstaticvoidregisterThemeChangeListener(OnThemeChangeListener listener){if(!mThemeChangeListenerList.contains(listener)) {            mThemeChangeListenerList.add(listener);        }    }/**    * 反注册ThemeChangeListener    *    *@paramlistener    */publicstaticvoidunregisterThemeChangeListener(OnThemeChangeListener listener){if(mThemeChangeListenerList.contains(listener)) {            mThemeChangeListenerList.remove(listener);        }    }/**    * 得到主题模式    *    *@return*/publicstaticThemeModegetThemeMode(){returnmThemeMode;    }/**

    * 主题模式切换监听器

    */publicinterfaceOnThemeChangeListener{/**

        * 主题切换时回调

        */voidonThemeChanged();    }}

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

context.getResources().getIdentifier(Stringname,StringdefType,StringdefPackage)

name参数就是资源名称,不过要注意的是这里的资源名称还要加上后缀 “_night” ,也就是上面在 colors.xml 中定义的名称;

defType参数就是资源的类型了。比如 color,drawable等;

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

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

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

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

publicclassMainActivityextendsAppCompatActivityimplementsThemeManager.OnThemeChangeListener{privateTextView tv;privateButton btn_theme;privateRelativeLayout relativeLayout;privateActionBar supportActionBar;@OverrideprotectedvoidonCreate(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(newView.OnClickListener() {@OverridepublicvoidonClick(View v){                ThemeManager.setThemeMode(ThemeManager.getThemeMode() == ThemeManager.ThemeMode.DAY                        ? ThemeManager.ThemeMode.NIGHT : ThemeManager.ThemeMode.DAY);            }        });    }publicvoidinitTheme(){        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(newColorDrawable(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)));        }    }@OverridepublicvoidonThemeChanged(){        initTheme();    }@OverrideprotectedvoidonDestroy(){super.onDestroy();        ThemeManager.unregisterThemeChangeListener(this);    }}

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

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

动态获取资源id的效果图gif

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

3:

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

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

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

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容