1
0
Fork 0

the Settings screen now follows the Dynamic Color theme on Android 12 and higher

This commit is contained in:
sspanak 2025-01-09 14:31:11 +02:00
parent 28801ba95b
commit ada5261773
19 changed files with 286 additions and 63 deletions

View file

@ -163,6 +163,7 @@ android {
}
dependencies {
implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
}

View file

@ -17,7 +17,7 @@
<application
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/TT9Theme"
android:theme="@style/TTheme"
android:supportsRtl="true">
<service android:name="io.github.sspanak.tt9.ime.TraditionalT9" android:permission="android.permission.BIND_INPUT_METHOD"
@ -42,7 +42,7 @@
android:excludeFromRecents="true"
android:label=""
android:name="io.github.sspanak.tt9.ui.dialogs.PopupDialogActivity"
android:theme="@style/alertDialog" />
android:theme="@style/TTheme.AddWord" />
<activity
android:label="@string/pref_help"

View file

@ -47,11 +47,11 @@ abstract public class ScreenPreference extends Preference {
if (pref instanceof PreferenceCategory) {
return R.layout.pref_category;
} else if (pref instanceof SwitchPreferenceCompat) {
return R.layout.pref_switch;
return R.layout.pref_switch_large;
} else if (pref instanceof DropDownPreference) {
return R.layout.pref_dropdown;
} else {
return R.layout.pref_text;
return R.layout.pref_default_large;
}
}

View file

