Переключение тем в 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 вы зарегистрировали в манифесте. Сделайте это, если забыли.
Всё. Теперь вы можете просмотреть на плоды своих трудов, просто скомпилировав проект. Если вы всё сделали правильно, то приложение будет работать как надо.