Androidは日本のナイトモードの理解を深めます。


本稿では、日中/夜間モードの切り替えを実現する3つの案が提示されています。この3つの案を総合すると、文章の幅が長すぎるかもしれません。
    1、setTheme の方法を使用して、Activity にテーマを再設定させる。
    2、Android Support Library UiMode を設定して、昼間/夜間モードの切り替えをサポートする。
    3、資源IDマッピングにより、カスタムThemeChangeListener インターフェースをフィードバックして、昼間/夜間モードの切り替えを処理する。
一、setTheメソッドを使う
まず、setTheme の方法を使って、昼と夜のモード切り替えを実現する方式を見てみましょう。このような方案の構想はとても簡単で、ユーザーがナイトモードを選択する時、Activityはナイトモードのテーマに設定して、その後Activity recreate()方法を呼び出してもう一度作成すればいいです。
手をつけましょう。colors.xmlの中で2つの色を定義して、それぞれ昼と夜のテーマ色を表します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <color name="colorPrimary">#3F51B5</color>
 <color name="colorPrimaryDark">#303F9F</color>
 <color name="colorAccent">#FF4081</color>

 <color name="nightColorPrimary">#3b3b3b</color>
 <color name="nightColorPrimaryDark">#383838</color>
 <color name="nightColorAccent">#a72b55</color>
</resources>
その後、stylies.xmlで二つのテーマを定義します。つまり、日中テーマと夜間テーマです。

<resources>

 <!-- Base application theme. -->
 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="colorAccent">@color/colorAccent</item>
  <item name="android:textColor">@android:color/black</item>
  <item name="mainBackground">@android:color/white</item>
 </style>

 <style name="NightAppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/nightColorPrimary</item>
  <item name="colorPrimaryDark">@color/nightColorPrimaryDark</item>
  <item name="colorAccent">@color/nightColorAccent</item>
  <item name="android:textColor">@android:color/white</item>
  <item name="mainBackground">@color/nightColorPrimaryDark</item>
 </style>

</resources>
テーマのmainBackground 属性は私達のカスタム属性で、背景色を表します。

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <attr name="mainBackground" format="color|reference"></attr>
</resources>
次はレイアウトを見てみます。main.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
 xmlns:tools="http://schemas.android.com/tools"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:background="?attr/mainBackground"
 android:paddingBottom="@dimen/activity_vertical_margin"
 android:paddingLeft="@dimen/activity_horizontal_margin"
 android:paddingRight="@dimen/activity_horizontal_margin"
 android:paddingTop="@dimen/activity_vertical_margin"
 tools:context="com.yuqirong.themedemo.MainActivity">

 <Button
  android:id="@+id/btn_theme"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:text="   /    " />

 <TextView
  android:id="@+id/tv"
  android:layout_below="@id/btn_theme"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:gravity="center_horizontal"
  android:text="  setTheme()   " />

</RelativeLayout>
「RelativeLayout」のandroid:background 属性では、「?atr/manBackgroundは、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) ? 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)を呼び出して関連データを復元しますので、Activityを再作成してから使用するためにtheme の値を保存します。
     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です。

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <color name="colorPrimary">#3F51B5</color>
 <color name="colorPrimaryDark">#303F9F</color>
 <color name="colorAccent">#FF4081</color>
 <color name="textColor">#FF000000</color>
 <color name="backgroundColor">#FFFFFF</color>
</resources>
values/colors.xmlを除いて、私達はもう一つのvalues-night/colors.xmlファイルを作成して、夜間モードの色を設定します。その中で<カラー>のnameはvalues/colors.xmlの中の対応が必要です。

<?xml version="1.0" encoding="utf-8"?>
<resources>
 <color name="colorPrimary">#3b3b3b</color>
 <color name="colorPrimaryDark">#383838</color>
 <color name="colorAccent">#a72b55</color>
 <color name="textColor">#FFFFFF</color>
 <color name="backgroundColor">#3b3b3b</color>
</resources>
stylies.xmlでcolors.xmlで定義された色を引用します。

<resources>

 <!-- Base application theme. -->
 <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
  <!-- Customize your theme here. -->
  <item name="colorPrimary">@color/colorPrimary</item>
  <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
  <item name="colorAccent">@color/colorAccent</item>
  <item name="android:textColor">@color/textColor</item>
  <item name="mainBackground">@color/backgroundColor</item>
 </style>

</resources>
activityメイン.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:明るいテーマを使って、ナイトモードを使用しません。
    2、MODE_NIGHT_YES:暗いテーマを使って、ナイトモードを使います。
    3、MODE_NIGHT_AUTO:現在の時間に応じて自動的に明るい色(light)/暗いテーマが切り替わります。
    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
      ? AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
    //       recreate      
    recreate();
   }
  });
 }

}
UiMode 案の実現の効果図を見てみます。

