文章大綱
引言
相信Android的日間/夜間模式切換相信大家在平時使用 APP 的過程中都遇到過,實現日間/夜間模式切換的方案也有許多種(比如說在setContentView之前setTheme、遍歷ViewTree動態設置、),在我們Android 5.0版本之後Framework 層下的android.support.v7包中就自帶日夜間模式的切換框架,可以幫助我們靈活實現切換,但但僅僅適用於僅僅支持固定兩套皮膚的切換,而且需要重啓App或者Activity,所以呢,對於用戶體驗來說可能不那麼好,但是在一些特定的需求下是成本最低實現換膚的思路之一,所以瞭解下也無妨,
一、系統自帶換膚框架概述
在Android 5.0之後Framework 在以前原有res 目錄的自動適配的基礎上,新增了名爲values-night、drawable-night等目錄的自動適配支持,並且通過AppCompatDelegate 提供了切換日夜間模式的API——AppCompatDelegate.setDefaultNightMode(int mode),其中mode有四個可選值:
- MODE_NIGHT_NO—— 使用亮色(light)主題,不使用夜間模式
- MODE_NIGHT_YES——使用暗色(dark)主題,使用夜間模式
- MODE_NIGHT_AUTO——根據當前時間自動切換 亮色(light)/暗色(dark)主題
- MODE_NIGHT_FOLLOW_SYSTEM(默認選項)——設置爲跟隨系統,通常爲MODE_NIGHT_NO
通過調用這個API告知系統當前希望是以何種模式,在App重啓或者Activity recreate時系統會像根據適配屏幕分辨率一樣,自動去對應的目錄下加載資源,從而完成了換膚。
二、實現換膚
1、引入support V7的依賴
implementation 'com.android.support:appcompat-v7:28.0.0'
2、在res目錄下新建兩套資源目錄
在res目錄下,新建名爲 values-night 目錄,當然還可以新建其他類型的目錄,比如說drawable-night等,總之就根據系統預訂的規範來命名,也可以直接在styles文件下配置,在加載資源的時候系統就會自動去進行適配。
3、調用AppCompatDelegate.setDefaultNightMode(int mode)
調用AppCompatDelegate.setDefaultNightMode(int mode)設置模式,調用之後還需要重啓App或者recreate Activity。
4、使用觀察者模式實現"即時"更新
通過以上三步,就可以完成Activity更新了,但是呢,如何去讓多個Activity 都能在我點擊換膚的時候,都能讓多個Activity同時更新呢,也有很多方案,通過廣播、通過監聽Activity的週期、通過註解等等,這裏我使用觀察者模式,但是並不意味着這是最佳的方案。
4.1、創建被觀察者角色
package com.crazymo.replaceskin.core;
import android.app.AlarmManager;
import android.app.Application;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import com.crazymo.replaceskin.MainActivity;
import java.util.Observable;
/**
* @author : Crazy.Mo
*/
public class SkinManager extends Observable {
private static SkinManager instance;
/**
* Activity生命週期回調
*/
private MoActivityLifecycleCallbacks skinActivityLifecycle;
private Application mContext;
/**
* 初始化 必須在Application中先進行初始化
*
* @param application
*/
public static void init(Application application) {
if (instance == null) {
synchronized (SkinManager.class) {
if (instance == null) {
instance = new SkinManager(application);
}
}
}
}
public static SkinManager getInstance() {
return instance;
}
private SkinManager(Application application) {
mContext = application;
//註冊Activity生命週期
skinActivityLifecycle = new MoActivityLifecycleCallbacks();
mContext.registerActivityLifecycleCallbacks(skinActivityLifecycle);
}
/**
* 在需要更新皮膚時, 通知其他觀察者,進行更新
*/
public void updateSkin(){
setChanged();
notifyObservers(null);
}
/**
* 直接重啓
*/
public void restart(){
Intent intent = new Intent(mContext, MainActivity.class);
PendingIntent restartIntent = PendingIntent.getActivity(
mContext.getApplicationContext(), 0, intent, PendingIntent.FLAG_ONE_SHOT);
AlarmManager mgr = (AlarmManager) mContext.getSystemService(Context.ALARM_SERVICE);
mgr.set(AlarmManager.RTC, System.currentTimeMillis() + 100,restartIntent);
/** 1秒鐘後重啓應用 */
/** 結束線程,一般與finishAll()一起使用 */
android.os.Process.killProcess(android.os.Process.myPid());
}
}
4.2、創建觀察者角色
package com.crazymo.replaceskin.core;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import java.util.Observable;
import java.util.Observer;
/**
* @author : Crazy.Mo
*/
public class BaseSkinActivity extends AppCompatActivity implements Observer {
@Override
public void update(Observable o, Object arg) {
if((boolean) Util.SharePreference.get(this,"config_skin","isNight",false)) {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
Util.SharePreference.save(this,"config_skin","isNight",true);
}else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
Util.SharePreference.save(this,"config_skin","isNight",false);
}
recreate();
/////SkinManager.getInstance().restart();
}
}
4.3、註冊觀察者
此處我是通過Application.ActivityLifecycleCallbacks監聽Activity的生命週期的,並且在onCreate時添加觀察者,在onDestroy時移除觀察者。
package com.crazymo.replaceskin.core;
import android.app.Activity;
import android.app.Application;
import android.os.Bundle;
/**
* @author : Crazy.Mo
*/
public class MoActivityLifecycleCallbacks implements Application.ActivityLifecycleCallbacks {
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
//添加觀察者
SkinManager.getInstance().addObserver((BaseSkinActivity)activity);
}
@Override
public void onActivityStarted(Activity activity) {
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
//移除觀察者
SkinManager.getInstance().deleteObserver((BaseSkinActivity)activity);
}
}
4.4、使用
- 初始化
package com.crazymo.replaceskin;
import android.app.Application;
import android.support.v7.app.AppCompatDelegate;
import android.util.Log;
import com.crazymo.replaceskin.core.SkinManager;
import com.crazymo.replaceskin.core.Util;
/**
* @author : Crazy.Mo
*/
public class MoApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
SkinManager.init(this);
if((Boolean) Util.SharePreference.get(this,"config_skin","isNight",false)) {
Log.e("cmskin","日間");
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO);
Util.SharePreference.save(this,"config_skin","isNight",false);
}else {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES);
Log.e("cmskin","夜間");
Util.SharePreference.save(this,"config_skin","isNight",true);
}
}
}
- 通知觀察者
public void replace(View view) {
Util.SharePreference.save(this,"config_skin","isNight",false);
SkinManager.getInstance().updateSkin();
}
以上就是換膚的核心代碼了,不過這種方案有一些缺點,比如說以上代碼使用recreate進行刷新的話,只會更新在執行換膚前的打開過且未被銷燬回收的Activity(因爲我這裏偷懶是在Activity註冊觀察者的,你也可以通過註解來註冊觀察者),只能接收兩套皮膚;如果使用重啓App方式的話,可能用戶體驗不太良好,總之謹慎直接使用,原本不打算寫這篇的,但是發現有些文章把換膚說得糊里糊塗的,亂七八糟,就分享下,順便再一次介紹下觀察者模式,至於類似雲村、酷狗那種支持皮膚包的換膚,無侵入的原理,也很簡單,後面看情況,我也寫個系列文章總結並且手寫實現吧。源碼傳送門或者碼雲