1
0
Fork 0

fixed suggestion list flashing when typing fast or holding backspace (rendering is now debounced)

This commit is contained in:
sspanak 2025-03-29 13:05:24 +02:00 committed by Dimo Karaivanov
parent f8d7af669d
commit 02af8561e2
3 changed files with 60 additions and 58 deletions

View file

@ -57,7 +57,7 @@ public class SuggestionOps {
@NonNull @NonNull
public String get(int index) { public String get(int index) {
return suggestionBar != null ? suggestionBar.getSuggestion(index) : ""; return suggestionBar != null ? suggestionBar.get(index) : "";
} }
@ -69,20 +69,20 @@ public class SuggestionOps {
public void set(ArrayList<String> suggestions) { public void set(ArrayList<String> suggestions) {
if (suggestionBar != null) { if (suggestionBar != null) {
suggestionBar.setSuggestions(suggestions, 0, false); suggestionBar.setMany(suggestions, 0, false);
} }
} }
public void set(ArrayList<String> suggestions, boolean containsGenerated) { public void set(ArrayList<String> suggestions, boolean containsGenerated) {
if (suggestionBar != null) { if (suggestionBar != null) {
suggestionBar.setSuggestions(suggestions, 0, containsGenerated); suggestionBar.setMany(suggestions, 0, containsGenerated);
} }
} }
public void set(ArrayList<String> suggestions, int selectIndex, boolean containsGenerated) { public void set(ArrayList<String> suggestions, int selectIndex, boolean containsGenerated) {
if (suggestionBar != null) { if (suggestionBar != null) {
suggestionBar.setSuggestions(suggestions, selectIndex, containsGenerated); suggestionBar.setMany(suggestions, selectIndex, containsGenerated);
} }
} }

View file

@ -34,6 +34,8 @@ public class SettingsStore extends SettingsHotkeys {
public final static int SUGGESTIONS_MAX = 20; public final static int SUGGESTIONS_MAX = 20;
public final static int SUGGESTIONS_MIN = 8; public final static int SUGGESTIONS_MIN = 8;
public final static int SUGGESTIONS_POSITIONS_LIMIT = 100; public final static int SUGGESTIONS_POSITIONS_LIMIT = 100;
public final static int SUGGESTIONS_RENDER_DEBOUNCE_TIME = 25; // ms
public final static int SUGGESTIONS_RENDER_CLEAR_DEBOUNCE_TIME = 60; // ms
public final static int SUGGESTIONS_SELECT_ANIMATION_DURATION = 66; public final static int SUGGESTIONS_SELECT_ANIMATION_DURATION = 66;
public final static int SUGGESTIONS_TRANSLATE_ANIMATION_DURATION = 0; public final static int SUGGESTIONS_TRANSLATE_ANIMATION_DURATION = 0;
public final static int TEXT_INPUT_DEBOUNCE_TIME = 500; // ms public final static int TEXT_INPUT_DEBOUNCE_TIME = 500; // ms

View file

@ -32,7 +32,9 @@ public class SuggestionsBar {
private final String STEM_PUNCTUATION_VARIATION_PREFIX = ""; private final String STEM_PUNCTUATION_VARIATION_PREFIX = "";
@NonNull private String stem = ""; @NonNull private String stem = "";
private int backgroundColor = Color.TRANSPARENT; private int defaultBackgroundColor = Color.TRANSPARENT;
private int backgroundColor;
private double lastClickTime = 0; private double lastClickTime = 0;
private int lastScrollIndex = 0; private int lastScrollIndex = 0;
private int selectedIndex = 0; private int selectedIndex = 0;
@ -47,7 +49,7 @@ public class SuggestionsBar {
private SuggestionsAdapter mSuggestionsAdapter; private SuggestionsAdapter mSuggestionsAdapter;
private Vibration vibration; private Vibration vibration;
private final Handler alternativeScrollingHandler = new Handler(); private final Handler delayedDisplayHandler = new Handler();
public SuggestionsBar(@NonNull SettingsStore settings, @NonNull ResizableMainView mainView, @NonNull Runnable onItemClick) { public SuggestionsBar(@NonNull SettingsStore settings, @NonNull ResizableMainView mainView, @NonNull Runnable onItemClick) {
@ -136,7 +138,7 @@ public class SuggestionsBar {
@NonNull @NonNull
public String getSuggestion(int id) { public String get(int id) {
if (id < 0 || id >= visibleSuggestions.size()) { if (id < 0 || id >= visibleSuggestions.size()) {
return ""; return "";
} }
@ -186,17 +188,17 @@ public class SuggestionsBar {
} }
public void setSuggestions(@Nullable List<String> newSuggestions, int initialSel, boolean containsGenerated) { public void setMany(@Nullable List<String> newSuggestions, int initialSel, boolean containsGenerated) {
suggestions = newSuggestions; suggestions = newSuggestions;
ecoSetBackground(newSuggestions);
visibleSuggestions.clear();
selectedIndex = newSuggestions == null || newSuggestions.isEmpty() ? 0 : Math.max(initialSel, 0); selectedIndex = newSuggestions == null || newSuggestions.isEmpty() ? 0 : Math.max(initialSel, 0);
visibleSuggestions.clear();
setStem(newSuggestions, containsGenerated); setStem(newSuggestions, containsGenerated);
addManySuggestions(newSuggestions, mView != null ? SettingsStore.SUGGESTIONS_MAX : Integer.MAX_VALUE); addMany(newSuggestions, mView != null ? SettingsStore.SUGGESTIONS_MAX : Integer.MAX_VALUE);
selectedIndex = Math.min(selectedIndex, visibleSuggestions.size() - 1); selectedIndex = Math.min(selectedIndex, visibleSuggestions.size() - 1);
displaySuggestions();
renderDebounced();
} }
@ -235,13 +237,13 @@ public class SuggestionsBar {
* for performance reasons, hence the "limit" parameter. When they are too many, the SHOW_MORE_SUGGESTION, * for performance reasons, hence the "limit" parameter. When they are too many, the SHOW_MORE_SUGGESTION,
* will be displayed at the end. * will be displayed at the end.
*/ */
private void addManySuggestions(List<String> newSuggestions, int limit) { private void addMany(List<String> newSuggestions, int limit) {
if (newSuggestions == null) { if (newSuggestions == null) {
return; return;
} }
for (int i = 0, end = Math.min(limit, newSuggestions.size()); i < end; i++) { for (int i = 0, end = Math.min(limit, newSuggestions.size()); i < end; i++) {
addSuggestion(newSuggestions.get(i)); add(newSuggestions.get(i));
} }
if (newSuggestions.size() > limit) { if (newSuggestions.size() > limit) {
@ -250,14 +252,14 @@ public class SuggestionsBar {
} }
private void addSuggestion(@NonNull String suggestion) { private void add(@NonNull String suggestion) {
// shorten the stem variations // shorten the stem variations
if (!stem.isEmpty() && suggestion.length() == stem.length() + 1 && suggestion.toLowerCase().startsWith(stem.toLowerCase())) { if (!stem.isEmpty() && suggestion.length() == stem.length() + 1 && suggestion.toLowerCase().startsWith(stem.toLowerCase())) {
String trimmedSuggestion = suggestion.substring(stem.length()); String trimmedSuggestion = suggestion.substring(stem.length());
char firstChar = trimmedSuggestion.charAt(0); char firstChar = trimmedSuggestion.charAt(0);
String prefix = Character.isAlphabetic(firstChar) && !Characters.isCombiningPunctuation(firstChar) ? STEM_VARIATION_PREFIX : STEM_PUNCTUATION_VARIATION_PREFIX; String prefix = Character.isAlphabetic(firstChar) && !Characters.isCombiningPunctuation(firstChar) ? STEM_VARIATION_PREFIX : STEM_PUNCTUATION_VARIATION_PREFIX;
prefix = Characters.isFathatan(firstChar) ? " " : prefix; // Fix incorrect display of fathatan without a base character. It is a combining character, but since it is a letter, we must include a base character not to break it, with a "..." prefix prefix = Characters.isFathatan(firstChar) ? " " : prefix; // Fix incorrect display of Fathatan without a base character. It is a combining character, but since it is a letter, we must include a base character not to break it, with a "..." prefix
visibleSuggestions.add(prefix + formatUnreadableSuggestion(trimmedSuggestion)); visibleSuggestions.add(prefix + formatUnreadableSuggestion(trimmedSuggestion));
return; return;
} }
@ -266,11 +268,24 @@ public class SuggestionsBar {
} }
private void displaySuggestions() { /**
* Reduces flashing of the suggestions bar when the suggestions are empty and saves some resources
* by reducing the calls to render().
*/
private void renderDebounced() {
final int delay = visibleSuggestions.isEmpty() ? SettingsStore.SUGGESTIONS_RENDER_CLEAR_DEBOUNCE_TIME : SettingsStore.SUGGESTIONS_RENDER_DEBOUNCE_TIME;
delayedDisplayHandler.removeCallbacksAndMessages(null);
delayedDisplayHandler.postDelayed(this::render, delay);
}
private void render() {
if (mView == null) { if (mView == null) {
return; return;
} }
setBackground(false);
boolean smooth = settings.getSuggestionSmoothScroll() && visibleSuggestions.size() <= SettingsStore.SUGGESTIONS_MAX + 1; boolean smooth = settings.getSuggestionSmoothScroll() && visibleSuggestions.size() <= SettingsStore.SUGGESTIONS_MAX + 1;
mView.setItemAnimator(smooth ? animator : null); mView.setItemAnimator(smooth ? animator : null);
@ -282,9 +297,10 @@ public class SuggestionsBar {
/** /**
* If addManySuggestions() constrained the visible suggestions, the end of the list will contain * If addMany() constrained the visible suggestions, the end of the list will contain
* the SHOW_MORE_SUGGESTION. This method will display remove the SHOW_MORE_SUGGESTION and display * the SHOW_MORE_SUGGESTION. This method will remove the SHOW_MORE_SUGGESTION, prepare
* all the hidden suggestions. It also scrolls correctly to the new visible suggestion. * all hidden suggestions for displaying, and will scroll correctly to the new visible suggestion.
* After that, you must call render(), to visualize the changes.
*/ */
private boolean appendHiddenSuggestionsIfNeeded(boolean scrollBack) { private boolean appendHiddenSuggestionsIfNeeded(boolean scrollBack) {
if (mView == null || !visibleSuggestions.get(selectedIndex).equals(SHOW_MORE_SUGGESTION)) { if (mView == null || !visibleSuggestions.get(selectedIndex).equals(SHOW_MORE_SUGGESTION)) {
@ -292,7 +308,7 @@ public class SuggestionsBar {
} }
visibleSuggestions.clear(); visibleSuggestions.clear();
addManySuggestions(suggestions, Integer.MAX_VALUE); addMany(suggestions, Integer.MAX_VALUE);
selectedIndex = scrollBack || selectedIndex >= visibleSuggestions.size() ? visibleSuggestions.size() - 1 : selectedIndex; selectedIndex = scrollBack || selectedIndex >= visibleSuggestions.size() ? visibleSuggestions.size() - 1 : selectedIndex;
return true; return true;
@ -320,9 +336,9 @@ public class SuggestionsBar {
calculateScrollIndex(increment); calculateScrollIndex(increment);
if (appendHiddenSuggestionsIfNeeded(increment < 0)) { if (appendHiddenSuggestionsIfNeeded(increment < 0)) {
displaySuggestions(); render();
} }
scrollToIndex(); scrollToSelected();
} }
@ -338,7 +354,7 @@ public class SuggestionsBar {
} }
private void scrollToIndex() { private void scrollToSelected() {
if (mView == null) { if (mView == null) {
return; return;
} }
@ -346,19 +362,19 @@ public class SuggestionsBar {
mSuggestionsAdapter.setSelection(selectedIndex); mSuggestionsAdapter.setSelection(selectedIndex);
if (settings.getSuggestionScrollingDelay() > 0) { if (settings.getSuggestionScrollingDelay() > 0) {
alternativeScrollingHandler.removeCallbacksAndMessages(null); delayedDisplayHandler.removeCallbacksAndMessages(null);
alternativeScrollingHandler.postDelayed(this::scrollView, settings.getSuggestionScrollingDelay()); delayedDisplayHandler.postDelayed(this::renderScroll, settings.getSuggestionScrollingDelay());
} else { } else {
scrollView(); renderScroll();
} }
} }
/** /**
* Tells the adapter to scroll. Always call scrollToIndex() first, * Tells the adapter to scroll. Always call scrollToSelected() first,
* to set the selected index in the adapter. * to set the selected index in the adapter.
*/ */
private void scrollView() { private void renderScroll() {
if (mView == null) { if (mView == null) {
return; return;
} }
@ -383,12 +399,12 @@ public class SuggestionsBar {
Context context = mView.getContext(); Context context = mView.getContext();
backgroundColor = ContextCompat.getColor(context, R.color.keyboard_background); defaultBackgroundColor = ContextCompat.getColor(context, R.color.keyboard_background);
mSuggestionsAdapter.setColorDefault(ContextCompat.getColor(context, R.color.keyboard_text)); mSuggestionsAdapter.setColorDefault(ContextCompat.getColor(context, R.color.keyboard_text));
mSuggestionsAdapter.setColorHighlight(ContextCompat.getColor(context, R.color.suggestion_selected_text)); mSuggestionsAdapter.setColorHighlight(ContextCompat.getColor(context, R.color.suggestion_selected_text));
mSuggestionsAdapter.setBackgroundHighlight(ContextCompat.getColor(context, R.color.suggestion_selected_background)); mSuggestionsAdapter.setBackgroundHighlight(ContextCompat.getColor(context, R.color.suggestion_selected_background));
setBackground(visibleSuggestions); setBackground(true);
} }
@ -397,36 +413,20 @@ public class SuggestionsBar {
* Makes the background transparent, when there are no suggestions and theme-colored, * Makes the background transparent, when there are no suggestions and theme-colored,
* when there are suggestions. * when there are suggestions.
*/ */
private void setBackground(List<String> newSuggestions) { private void setBackground(boolean force) {
if (mView == null) { if (mView == null) {
return; return;
} }
int newSuggestionsSize = newSuggestions != null ? newSuggestions.size() : 0; boolean mustChange = (
if (newSuggestionsSize == 0) { (backgroundColor == Color.TRANSPARENT && !visibleSuggestions.isEmpty()) ||
mView.setBackgroundColor(Color.TRANSPARENT); (backgroundColor == defaultBackgroundColor && visibleSuggestions.isEmpty())
return; );
if (force || mustChange) {
backgroundColor = visibleSuggestions.isEmpty() ? Color.TRANSPARENT : defaultBackgroundColor;
mView.setBackgroundColor(backgroundColor);
} }
mView.setBackgroundColor(backgroundColor);
}
/**
* ecoSetBackground
* A performance-optimized version of "setBackground().
* Determines if the suggestions have changed and only then it changes the background.
*/
private void ecoSetBackground(List<String> newSuggestions) {
int newSuggestionsSize = newSuggestions != null ? newSuggestions.size() : 0;
if (
(newSuggestionsSize == 0 && visibleSuggestions.isEmpty())
|| (newSuggestionsSize > 0 && !visibleSuggestions.isEmpty())
) {
return;
}
setBackground(newSuggestions);
} }
@ -438,7 +438,7 @@ public class SuggestionsBar {
vibration.vibrate(); vibration.vibrate();
selectedIndex = position; selectedIndex = position;
if (appendHiddenSuggestionsIfNeeded(false)) { if (appendHiddenSuggestionsIfNeeded(false)) {
displaySuggestions(); render();
} else { } else {
onItemClick.run(); onItemClick.run();
} }