前の2つの方法から言えば、配置が簡単で、最後の実現効果もほぼ同じです。しかし、デメリットはrecreate()を呼び出して有効にすることです。Activityを再作成するには、いくつかの状態の保存に関わる必要があります。これはちょっと難しいです。ですから、私たちは一緒に第三の解決方法を見てみましょう。
リソースIDマッピングにより、フィードバックインターフェース
第3の方法の考え方は、設定された主題に従ってリソースIDのマッピングを動的に取得し、その後、UIに関連する属性値を設定させることである。私達はここでまず決めます。夜間モードの資源はネーミングに全部拡張子を付けます。night」など、昼間モードの背景色をカラーと名付けています。backgroundは、それに対応するナイトモードの背景資源をカラーと名づけます。background_nightはい、次は私達のDemoの必要なcolors.xmlです。

<?xml version="1.0" encoding="utf-8"?>
<resources>
 
 <color name="colorPrimary">#3F51B5</color>
 <color name="colorPrimary_night">#3b3b3b</color>
 <color name="colorPrimaryDark">#303F9F</color>
 <color name="colorPrimaryDark_night">#383838</color>
 <color name="colorAccent">#FF4081</color>
 <color name="colorAccent_night">#a72b55</color>
 <color name="textColor">#FF000000</color>
 <color name="textColor_night">#FFFFFF</color>
 <color name="backgroundColor">#FFFFFF</color>
 <color name="backgroundColor_night">#3b3b3b</color>
 
</resources>
カラーごとに対応が見られます。night」とマッチします。
ここを見たら、なぜ対応しているのかという質問があります。night」?いったいどんな方式で日/夜のモードを設定しますか?以下はThe Managerがあなたのために解答します。

public class ThemeManager {

 //        
 private static ThemeMode mThemeMode = ThemeMode.DAY;
 //        
 private static List<OnThemeChangeListener> mThemeChangeListenerList = new LinkedList<>();
 //        ,key :     ,  <key:    , value:int >
 private static HashMap<String, HashMap<String, Integer>> 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<String, Integer> 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 の論理を説明します。パラメータの中のday Residは、日中モードの資源idであり、現在のテーマがデイモードであれば、day Residに直接戻ります。逆に現在のテーマがナイトモードの場合は、まずdayResidに従ってリソース名とリソースタイプを取得します。例えば、現在はR.color.colorPrimary資源がありますが、資源名はcolorPrimaryで、資源タイプはカラーです。その後、リソースタイプとリソース名に基づいてキャッシュを取得する。キャッシュがないと、リソースを動的に取得します。ここの使い方は

context.getResources().getIdentifier(String name, String defType, String defPackage)
nameパラメータは資源名ですが、ここの資源名には拡張子が付けられています。night」とは、colors.xmlで定義されている名前のことです。
defTypeパラメータはリソースのタイプです。例えば、drawableなど;
defPackageとは、リソースファイルのパケット名であり、現在のAPPのパケット名である。
上記の方法があれば、R.color.colorPrimaryリソースを通じて対応するR.color.colorPrimary_night リソースを見つけることができる。最後に見つけたナイトモードのリソースをキャッシュに追加します。このようにすれば、今後は直接キャッシュに行って読み込むことができ、また動的に資源IDを探す必要がなくなります。
The Managerに残っているコードは全部簡単なはずです。みんなが分かります。
今から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
      ? 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() でThe meChange Listenerを除去します。
最後に第三の方法の効果を見てみましょう。

前の2つの方法とは効果があまり変わらないという人もいるかもしれませんが、よく見ると、前の2つの方法はモードを切り替える瞬間に短い黒スクリーンがあります。第3の方法はありません。これは、前の2つの方法がいずれもrecreate() を呼び出すためである。第三の方法はActivityの再作成を必要とせず、フィードバックの方法で実現される。
三つの方法の対比
ここに来たら、コースによってまとめるべきです。上にあげた3つの方法によって簡単な対比をしてみましょう。setTheme 方法:複数のテーマを配置することができ、より使いやすいです。日/ナイトモード以外にも、他のカラフルなテーマがあります。しかし、recreate()を呼び出す必要があります。切り替えの瞬間にブラックスクリーンが現れます。UiMode 方法:利点はAndroid Support Libraryで既にサポートされています。簡単な仕様です。しかし、recreate() を呼び出す必要があります。ブラックスクリーンのフラッシュ現象があります。
リソースIDを動的に取得し、フィードバックインターフェース:この方法は前の2つの方法より複雑であり、また、各UIに関する属性値を設定する必要がある。しかし、recreate() を呼び出す必要はなく、ブラックスクリーンのフラッシュがない。
締め括りをつける
以上がこの文章のすべての内容です。Androidの開発者の皆さんに役に立ちたいです。