Переключение тем в Android приложении
Наверное каждому в этом мире охота иметь право выбора. Даже если это лишь выбор цветовой схемы приложения. Тёмная тема легче воспринимается в вечернее время, а также положительно влияет на энергосбережение (в частности на AMOLED дисплеях)
Когда-то я хотел сделать изменение темы в приложении "на лету", но не мог из-за своих малых знаний (от силы 2 месяца программирования). Всё приходит со временем, и, возможно, это тот самый момент, когда вы овладеете полезным навыком.
Хочу сразу предупредить:
Пример будет предназначен для новичков, имеющих малый (менее 1 года) опыт разработки под Андроид. В проекте не будет никакой сложной архитектуры, типо MVP, которая только лишь собьёт новичка с толку.
Подразумевается, что:
- Вы уже знакомы с методами жизненного цикла Activity, умеете запускать и управлять их состоянием
- Вы уже знакомы с классом Fragment, умеете помещать несколько фрагментов внутри одной Activity, и управлять ими
Подготовка
Перейдите в каталог ресурсов вашего проекта, найдите файл styles.xml, и добавьте туда новые темы для приложения.
Создайте класс App, который будет наследоваться от android.app.Application
, и напишите следующий код:
import android.app.Application; import android.content.SharedPreferences; import android.preference.PreferenceManager; public class App extends Application { private static App instance = null; private SharedPreferences preferences; public App() { super(); } @Override public void onCreate() { super.onCreate(); instance = this; // Если в вашем приложении есть экран настроек, замените preferences на нужное имя файла PreferenceManager.setDefaultValues(this, R.xml.preferences, false); } public static App getInstance(){ if (instance == null) instance = new App(); return instance; } public SharedPreferences getPreferences(){ if (preferences == null) preferences = PreferenceManager.getDefaultSharedPreferences(this); return preferences; } }
Зарегистрируйте класс в файле AndroidManifest.xml
<application android:name=".App" ... ... .../> ... ... ... </application>
Теперь пройдемся по методам. Мы создали экземпляр класса Application, который сам по себе является синглтоном. Переопределив метод onCreate()
, присвоили значение переменной instance
.
В методе getInstance()
мы просто возвращаем переменную instance, которая является экземпляром класса App.
В методе getPreferences()
мы возвращаем экземпляр SharedPreferences
, необходимый для хранения настроек.
Создайте новый класс, который будет отвечать за смену темы. Я назову его ThemeWrapper.java. напишите какой код:
import android.app.Activity; public abstract class ThemeWrapper { public enum Theme { LIGHT, DARK } public static void applyTheme(Activity ctx) { int theme; switch (Theme.values()[getThemeIndex()]) { case LIGHT: theme = R.style.AppTheme; break; case DARK: theme = R.style.AppTheme_Dark; break; default: theme = R.style.AppTheme; break; } ctx.setTheme(theme); } private static int getThemeIndex() { return Integer.parseInt(App.getInstance().getPreferences().getString("ui.theme", String.valueOf(ThemeWrapper.Theme.LIGHT.ordinal()))); } }
Самостоятельно импортируйте классы R и App.
Внедрение кода
Если ваше приложение использует более чем одно Activity, то лучше всего будет создать некий базовый класс, от которого всё будет наследоваться. Я назвал такой класс BaseActivity.java
import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.app.AppCompatActivity; public class BaseActivity extends AppCompatActivity { //Theme private final BroadcastReceiver mThemeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (SettingsActivity.class.equals(BaseActivity.this.getClass())){ finish(); startActivity(getIntent()); overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out); } else recreate(); } }; public BaseActivity(){ } @Override protected void onCreate(Bundle savedInstanceState) { //Приёмник сигналов от SettingsActivity LocalBroadcastManager.getInstance(this).registerReceiver(mThemeReceiver, new IntentFilter("ru.svolf.action.REFRESH_THEME")); ThemeWrapper.applyTheme(this); super.onCreate(savedInstanceState); } @Override public void onResume() { super.onResume(); } @Override protected void onDestroy() { LocalBroadcastManager.getInstance(this).unregisterReceiver(mThemeReceiver); super.onDestroy(); } }
Импортируйте класс ThemeWrapper. Если у вас уже есть Activity настроек, то импортируйте и её, в противном случае создайте с нуля.
/res/layout/activity_settings.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto" android:orientation="vertical"> <android.support.design.widget.AppBarLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:theme="@style/AppTheme.AppBarOverlay"> <android.support.v7.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" app:popupTheme="?popupTheme" /> </android.support.design.widget.AppBarLayout> <FrameLayout android:id="@+id/frame_container" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
Возможно вам придется добавить библиотекиAppCompat
иDesign
, для корректной работы
SettingsActivity.java
import android.os.Bundle; import android.support.v7.widget.Toolbar; import android.view.MenuItem; import ru.SnowVolf.devtheme.R; public class SettingsActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_settings); Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar); setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); getFragmentManager() .beginTransaction() .replace(R.id.frame_container, new SettingsFragment()) .commit(); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()){ case android.R.id.home:{ finish(); } } return super.onOptionsItemSelected(item); } }
Создадим класс SettingsFragment.java с содержимым настроек
import android.content.Intent; import android.os.Bundle; import android.preference.ListPreference; import android.preference.PreferenceFragment; import android.content.SharedPreferences; import android.support.annotation.Nullable; import android.support.v4.content.LocalBroadcastManager; public class SettingsFragment extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); addPreferencesFromResource(R.xml.preferences); //Регистрация слушателя настроек getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); //Ставим текущее значение как подзаголовок настройки setCurrentValue((ListPreference) findPreference("ui.theme")); } @Override public void onDestroy() { super.onDestroy(); //Отключение слушателя настроек getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); } private void setCurrentValue(ListPreference listPreference){ listPreference.setSummary(listPreference.getEntry()); } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { switch (key) { case "ui.theme": { setCurrentValue((ListPreference) findPreference(key)); LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(new Intent("ru.svolf.action.REFRESH_THEME")); break; } } } }
В onCreate мы регистрируем слушателя настроек, для того чтобы данные на экране обновлялись сразу же.
В методе onSharedPreferenceChanged
посылаем сигнал для того, чтобы activity пересоздалась. Чтобы это было менее заметно пользователю, мы добавили метод переопределения системных анимаций в BaseActivity (overridePendingTransaction
)
/res/xml/preferences.xml разметка для экрана настроек
<?xml version="1.0" encoding="utf-8"?> <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> <ListPreference android:defaultValue="0" android:entries="@array/theme_names" android:entryValues="@array/theme_values" android:key="ui.theme" android:title="App theme" /> </PreferenceScreen>
/res/values/arrays.xml Файл с названиями настроек
<?xml version="1.0" encoding="utf-8"?> <resources> <string-array name="theme_values" translatable="false"> <item>0</item> <item>1</item> </string-array> <string-array name="theme_names"> <item>Light</item> <item>Dark</item> </string-array> </resources>
Ещё раз проверьте, всё ли Activity вы зарегистрировали в манифесте. Сделайте это, если забыли.
Всё. Теперь вы можете просмотреть на плоды своих трудов, просто скомпилировав проект. Если вы всё сделали правильно, то приложение будет работать как надо.