From ada526177355939aa0aeb28769ab240ca7033b4f Mon Sep 17 00:00:00 2001 From: sspanak Date: Thu, 9 Jan 2025 14:31:11 +0200 Subject: [PATCH] the Settings screen now follows the Dynamic Color theme on Android 12 and higher --- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 4 +- .../preferences/custom/ScreenPreference.java | 4 +- .../tt9/preferences/items/ItemSearch.java | 54 ++++++++++++++ .../tt9/preferences/items/ItemTextInput.java | 73 +++++++++++-------- .../deleteWords/PreferenceSearchWords.java | 11 +-- .../PreferenceSearchLanguage.java | 16 ++-- .../AbstractPreferenceCharList.java | 6 +- .../java/io/github/sspanak/tt9/ui/UI.java | 40 ++++++++-- .../sspanak/tt9/ui/main/BaseMainLayout.java | 2 +- .../{pref_text.xml => pref_default_large.xml} | 0 app/src/main/res/layout/pref_input_text.xml | 2 +- app/src/main/res/layout/pref_search_v31.xml | 23 ++++++ ...{pref_switch.xml => pref_switch_large.xml} | 0 app/src/main/res/layout/pref_switch_v31.xml | 11 +++ app/src/main/res/values-night-v31/styles.xml | 41 +++++++++++ app/src/main/res/values-v31/styles.xml | 54 ++++++++++++++ app/src/main/res/values/dimens.xml | 1 + app/src/main/res/values/styles.xml | 6 +- 19 files changed, 286 insertions(+), 63 deletions(-) create mode 100644 app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemSearch.java rename app/src/main/res/layout/{pref_text.xml => pref_default_large.xml} (100%) create mode 100644 app/src/main/res/layout/pref_search_v31.xml rename app/src/main/res/layout/{pref_switch.xml => pref_switch_large.xml} (100%) create mode 100644 app/src/main/res/layout/pref_switch_v31.xml create mode 100644 app/src/main/res/values-night-v31/styles.xml create mode 100644 app/src/main/res/values-v31/styles.xml diff --git a/app/build.gradle b/app/build.gradle index e3bc6d29..07c8f6dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c534517..4d98a64a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ + android:theme="@style/TTheme.AddWord" /> = 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(); + } + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTextInput.java b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTextInput.java index 96684b6b..381b34ab 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTextInput.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/items/ItemTextInput.java @@ -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(); } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java index ffbf66d5..80a96c15 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/deleteWords/PreferenceSearchWords.java @@ -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> onWords) { this.onWords = onWords; } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/PreferenceSearchLanguage.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/PreferenceSearchLanguage.java index f26c4720..3243d3f9 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/PreferenceSearchLanguage.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/languageSelection/PreferenceSearchLanguage.java @@ -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 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 languageItems) { this.languageItems = languageItems; return this; } + void setNoResultItem(Preference noResultItem) { this.noResultItem = noResultItem; } diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/punctuation/AbstractPreferenceCharList.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/punctuation/AbstractPreferenceCharList.java index 723dc462..63b18299 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/punctuation/AbstractPreferenceCharList.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/punctuation/AbstractPreferenceCharList.java @@ -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()); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/UI.java b/app/src/main/java/io/github/sspanak/tt9/ui/UI.java index f5cc6346..570da5dd 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/UI.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/UI.java @@ -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,20 +54,33 @@ public class UI { ims.startActivity(prefIntent); } + public static void confirm(Context context, String title, String message, String OKLabel, Runnable onOk, Runnable onCancel) { - new AlertDialog.Builder(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(); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + new AlertDialog.Builder(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(); + } 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); } diff --git a/app/src/main/java/io/github/sspanak/tt9/ui/main/BaseMainLayout.java b/app/src/main/java/io/github/sspanak/tt9/ui/main/BaseMainLayout.java index 4573049a..d0de0672 100644 --- a/app/src/main/java/io/github/sspanak/tt9/ui/main/BaseMainLayout.java +++ b/app/src/main/java/io/github/sspanak/tt9/ui/main/BaseMainLayout.java @@ -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); } diff --git a/app/src/main/res/layout/pref_text.xml b/app/src/main/res/layout/pref_default_large.xml similarity index 100% rename from app/src/main/res/layout/pref_text.xml rename to app/src/main/res/layout/pref_default_large.xml diff --git a/app/src/main/res/layout/pref_input_text.xml b/app/src/main/res/layout/pref_input_text.xml index 5b62a29c..cfcc49bb 100644 --- a/app/src/main/res/layout/pref_input_text.xml +++ b/app/src/main/res/layout/pref_input_text.xml @@ -9,7 +9,7 @@ diff --git a/app/src/main/res/layout/pref_search_v31.xml b/app/src/main/res/layout/pref_search_v31.xml new file mode 100644 index 00000000..f428336a --- /dev/null +++ b/app/src/main/res/layout/pref_search_v31.xml @@ -0,0 +1,23 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/pref_switch.xml b/app/src/main/res/layout/pref_switch_large.xml similarity index 100% rename from app/src/main/res/layout/pref_switch.xml rename to app/src/main/res/layout/pref_switch_large.xml diff --git a/app/src/main/res/layout/pref_switch_v31.xml b/app/src/main/res/layout/pref_switch_v31.xml new file mode 100644 index 00000000..f6c355e5 --- /dev/null +++ b/app/src/main/res/layout/pref_switch_v31.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/values-night-v31/styles.xml b/app/src/main/res/values-night-v31/styles.xml new file mode 100644 index 00000000..05986c87 --- /dev/null +++ b/app/src/main/res/values-night-v31/styles.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values-v31/styles.xml b/app/src/main/res/values-v31/styles.xml new file mode 100644 index 00000000..16264cc3 --- /dev/null +++ b/app/src/main/res/values-v31/styles.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index e73cf4dd..7a102f38 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -5,6 +5,7 @@ 6sp 48dp + 72dp 44dp 24sp diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 23c1bff1..13d334a0 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,6 +1,6 @@ - -