Port EmojiSearch to use Autocomplete

Stephen Paul Weber created

Change summary

src/cheogram/java/com/cheogram/android/EmojiSearch.java           | 67 
src/cheogram/res/layout/emoji_search.xml                          | 16 
src/main/java/eu/siacs/conversations/ui/ConversationFragment.java | 90 
3 files changed, 86 insertions(+), 87 deletions(-)

Detailed changes

src/cheogram/java/com/cheogram/android/EmojiSearch.java 🔗

@@ -8,9 +8,11 @@ import android.text.SpannableStringBuilder;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
 
 import androidx.databinding.DataBindingUtil;
+import androidx.recyclerview.widget.DiffUtil;
+import androidx.recyclerview.widget.ListAdapter;
+import androidx.recyclerview.widget.RecyclerView;
 
 import com.google.common.collect.Lists;
 import com.google.common.io.CharStreams;
@@ -25,6 +27,7 @@ import java.util.List;
 import java.util.PriorityQueue;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.function.Consumer;
 
 import me.xdrop.fuzzywuzzy.FuzzySearch;
 import me.xdrop.fuzzywuzzy.model.BoundExtractedResult;
@@ -87,8 +90,8 @@ public class EmojiSearch {
 		return result;
 	}
 
-	public EmojiSearchAdapter makeAdapter(Activity context) {
-		return new EmojiSearchAdapter(context);
+	public EmojiSearchAdapter makeAdapter(Consumer<Emoji> callback) {
+		return new EmojiSearchAdapter(callback);
 	}
 
 	public static class ResultPQ extends PriorityQueue<BoundExtractedResult<Emoji>> {
@@ -198,38 +201,68 @@ public class EmojiSearch {
 		}
 	}
 
-	public class EmojiSearchAdapter extends ArrayAdapter<Emoji> {
+	public class EmojiSearchAdapter extends ListAdapter<Emoji, EmojiSearchAdapter.ViewHolder> {
 		ReplacingSerialSingleThreadExecutor executor = new ReplacingSerialSingleThreadExecutor("EmojiSearchAdapter");
 
-		public EmojiSearchAdapter(Activity context) {
-			super(context, 0);
+		static final DiffUtil.ItemCallback<Emoji> DIFF = new DiffUtil.ItemCallback<Emoji>() {
+			@Override
+			public boolean areItemsTheSame(Emoji a, Emoji b) {
+				return a.equals(b);
+			}
+
+			@Override
+			public boolean areContentsTheSame(Emoji a, Emoji b) {
+				return a.equals(b);
+			}
+		};
+		final Consumer<Emoji> callback;
+
+		public EmojiSearchAdapter(final Consumer<Emoji> callback) {
+			super(DIFF);
+			this.callback = callback;
+		}
+
+		@Override
+		public ViewHolder onCreateViewHolder(ViewGroup viewGroup, int position) {
+			return new ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.getContext()), R.layout.emoji_search_row, viewGroup, false));
 		}
 
 		@Override
-		public View getView(int position, View view, ViewGroup parent) {
-			EmojiSearchRowBinding binding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), R.layout.emoji_search_row, parent, false);
-			if (getItem(position) instanceof CustomEmoji) {
-				binding.nonunicode.setText(getItem(position).toInsert());
+		public void onBindViewHolder(ViewHolder viewHolder, int position) {
+			final var binding = viewHolder.binding;
+			final var item = getItem(position);
+			if (item instanceof CustomEmoji) {
+				binding.nonunicode.setText(item.toInsert());
 				binding.nonunicode.setVisibility(View.VISIBLE);
 				binding.unicode.setVisibility(View.GONE);
 			} else {
-				binding.unicode.setText(getItem(position).toInsert());
+				binding.unicode.setText(item.toInsert());
 				binding.unicode.setVisibility(View.VISIBLE);
 				binding.nonunicode.setVisibility(View.GONE);
 			}
-			binding.shortcode.setText(getItem(position).shortcodes.get(0));
-			return binding.getRoot();
+			binding.shortcode.setText(item.shortcodes.get(0));
+			binding.getRoot().setOnClickListener(v -> {
+				callback.accept(item);
+			});
 		}
 
-		public void search(final String q) {
+		public void search(final Activity activity, final String q) {
 			executor.execute(() -> {
 				final List<Emoji> results = find(q);
-				((Activity) getContext()).runOnUiThread(() -> {
-					clear();
-					addAll(results);
+				activity.runOnUiThread(() -> {
+					submitList(results);
 					notifyDataSetChanged();
 				});
 			});
 		}
+
+		public static class ViewHolder extends RecyclerView.ViewHolder {
+			public final EmojiSearchRowBinding binding;
+
+			private ViewHolder(EmojiSearchRowBinding binding) {
+				super(binding.getRoot());
+				this.binding = binding;
+			}
+		}
 	}
 }

src/cheogram/res/layout/emoji_search.xml 🔗

@@ -1,16 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<layout xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-  <ListView
-      android:id="@+id/emoji"
-      android:layout_width="fill_parent"
-      android:layout_height="fill_parent"
-      android:layout_alignParentStart="true"
-      android:layout_alignParentLeft="true"
-      android:layout_alignParentTop="true"
-      android:divider="@android:color/transparent"
-      android:background="?colorSurfaceVariant">
-      android:dividerHeight="0dp"></ListView>
-
-</layout>

src/main/java/eu/siacs/conversations/ui/ConversationFragment.java 🔗

@@ -133,7 +133,6 @@ import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
-import eu.siacs.conversations.databinding.EmojiSearchBinding;
 import eu.siacs.conversations.databinding.FragmentConversationBinding;
 import eu.siacs.conversations.entities.Account;
 import eu.siacs.conversations.entities.Blockable;
@@ -274,8 +273,6 @@ public class ConversationFragment extends XmppFragment
     private int identiconWidth = -1;
     private File savingAsSticker = null;
     private EmojiSearch emojiSearch = null;
-    private EmojiSearchBinding emojiSearchBinding = null;
-    private PopupWindow emojiPopup = null;
     private final OnClickListener clickToMuc =
             new OnClickListener() {
 
@@ -1594,55 +1591,45 @@ public class ConversationFragment extends XmppFragment
                 public void onPopupVisibilityChanged(boolean shown) {}
             }).build();
 
-        final Pattern lastColonPattern = Pattern.compile("(?<!\\w):");
-        emojiSearchBinding = DataBindingUtil.inflate(inflater, R.layout.emoji_search, null, false);
-        emojiSearchBinding.emoji.setOnItemClickListener((parent, view, position, id) -> {
-            EmojiSearch.EmojiSearchAdapter adapter = ((EmojiSearch.EmojiSearchAdapter) emojiSearchBinding.emoji.getAdapter());
-            Editable toInsert = adapter.getItem(position).toInsert();
-            toInsert.append(" ");
-            Editable s = binding.textinput.getText();
-
-            Matcher lastColonMatcher = lastColonPattern.matcher(s);
-            int lastColon = -1;
-            while(lastColonMatcher.find()) lastColon = lastColonMatcher.end();
-            if (lastColon > 0) s.replace(lastColon - 1, s.length(), toInsert, 0, toInsert.length());
-        });
-        setupEmojiSearch();
-        int popupHeight = (int) displayMetrics.density * 200;
-        emojiPopup = new PopupWindow(emojiSearchBinding.getRoot(), WindowManager.LayoutParams.MATCH_PARENT, Math.min(popupHeight, displayMetrics.heightPixels > 0 ? displayMetrics.heightPixels / 5 : popupHeight));
         Handler emojiDebounce = new Handler(Looper.getMainLooper());
-        final Pattern notEmojiSearch = Pattern.compile("[^\\w\\(\\)\\+'\\-]");
-        binding.textinput.addTextChangedListener(new TextWatcher() {
-            @Override
-            public void afterTextChanged(Editable s) {
-                emojiDebounce.removeCallbacksAndMessages(null);
-                emojiDebounce.postDelayed(() -> {
-                    Matcher lastColonMatcher = lastColonPattern.matcher(s);
-                    int lastColon = -1;
-                    while(lastColonMatcher.find()) lastColon = lastColonMatcher.end();
-                    if (lastColon < 0) {
-                        emojiPopup.dismiss();
-                        return;
-                    }
-                    final String q = s.toString().substring(lastColon);
-                    if (notEmojiSearch.matcher(q).find()) {
-                        emojiPopup.dismiss();
-                    } else {
-                        EmojiSearch.EmojiSearchAdapter adapter = ((EmojiSearch.EmojiSearchAdapter) emojiSearchBinding.emoji.getAdapter());
-                        if (adapter != null) {
-                            adapter.search(q);
-                            emojiPopup.showAsDropDown(binding.textsend);
-                        }
-                    }
-                }, 400L);
-            }
+        setupEmojiSearch();
+        Autocomplete.<EmojiSearch.Emoji>on(binding.textinput)
+            .with(activity.getDrawable(R.drawable.background_message_bubble))
+            .with(new CharPolicy(':'))
+            .with(new RecyclerViewPresenter<EmojiSearch.Emoji>(activity) {
+                protected EmojiSearch.EmojiSearchAdapter adapter;
 
-            @Override
-            public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
+                @Override
+                protected Adapter instantiateAdapter() {
+                    adapter = emojiSearch.makeAdapter(item -> dispatchClick(item));
+                    return adapter;
+                }
 
-            @Override
-            public void onTextChanged(CharSequence s, int start, int count, int after) { }
-        });
+                @Override
+                protected void onQuery(@Nullable CharSequence query) {
+                    emojiDebounce.removeCallbacksAndMessages(null);
+                    emojiDebounce.postDelayed(() -> {
+                        if (getRecyclerView() == null) return;
+                        getRecyclerView().getItemAnimator().endAnimations();
+                        adapter.search(activity, query.toString());
+                    }, 100L);
+                }
+            })
+            .with(new AutocompleteCallback<EmojiSearch.Emoji>() {
+                @Override
+                public boolean onPopupItemClicked(Editable editable, EmojiSearch.Emoji emoji) {
+                    int[] range = com.otaliastudios.autocomplete.CharPolicy.getQueryRange(editable);
+                    if (range == null) return false;
+                    range[0] -= 1;
+                    final var toInsert = emoji.toInsert();
+                    toInsert.append(" ");
+                    editable.replace(Math.max(0, range[0]), Math.min(editable.length(), range[1]), toInsert);
+                    return true;
+                }
+
+                @Override
+                public void onPopupVisibilityChanged(boolean shown) {}
+            }).build();
 
         return binding.getRoot();
     }
@@ -1651,16 +1638,12 @@ public class ConversationFragment extends XmppFragment
         if (activity != null && activity.xmppConnectionService != null) {
             if (!activity.xmppConnectionService.getBooleanPreference("message_autocomplete", R.bool.message_autocomplete)) {
                 emojiSearch = null;
-                if (emojiSearchBinding != null) emojiSearchBinding.emoji.setAdapter(null);
                 return;
             }
             if (emojiSearch == null) {
                 emojiSearch = activity.xmppConnectionService.emojiSearch();
             }
         }
-        if (emojiSearch == null || emojiSearchBinding == null) return;
-
-        emojiSearchBinding.emoji.setAdapter(emojiSearch.makeAdapter(activity));
     }
 
     protected void newThreadTutorialToast(String s) {
@@ -3225,7 +3208,6 @@ public class ConversationFragment extends XmppFragment
             this.activity.xmppConnectionService.getNotificationService().setOpenConversation(null);
         }
         this.reInitRequiredOnStart = true;
-        if (emojiPopup != null) emojiPopup.dismiss();
     }
 
     private void updateChatState(final Conversation conversation, final String msg) {