Theming improvements (#502)

* Split theme definitions into day and night

* Add support for Night Mode in code

* Add theme chooser in preferences

* Fix translations

* Adjust IDs

* Adjust preferences for custom themes

* UI tweaks for custom theme support

* Added code for custom theme support 🍅

* Fixed resource display in Kotlin 🍅

* Restored styles

* Updated strings

* Fixed getIdentifier() to fit into setTheme()

* Removed redundant resources

* Reset default theme to "Dusky"

* Fixed night mode handler to maintain compatibility

* Refactor functions to use helper methods

* Added license block

* Added preview to theme selector

* Added color identifier getter helper method

* Fixed reference in AccountMediaFragment

* Cleanup

* Fixed navbar foreground not changing color

* Fix fallback theme switch(){}

* Enable location-based daylight trigger

* Cleanup

* Modified theming strategy to reduce clutter in preferences

* Updated translations for latest version

* Removed "Default" theme flavor from settings

* Updated Polish translations 🇵🇱

* Modified TwilightManager handling code to support Android M's UiModeManager features and moved it to its own function

* Updated Polish translations 🇵🇱

* Cleanup; Fixed hardcoded string

* Added missing escape in string

* Removed permission request dialog.

As we now use native UiModeManager APIs that don't need special permission for Android 6.0 and above, we no longer need to bother user with Android M+ specific location permission request dialog.

* Increased readability of ThemeUtil class

* Refactored ThemeUtils.setAppNightMode method

* Cleanup
This commit is contained in:
remi6397 2018-01-20 13:39:01 +01:00 committed by Konrad Pozniak
commit 11105f4aac
28 changed files with 341 additions and 142 deletions

View file

@ -37,6 +37,8 @@ import com.keylesspalace.tusky.json.SpannedTypeAdapter;
import com.keylesspalace.tusky.network.AuthInterceptor;
import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.ResourcesUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import okhttp3.Dispatcher;
import okhttp3.OkHttpClient;
@ -61,9 +63,15 @@ public abstract class BaseActivity extends AppCompatActivity {
/* There isn't presently a way to globally change the theme of a whole application at
* runtime, just individual activities. So, each activity has to set its theme before any
* views are created. */
if (preferences.getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
}
String[] themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
String appTheme = themeFlavorPair[0], themeFlavorPreference = themeFlavorPair[2];
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
String flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT);
if (flavor.equals(ThemeUtils.THEME_FLAVOR_DEFAULT))
flavor = themeFlavorPreference;
ThemeUtils.setAppNightMode(flavor);
int style;
switch(preferences.getString("statusTextSize", "medium")) {

View file

@ -26,7 +26,6 @@ import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.text.method.LinkMovementMethod;
import android.util.Log;
@ -42,6 +41,8 @@ import com.keylesspalace.tusky.network.MastodonApi;
import com.keylesspalace.tusky.util.CustomTabsHelper;
import com.keylesspalace.tusky.util.NotificationManager;
import com.keylesspalace.tusky.util.OkHttpUtils;
import com.keylesspalace.tusky.util.ResourcesUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
import java.util.HashMap;
import java.util.Map;
@ -68,9 +69,16 @@ public class LoginActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
}
preferences = PreferenceManager.getDefaultSharedPreferences(this);
String[] themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
String appTheme = themeFlavorPair[0], themeFlavorPreference = themeFlavorPair[2];
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
String flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT);
if (flavor.equals(ThemeUtils.THEME_FLAVOR_DEFAULT))
flavor = themeFlavorPreference;
ThemeUtils.setAppNightMode(flavor);
setContentView(R.layout.activity_login);
@ -234,15 +242,8 @@ public class LoginActivity extends AppCompatActivity {
}
private static boolean openInCustomTab(Uri uri, Context context) {
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("lightTheme", false);
int toolbarColorRes;
if (lightTheme) {
toolbarColorRes = R.color.custom_tab_toolbar_light;
} else {
toolbarColorRes = R.color.custom_tab_toolbar_dark;
}
int toolbarColor = ContextCompat.getColor(context, toolbarColorRes);
int toolbarColor = ThemeUtils.getColorById(context, "custom_tab_toolbar");
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(toolbarColor);
CustomTabsIntent customTabsIntent = builder.build();

View file

@ -27,6 +27,8 @@ import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import com.keylesspalace.tusky.fragment.PreferencesFragment;
import com.keylesspalace.tusky.util.ResourcesUtils;
import com.keylesspalace.tusky.util.ThemeUtils;
public class PreferencesActivity extends BaseActivity
implements SharedPreferences.OnSharedPreferenceChangeListener {
@ -46,9 +48,6 @@ public class PreferencesActivity extends BaseActivity
}
SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
if (preferences.getBoolean("lightTheme", false)) {
setTheme(R.style.AppTheme_Light);
}
setContentView(R.layout.activity_preferences);
@ -72,6 +71,28 @@ public class PreferencesActivity extends BaseActivity
}
showFragment(currentPreferences, currentTitle);
PreferencesFragment preferencesFragment = (PreferencesFragment)getFragmentManager().findFragmentById(R.id.fragment_container);
String[] themeFlavorPair = preferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
String appTheme = themeFlavorPair[0], themeFlavorMode = themeFlavorPair[1], themeFlavorPreference = themeFlavorPair[2];
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
if (preferencesFragment.findPreference("appThemeFlavor") != null) {
boolean lockFlavor = themeFlavorMode.equals(ThemeUtils.THEME_MODE_ONLY);
preferencesFragment.findPreference("appThemeFlavor").setEnabled(!lockFlavor);
}
String flavor = preferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT);
if (flavor.equals(ThemeUtils.THEME_FLAVOR_DEFAULT)) {
flavor = themeFlavorPreference;
preferences.edit()
.putString("appThemeFlavor", flavor)
.apply();
}
// Set theme based on preference
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
}
public void showFragment(@XmlRes int preferenceId, @StringRes int title) {
@ -81,6 +102,8 @@ public class PreferencesActivity extends BaseActivity
.replace(R.id.fragment_container, PreferencesFragment.newInstance(preferenceId))
.commit();
getFragmentManager().executePendingTransactions();
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setTitle(title);
@ -104,7 +127,32 @@ public class PreferencesActivity extends BaseActivity
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
switch (key) {
case "lightTheme": {
case "appTheme": {
String[] themeFlavorPair = sharedPreferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
String appTheme = themeFlavorPair[0];
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
sharedPreferences.edit()
.remove("appThemeFlavor")
.apply();
}
case "appThemeFlavor": {
String[] themeFlavorPair = sharedPreferences.getString("appTheme", TuskyApplication.APP_THEME_DEFAULT).split(":");
String appTheme = themeFlavorPair[0], themeFlavorPreference = themeFlavorPair[2];
setTheme(ResourcesUtils.getResourceIdentifier(this, "style", appTheme));
String flavor = sharedPreferences.getString("appThemeFlavor", ThemeUtils.THEME_FLAVOR_DEFAULT);
if (flavor.equals(ThemeUtils.THEME_FLAVOR_DEFAULT)) {
flavor = themeFlavorPreference;
sharedPreferences.edit()
.putString("appThemeFlavor", flavor)
.apply();
}
ThemeUtils.setAppNightMode(flavor);
restartActivitiesOnExit = true;
// recreate() could be used instead, but it doesn't have an animation B).
Intent intent = getIntent();

View file

@ -16,7 +16,9 @@
package com.keylesspalace.tusky;
import android.app.Application;
import android.app.UiModeManager;
import android.arch.persistence.room.Room;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v7.app.AppCompatDelegate;
@ -28,12 +30,18 @@ import com.keylesspalace.tusky.util.OkHttpUtils;
import com.squareup.picasso.Picasso;
public class TuskyApplication extends Application {
public static final String APP_THEME_DEFAULT = "AppTheme:prefer:night";
private static AppDatabase db;
public static AppDatabase getDB() {
return db;
}
private static UiModeManager uiModeManager;
public static UiModeManager getUiModeManager() { return uiModeManager; }
@Override
public void onCreate() {
super.onCreate();
@ -59,6 +67,8 @@ public class TuskyApplication extends Application {
JobManager.create(this).addJobCreator(new NotificationPullJobCreator(this));
uiModeManager = (UiModeManager)getSystemService(Context.UI_MODE_SERVICE);
//necessary for Android < APi 21
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}

View file

@ -18,7 +18,6 @@ package com.keylesspalace.tusky.fragment
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.preference.PreferenceManager
import android.support.v4.app.ActivityOptionsCompat
import android.support.v4.content.ContextCompat
import android.support.v4.view.ViewCompat
@ -26,6 +25,7 @@ import android.support.v4.widget.SwipeRefreshLayout
import android.support.v7.widget.GridLayoutManager
import android.support.v7.widget.RecyclerView
import android.util.Log
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -37,6 +37,7 @@ import com.keylesspalace.tusky.ViewVideoActivity
import com.keylesspalace.tusky.entity.Attachment
import com.keylesspalace.tusky.entity.Status
import com.keylesspalace.tusky.network.MastodonApi
import com.keylesspalace.tusky.util.ThemeUtils
import com.keylesspalace.tusky.view.SquareImageView
import com.squareup.picasso.Picasso
import retrofit2.Call
@ -133,10 +134,8 @@ class AccountMediaFragment : BaseFragment() {
val columnCount = context?.resources?.getInteger(R.integer.profile_media_column_count) ?: 2
val layoutManager = GridLayoutManager(context, columnCount)
val lightThemeEnabled = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("lightTheme", false)
val bgRes = if (lightThemeEnabled) R.color.window_background_light
else R.color.window_background_dark
val bgRes = ThemeUtils.getColorId(context, R.attr.window_background)
adapter.baseItemColor = ContextCompat.getColor(recyclerView.context, bgRes)
recyclerView.layoutManager = layoutManager

View file

@ -23,7 +23,6 @@ import android.preference.PreferenceManager;
import android.provider.Browser;
import android.support.annotation.Nullable;
import android.support.customtabs.CustomTabsIntent;
import android.support.v4.content.ContextCompat;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextPaint;
@ -34,7 +33,6 @@ import android.util.Log;
import android.view.View;
import android.widget.TextView;
import com.keylesspalace.tusky.R;
import com.keylesspalace.tusky.entity.Status;
import com.keylesspalace.tusky.interfaces.LinkListener;
@ -172,8 +170,8 @@ public class LinkHelper {
* @param context context
*/
public static void openLinkInCustomTab(Uri uri, Context context) {
boolean lightTheme = PreferenceManager.getDefaultSharedPreferences(context).getBoolean("lightTheme", false);
int toolbarColor = ContextCompat.getColor(context, lightTheme ? R.color.custom_tab_toolbar_light : R.color.custom_tab_toolbar_dark);
int toolbarColor = ThemeUtils.getColorById(context, "custom_tab_toolbar");
CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
builder.setToolbarColor(toolbarColor);
CustomTabsIntent customTabsIntent = builder.build();

View file

@ -0,0 +1,29 @@
/* Copyright 2017 Andrew Dawson
*
* This file is a part of Tusky.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tusky; if not,
* see <http://www.gnu.org/licenses>. */
package com.keylesspalace.tusky.util;
import android.content.Context;
import android.support.annotation.AnyRes;
/**
* Created by remi on 1/14/18.
*/
public class ResourcesUtils {
public static @AnyRes int getResourceIdentifier(Context context, String defType, String name) {
return context.getResources().getIdentifier(name, defType, context.getPackageName());
}
}

View file

@ -15,22 +15,35 @@
package com.keylesspalace.tusky.util;
import android.app.UiModeManager;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.annotation.AttrRes;
import android.support.annotation.ColorInt;
import android.support.annotation.ColorRes;
import android.support.annotation.DrawableRes;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatDelegate;
import android.util.TypedValue;
import android.widget.ImageView;
import com.keylesspalace.tusky.TuskyApplication;
/**
* Provides runtime compatibility to obtain theme information and re-theme views, especially where
* the ability to do so is not supported in resource files.
*/
public class ThemeUtils {
public static final String THEME_MODE_PREFER = "prefer";
public static final String THEME_MODE_ONLY = "only";
public static final String THEME_FLAVOR_NIGHT = "night";
public static final String THEME_FLAVOR_DAY = "day";
public static final String THEME_FLAVOR_AUTO = "auto";
public static final String THEME_FLAVOR_DEFAULT = "preferred";
public static Drawable getDrawable(Context context, @AttrRes int attribute,
@DrawableRes int fallbackDrawable) {
TypedValue value = new TypedValue();
@ -62,6 +75,17 @@ public class ThemeUtils {
}
}
public static @ColorRes int getColorId(Context context, @AttrRes int attribute) {
TypedValue value = new TypedValue();
context.getTheme().resolveAttribute(attribute, value, true);
return value.resourceId;
}
public static @ColorInt int getColorById(Context context, String name) {
return getColor(context,
ResourcesUtils.getResourceIdentifier(context, "attr", name));
}
public static void setImageViewTint(ImageView view, @AttrRes int attribute) {
view.setColorFilter(getColor(view.getContext(), attribute), PorterDuff.Mode.SRC_IN);
}
@ -69,4 +93,29 @@ public class ThemeUtils {
public static void setDrawableTint(Context context, Drawable drawable, @AttrRes int attribute) {
drawable.setColorFilter(getColor(context, attribute), PorterDuff.Mode.SRC_IN);
}
public static boolean setAppNightMode(String flavor) {
int mode;
switch (flavor) {
case THEME_FLAVOR_AUTO:
mode = UiModeManager.MODE_NIGHT_AUTO;
break;
case THEME_FLAVOR_NIGHT:
mode = UiModeManager.MODE_NIGHT_YES;
break;
case THEME_FLAVOR_DAY:
mode = UiModeManager.MODE_NIGHT_NO;
break;
default:
return false;
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
TuskyApplication.getUiModeManager().setNightMode(mode);
} else {
AppCompatDelegate.setDefaultNightMode(mode);
}
return true;
}
}