@ -0,0 +1,54 @@
package io.github.sspanak.tt9.preferences.items;
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.preference.PreferenceViewHolder;
import com.google.android.material.search.SearchView;
import io.github.sspanak.tt9.R;
abstract public class ItemSearch extends ItemTextInput {
private final boolean isModernDevice = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S;
public ItemSearch(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ItemSearch(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ItemSearch(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ItemSearch(@NonNull Context context) {
super(context);
}
@Override protected int getDefaultLayout() {
return isModernDevice ? R.layout.pref_search_v31 : R.layout.pref_input_text;
}
@Override protected int getLargeLayout() {
return isModernDevice ? R.layout.pref_search_v31 : R.layout.pref_input_text_large;
}
protected void setTextField(@NonNull PreferenceViewHolder holder) {
if (!isModernDevice) {
super.setTextField(holder);
return;
}
SearchView searchView = holder.itemView.findViewById(R.id.search_view);
if (searchView != null) {
this.textField = searchView.getEditText();
}
}
}

View file

@ -3,11 +3,8 @@ package io.github.sspanak.tt9.preferences.items;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
@ -17,70 +14,86 @@ import androidx.preference.PreferenceViewHolder;
import io.github.sspanak.tt9.R;
import io.github.sspanak.tt9.preferences.custom.ScreenPreference;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.Logger;
abstract public class ItemTextInput extends ScreenPreference implements TextWatcher {
@NonNull private final Handler debouncer = new Handler(Looper.getMainLooper());
private EditText editText;
abstract public class ItemTextInput extends ScreenPreference {
@NonNull private final Handler listener = new Handler(Looper.getMainLooper());
protected EditText textField;
@NonNull protected String text = "";
public ItemTextInput(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public ItemTextInput(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public ItemTextInput(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public ItemTextInput(@NonNull Context context) {
super(context);
}
@Override
public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
super.onBindViewHolder(holder);
EditText editText = holder.itemView.findViewById(R.id.input_text_input_field);
if (editText == null) {
Logger.e(getClass().getSimpleName(), "Cannot attach a text change listener. Unable to find the EditText element.");
} else {
this.editText = editText;
editText.addTextChangedListener(this);
editText.setOnKeyListener(this::ignoreEnter);
setTextField(holder);
if (textField != null) {
ignoreEnter();
checkTextChange();
}
}
@Override protected int getDefaultLayout() { return R.layout.pref_input_text; }
@Override protected int getLargeLayout() { return R.layout.pref_input_text_large; }
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {}
@Override
public void afterTextChanged(Editable s) {
debouncer.removeCallbacksAndMessages(null);
debouncer.postDelayed(() -> onChange(s.toString()), getChangeHandlerDebounceTime());
}
protected int getChangeHandlerDebounceTime() {
return SettingsStore.TEXT_INPUT_DEBOUNCE_TIME;
}
protected void setText(CharSequence text) {
protected void setTextField(@NonNull PreferenceViewHolder holder) {
EditText editText = holder.itemView.findViewById(R.id.input_text_input_field);
if (editText != null) {
editText.setText(text);
this.textField = editText;
}
}
protected void setText(CharSequence newText) {
if (textField != null && newText != null && !text.equals(newText.toString())) {
textField.setText(newText);
text = newText.toString();
}
}
/**
* Internal text change detector that calls the onTextChange() when needed.
* IMPORTANT: do not call this method more than once per instance to avoid creating multiple
* listeners and memory leaks.
*/
private void checkTextChange() {
String newText = textField != null ? textField.getText().toString() : "";
if (!text.equals(newText)) {
text = newText;
onTextChange();
}
listener.postDelayed(this::checkTextChange, getChangeHandlerDebounceTime());
}
/**
* This prevents IllegalStateException "focus search returned a view that wasn't able to take focus!",
* which is thrown when the EditText is focused and it receives a simulated ENTER key event.
*/
private boolean ignoreEnter(View v, int keyCode, KeyEvent e) {
return keyCode == KeyEvent.KEYCODE_ENTER;
private void ignoreEnter() {
textField.setOnKeyListener((v, keyCode, e) -> keyCode == KeyEvent.KEYCODE_ENTER);
}
protected abstract void onChange(String word);
abstract protected void onTextChange();
}

View file

@ -9,12 +9,12 @@ import androidx.annotation.Nullable;
import java.util.ArrayList;
import io.github.sspanak.tt9.db.DataStore;
import io.github.sspanak.tt9.preferences.items.ItemTextInput;
import io.github.sspanak.tt9.preferences.items.ItemSearch;
import io.github.sspanak.tt9.preferences.settings.SettingsStore;
import io.github.sspanak.tt9.util.ConsumerCompat;
import io.github.sspanak.tt9.util.Logger;
public class PreferenceSearchWords extends ItemTextInput {
public class PreferenceSearchWords extends ItemSearch {
public static final String NAME = "dictionary_delete_words_search";
private static final String LOG_TAG = PreferenceSearchWords.class.getSimpleName();
@ -31,17 +31,15 @@ public class PreferenceSearchWords extends ItemTextInput {
@Override
protected void onChange(String word) {
search(word);
protected void onTextChange() {
search(text);
}
@NonNull
public String getLastSearchTerm() {
return lastSearchTerm;
}
void search(String word) {
lastSearchTerm = word == null || word.trim().isEmpty() ? "" : word.trim();
@ -56,7 +54,6 @@ public class PreferenceSearchWords extends ItemTextInput {
}
}
void setOnWordsHandler(ConsumerCompat<ArrayList<String>> onWords) {
this.onWords = onWords;
}

View file

@ -9,37 +9,37 @@ import androidx.preference.Preference;
import java.util.ArrayList;
import io.github.sspanak.tt9.preferences.items.ItemTextInput;
import io.github.sspanak.tt9.preferences.items.ItemSearch;
public class PreferenceSearchLanguage extends ItemTextInput {
public class PreferenceSearchLanguage extends ItemSearch {
@NonNull private ArrayList<PreferenceSwitchLanguage> languageItems = new ArrayList<>();
private Preference noResultItem;
public PreferenceSearchLanguage(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public PreferenceSearchLanguage(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public PreferenceSearchLanguage(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public PreferenceSearchLanguage(@NonNull Context context) {
super(context);
}
private void showNoResultItem(boolean show) {
if (noResultItem != null) {
noResultItem.setVisible(show);
}
}
@Override
protected void onChange(String word) {
word = word == null ? "" : word.trim().toLowerCase();
protected void onTextChange() {
String word = text.trim().toLowerCase();
String wordInTheMiddle = " " + word;
String wordInParenthesis = "(" + word;
@ -63,11 +63,13 @@ public class PreferenceSearchLanguage extends ItemTextInput {
showNoResultItem(visibleLanguages == 0);
}
PreferenceSearchLanguage setLanguageItems(@NonNull ArrayList<PreferenceSwitchLanguage> languageItems) {
this.languageItems = languageItems;
return this;
}
void setNoResultItem(Preference noResultItem) {
this.noResultItem = noResultItem;
}

View file

@ -48,8 +48,8 @@ abstract class AbstractPreferenceCharList extends ItemTextInput {
@Override
protected void onChange(String word) {
currentChars = word == null ? "" : word;
protected void onTextChange() {
currentChars = text;
validateCurrentChars();
}
@ -78,7 +78,7 @@ abstract class AbstractPreferenceCharList extends ItemTextInput {
}
}
setText(optional.toString());
setText(currentChars = optional.toString());
}

View file

@ -5,12 +5,15 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
import android.os.Looper;
import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.HashMap;
import io.github.sspanak.tt9.preferences.PreferencesActivity;
@ -23,6 +26,7 @@ public class UI {
((InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE)).showInputMethodPicker();
}
public static boolean showSystemSpellCheckerSettings(Context context) {
ComponentName component = new ComponentName(
"com.android.settings",
@ -50,7 +54,9 @@ public class UI {
ims.startActivity(prefIntent);
}
public static void confirm(Context context, String title, String message, String OKLabel, Runnable onOk, Runnable onCancel) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
@ -58,12 +64,23 @@ public class UI {
.setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> { if (onCancel != null) onCancel.run(); })
.setCancelable(false)
.show();
} else {
new MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(OKLabel, (dialog, whichButton) -> { if (onOk != null) onOk.run(); })
.setNegativeButton(android.R.string.cancel, (dialog, whichButton) -> { if (onCancel != null) onCancel.run(); })
.setCancelable(false)
.show();
}
}
public static void toast(Context context, CharSequence msg) {
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
}
public static void toastFromAsync(Context context, CharSequence msg) {
if (Looper.myLooper() == null) {
Looper.prepare();
@ -71,10 +88,12 @@ public class UI {
toast(context, msg);
}
public static void toast(Context context, int resourceId) {
Toast.makeText(context, resourceId, Toast.LENGTH_SHORT).show();
}
public static void toastFromAsync(Context context, int resourceId) {
if (Looper.myLooper() == null) {
Looper.prepare();
@ -82,14 +101,17 @@ public class UI {
toast(context, resourceId);
}
public static void toastLong(Context context, int resourceId) {
Toast.makeText(context, resourceId, Toast.LENGTH_LONG).show();
}
public static void toastLong(Context context, CharSequence msg) {
Toast.makeText(context, msg, Toast.LENGTH_LONG).show();
}
public static void toastLongFromAsync(Context context, CharSequence msg) {
if (Looper.myLooper() == null) {
Looper.prepare();
@ -97,6 +119,7 @@ public class UI {
toastLong(context, msg);
}
public static void toastSingle(@NonNull Context context, @NonNull String uniqueId, @NonNull String message, boolean isShort) {
Toast toast = singleToasts.get(uniqueId);
@ -111,14 +134,17 @@ public class UI {
singleToasts.put(uniqueId, toast);
}
public static void toastShortSingle(@NonNull Context context, @NonNull String uniqueId, @NonNull String message) {
toastSingle(context, uniqueId, message, true);
}
public static void toastShortSingle(@NonNull Context context, int resourceId) {
toastSingle(context, String.valueOf(resourceId), context.getString(resourceId), true);
}
public static void toastLongSingle(@NonNull Context context, int resourceId) {
toastSingle(context, String.valueOf(resourceId), context.getString(resourceId), false);
}

View file

@ -55,7 +55,7 @@ abstract class BaseMainLayout {
// Adding the ContextThemeWrapper fixes this error log:
// "View class SoftKeyXXX is an AppCompat widget that can only be used with a
// Theme.AppCompat theme (or descendant)."
ContextThemeWrapper themedCtx = new ContextThemeWrapper(tt9, R.style.TT9Theme);
ContextThemeWrapper themedCtx = new ContextThemeWrapper(tt9, R.style.TTheme);
view = View.inflate(themedCtx, xml, null);
}

View file

@ -9,7 +9,7 @@
<TextView
android:id="@android:id/title"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:labelFor="@id/input_text_input_field"
android:text="?android:title" />

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/preference_search_height">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.search.SearchBar
android:id="@+id/search_bar"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</com.google.android.material.appbar.AppBarLayout>
<com.google.android.material.search.SearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_anchor="@id/search_bar" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Derived from https://github.com/androidx/androidx/blob/8cb282cc/preference/preference/res/layout/preference_widget_switch_compat.xml -->
<!-- Thanks to https://stackoverflow.com/a/73782598 -->
<com.google.android.material.materialswitch.MaterialSwitch xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/switchWidget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:focusable="false"
android:clickable="false"
android:background="@null" />

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TTheme" parent="Theme.Material3.DynamicColors.Dark">
<item name="preferenceTheme">@style/PreferenceOverlay</item>
<!-- title bar -->
<item name="tint">@color/material_dynamic_neutral90</item> <!-- back button color -->
<item name="colorOnSurface">@color/material_dynamic_neutral90</item> <!-- title text color -->
<item name="colorSurfaceContainer">@color/material_dynamic_neutral_variant30</item> <!-- title background -->
<!-- page -->
<item name="android:windowBackground">@color/material_dynamic_neutral10</item> <!-- page background -->
<item name="colorSecondary">@color/material_dynamic_primary70</item> <!-- category title -->
<item name="android:textAppearanceListItem">@style/TextAppearance.Material3.TitleLarge</item> <!-- preference title -->
<!-- <item name="android:popupMenuStyle">@style/AppDropDownStyle</item> &lt;!&ndash; dropdown background &ndash;&gt;-->
</style>
<style name="PreferenceOverlay" parent="@style/PreferenceThemeOverlay">
<item name="switchPreferenceCompatStyle">@style/AppSwitchStyle</item>
</style>
<!-- <style name="AppDropDownStyle" parent="Widget.AppCompat.ListPopupWindow">-->
<!-- <item name="android:background">@color/material_dynamic_neutral_variant30</item>-->
<!-- <item name="popupMenuBackground">@color/material_dynamic_neutral_variant30</item>-->
<!-- </style>-->
<style name="AppSwitchStyle" parent="@style/Preference.SwitchPreferenceCompat.Material">
<item name="widgetLayout">@layout/pref_switch_v31</item>
</style>
<style name="TTheme.AddWord" parent="Theme.Material3.Dark.Dialog.Alert">
<item name="windowNoTitle">true</item> <!-- hide some weird floating rectangle above the dialog -->
<item name="android:textColor">@color/material_dynamic_neutral99</item> <!-- headline (title) text color -->
<item name="android:textColorPrimary">@color/material_dynamic_neutral_variant95</item> <!-- supporting text (body text) color -->
<item name="android:background">@color/material_dynamic_neutral20</item> <!-- container background -->
<item name="colorPrimary">@color/material_dynamic_primary90</item> <!-- label text (button text) color -->
<item name="textAppearanceBodyMedium">@style/TextAppearance.AppCompat.Widget.PopupMenu.Large</item> <!-- body text size -->
<item name="textAppearanceLabelLarge">@style/TextAppearance.MaterialComponents.Button</item> <!-- button text size -->
</style>
</resources>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TTheme" parent="Theme.Material3.DynamicColors.Light">
<item name="preferenceTheme">@style/PreferenceOverlay</item>
<!-- title bar -->
<item name="tint">@color/material_dynamic_neutral20</item> <!-- back button color -->
<item name="colorSurfaceContainer">@color/material_dynamic_neutral_variant90</item> <!-- title bar background -->
<!-- page -->
<item name="android:windowBackground">@color/material_dynamic_neutral95</item> <!-- page background -->
<item name="colorSecondary">@color/material_dynamic_primary40</item> <!-- category title -->
<item name="android:textAppearanceListItem">@style/TextAppearance.Material3.TitleLarge</item> <!-- preference title -->
<!-- <item name="android:popupWindowStyle">@style/AppDropDownStyle</item> &lt;!&ndash; dropdown background &ndash;&gt;-->
<!--
// ANY PREFERENCE
android:textColor = preference text color
android:textColorSecondary = preference summary color
// SWITCH
colorOutline = switch outline
colorSurfaceContainerHighest = switch background when off
colorPrimary = switch background when on
colorOnPrimary = switch handle color
colorPrimaryContainer = switch handle color when moving
-->
</style>
<style name="PreferenceOverlay" parent="@style/PreferenceThemeOverlay">
<item name="switchPreferenceCompatStyle">@style/AppSwitchStyle</item>
</style>
<style name="AppSwitchStyle" parent="@style/Preference.SwitchPreferenceCompat.Material">
<item name="widgetLayout">@layout/pref_switch_v31</item>
</style>
<!-- <style name="AppDropDownStyle" parent="Widget.Material3.PopupMenu.ListPopupWindow">-->
<!-- <item name="android:background">@color/material_dynamic_neutral_variant90</item>-->
<!-- <item name="popupMenuBackground">@color/material_dynamic_neutral_variant90</item>-->
<!-- </style>-->
<style name="TTheme.AddWord" parent="Theme.Material3.Light.Dialog.Alert">
<item name="windowNoTitle">true</item> <!-- hide some weird floating rectangle above the dialog -->
<item name="android:textColor">@color/material_dynamic_neutral10</item> <!-- headline (title) text color -->
<item name="android:textColorPrimary">@color/material_dynamic_neutral_variant10</item> <!-- supporting text (body text) color -->
<item name="android:background">@color/material_dynamic_neutral95</item> <!-- container background -->
<item name="colorPrimary">@color/material_dynamic_primary20</item> <!-- label text (button text) color -->
<item name="textAppearanceBodyMedium">@style/TextAppearance.AppCompat.Widget.PopupMenu.Large</item> <!-- body text size -->
<item name="textAppearanceLabelLarge">@style/TextAppearance.MaterialComponents.Button</item> <!-- button text size -->
</style>
</resources>

View file

@ -5,6 +5,7 @@
<dimen name="candidate_padding_horizontal">6sp</dimen>
<dimen name="preferences_text_min_height">48dp</dimen>
<dimen name="preference_search_height">72dp</dimen>
<dimen name="soft_key_height">44dp</dimen>
<dimen name="soft_key_icon_size">24sp</dimen>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="TT9Theme" parent="Theme.AppCompat.DayNight" />
<style name="TTheme" parent="Theme.AppCompat.DayNight" />
<style name="hSeparator">
<item name="android:layout_height">match_parent</item>
@ -22,7 +22,7 @@
<item name="android:layout_width">match_parent</item>
</style>
<style name="alertDialog" parent="Theme.AppCompat.DayNight.Dialog.Alert">
<item name="android:windowBackground">@android:color/transparent</item>
<style name="TTheme.AddWord" parent="Theme.AppCompat.DayNight.Dialog.Alert">
<item name="windowNoTitle">true</item> <!-- hide some weird floating rectangle above the dialog -->
</style>
</resources>