Import source from https://github.com/natario1/Autocomplete
This commit is contained in:
parent
f304e40d57
commit
3da1497d27
|
@ -115,6 +115,7 @@ ext.groups = [
|
|||
'com.linkedin.dexmaker',
|
||||
'com.mapbox.mapboxsdk',
|
||||
'com.nulab-inc',
|
||||
'com.otaliastudios',
|
||||
'com.otaliastudios.opengl',
|
||||
'com.parse.bolts',
|
||||
'com.pinterest',
|
||||
|
@ -238,7 +239,6 @@ ext.groups = [
|
|||
regex: [
|
||||
],
|
||||
group: [
|
||||
'com.otaliastudios',
|
||||
'com.yqritc',
|
||||
// https://github.com/cmelchior/realmfieldnameshelper/issues/42
|
||||
'dk.ilios',
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
android {
|
||||
namespace "com.otaliastudios.autocomplete"
|
||||
|
||||
compileSdk versions.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk versions.minSdk
|
||||
targetSdk versions.targetSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility versions.sourceCompat
|
||||
targetCompatibility versions.targetCompat
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "11"
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation libs.androidx.recyclerview
|
||||
}
|
||||
|
||||
afterEvaluate {
|
||||
tasks.findAll { it.name.startsWith("lint") }.each {
|
||||
it.enabled = false
|
||||
}
|
||||
}
|
434
library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java
vendored
Normal file
434
library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/Autocomplete.java
vendored
Normal file
|
@ -0,0 +1,434 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.database.DataSetObserver;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.Selection;
|
||||
import android.text.SpanWatcher;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.Gravity;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
||||
/**
|
||||
* Entry point for adding Autocomplete behavior to a {@link EditText}.
|
||||
*
|
||||
* You can construct a {@code Autocomplete} using the builder provided by {@link Autocomplete#on(EditText)}.
|
||||
* Building is enough, but you can hold a reference to this class to call its public methods.
|
||||
*
|
||||
* Requires:
|
||||
* - {@link EditText}: this is both the anchor for the popup, and the source of text events that we listen to
|
||||
* - {@link AutocompletePresenter}: this presents items in the popup window. See class for more info.
|
||||
* - {@link AutocompleteCallback}: if specified, this listens to click events and visibility changes
|
||||
* - {@link AutocompletePolicy}: if specified, this controls how and when to show the popup based on text events
|
||||
* If not, this defaults to {@link SimplePolicy}: shows the popup when text.length() bigger than 0.
|
||||
*/
|
||||
public final class Autocomplete<T> implements TextWatcher, SpanWatcher {
|
||||
|
||||
private final static String TAG = Autocomplete.class.getSimpleName();
|
||||
private final static boolean DEBUG = false;
|
||||
|
||||
private static void log(String log) {
|
||||
if (DEBUG) Log.e(TAG, log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for building {@link Autocomplete}.
|
||||
* The only mandatory item is a presenter, {@link #with(AutocompletePresenter)}.
|
||||
*
|
||||
* @param <T> the data model
|
||||
*/
|
||||
public final static class Builder<T> {
|
||||
private EditText source;
|
||||
private AutocompletePresenter<T> presenter;
|
||||
private AutocompletePolicy policy;
|
||||
private AutocompleteCallback<T> callback;
|
||||
private Drawable backgroundDrawable;
|
||||
private float elevationDp = 6;
|
||||
|
||||
private Builder(EditText source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the {@link AutocompletePresenter} to be used, responsible for showing
|
||||
* items. See the class for info.
|
||||
*
|
||||
* @param presenter desired presenter
|
||||
* @return this for chaining
|
||||
*/
|
||||
public Builder<T> with(AutocompletePresenter<T> presenter) {
|
||||
this.presenter = presenter;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the {@link AutocompleteCallback} to be used, responsible for listening to
|
||||
* clicks provided by the presenter, and visibility changes.
|
||||
*
|
||||
* @param callback desired callback
|
||||
* @return this for chaining
|
||||
*/
|
||||
public Builder<T> with(AutocompleteCallback<T> callback) {
|
||||
this.callback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the {@link AutocompletePolicy} to be used, responsible for showing / dismissing
|
||||
* the popup when certain events happen (e.g. certain characters are typed).
|
||||
*
|
||||
* @param policy desired policy
|
||||
* @return this for chaining
|
||||
*/
|
||||
public Builder<T> with(AutocompletePolicy policy) {
|
||||
this.policy = policy;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a background drawable for the popup.
|
||||
*
|
||||
* @param backgroundDrawable drawable
|
||||
* @return this for chaining
|
||||
*/
|
||||
public Builder<T> with(Drawable backgroundDrawable) {
|
||||
this.backgroundDrawable = backgroundDrawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets elevation for the popup. Defaults to 6 dp.
|
||||
*
|
||||
* @param elevationDp popup elevation, in DP
|
||||
* @return this for chaning.
|
||||
*/
|
||||
public Builder<T> with(float elevationDp) {
|
||||
this.elevationDp = elevationDp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds an Autocomplete instance. This is enough for autocomplete to be set up,
|
||||
* but you can hold a reference to the object and call its public methods.
|
||||
*
|
||||
* @return an Autocomplete instance, if you need it
|
||||
*
|
||||
* @throws RuntimeException if either EditText or the presenter are null
|
||||
*/
|
||||
public Autocomplete<T> build() {
|
||||
if (source == null) throw new RuntimeException("Autocomplete needs a source!");
|
||||
if (presenter == null) throw new RuntimeException("Autocomplete needs a presenter!");
|
||||
if (policy == null) policy = new SimplePolicy();
|
||||
return new Autocomplete<T>(this);
|
||||
}
|
||||
|
||||
private void clear() {
|
||||
source = null;
|
||||
presenter = null;
|
||||
callback = null;
|
||||
policy = null;
|
||||
backgroundDrawable = null;
|
||||
elevationDp = 6;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for building autocomplete on a certain {@link EditText}.
|
||||
* @param anchor the anchor for the popup, and the source of text events
|
||||
* @param <T> your data model
|
||||
* @return a Builder for set up
|
||||
*/
|
||||
public static <T> Builder<T> on(EditText anchor) {
|
||||
return new Builder<T>(anchor);
|
||||
}
|
||||
|
||||
private AutocompletePolicy policy;
|
||||
private AutocompletePopup popup;
|
||||
private AutocompletePresenter<T> presenter;
|
||||
private AutocompleteCallback<T> callback;
|
||||
private EditText source;
|
||||
|
||||
private boolean block;
|
||||
private boolean disabled;
|
||||
private boolean openBefore;
|
||||
private String lastQuery = "null";
|
||||
|
||||
private Autocomplete(Builder<T> builder) {
|
||||
policy = builder.policy;
|
||||
presenter = builder.presenter;
|
||||
callback = builder.callback;
|
||||
source = builder.source;
|
||||
|
||||
// Set up popup
|
||||
popup = new AutocompletePopup(source.getContext());
|
||||
popup.setAnchorView(source);
|
||||
popup.setGravity(Gravity.START);
|
||||
popup.setModal(false);
|
||||
popup.setBackgroundDrawable(builder.backgroundDrawable);
|
||||
popup.setElevation(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, builder.elevationDp,
|
||||
source.getContext().getResources().getDisplayMetrics()));
|
||||
|
||||
// popup dimensions
|
||||
AutocompletePresenter.PopupDimensions dim = this.presenter.getPopupDimensions();
|
||||
popup.setWidth(dim.width);
|
||||
popup.setHeight(dim.height);
|
||||
popup.setMaxWidth(dim.maxWidth);
|
||||
popup.setMaxHeight(dim.maxHeight);
|
||||
|
||||
// Fire visibility events
|
||||
popup.setOnDismissListener(new PopupWindow.OnDismissListener() {
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
lastQuery = "null";
|
||||
if (callback != null) callback.onPopupVisibilityChanged(false);
|
||||
boolean saved = block;
|
||||
block = true;
|
||||
policy.onDismiss(source.getText());
|
||||
block = saved;
|
||||
presenter.hideView();
|
||||
}
|
||||
});
|
||||
|
||||
// Set up source
|
||||
source.getText().setSpan(this, 0, source.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
source.addTextChangedListener(this);
|
||||
|
||||
// Set up presenter
|
||||
presenter.registerClickProvider(new AutocompletePresenter.ClickProvider<T>() {
|
||||
@Override
|
||||
public void click(@NonNull T item) {
|
||||
AutocompleteCallback<T> callback = Autocomplete.this.callback;
|
||||
EditText edit = Autocomplete.this.source;
|
||||
if (callback == null) return;
|
||||
boolean saved = block;
|
||||
block = true;
|
||||
boolean dismiss = callback.onPopupItemClicked(edit.getText(), item);
|
||||
if (dismiss) dismissPopup();
|
||||
block = saved;
|
||||
}
|
||||
});
|
||||
|
||||
builder.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls how the popup operates with an input method.
|
||||
*
|
||||
* If the popup is showing, calling this method will take effect only
|
||||
* the next time the popup is shown.
|
||||
*
|
||||
* @param mode a {@link PopupWindow} input method mode
|
||||
*/
|
||||
public void setInputMethodMode(int mode) {
|
||||
popup.setInputMethodMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the operating mode for the soft input area.
|
||||
*
|
||||
* @param mode The desired mode, see {@link WindowManager.LayoutParams#softInputMode}
|
||||
*/
|
||||
public void setSoftInputMode(int mode) {
|
||||
popup.setSoftInputMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the popup with the given query.
|
||||
* There is rarely need to call this externally: it is already triggered by events on the anchor.
|
||||
* To control when this is called, provide a good implementation of {@link AutocompletePolicy}.
|
||||
*
|
||||
* @param query query text.
|
||||
*/
|
||||
public void showPopup(@NonNull CharSequence query) {
|
||||
if (isPopupShowing() && lastQuery.equals(query.toString())) return;
|
||||
lastQuery = query.toString();
|
||||
|
||||
log("showPopup: called with filter "+query);
|
||||
if (!isPopupShowing()) {
|
||||
log("showPopup: showing");
|
||||
presenter.registerDataSetObserver(new Observer()); // Calling new to avoid leaking... maybe...
|
||||
popup.setView(presenter.getView());
|
||||
presenter.showView();
|
||||
popup.show();
|
||||
if (callback != null) callback.onPopupVisibilityChanged(true);
|
||||
}
|
||||
log("showPopup: popup should be showing... "+isPopupShowing());
|
||||
presenter.onQuery(query);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the popup, if showing.
|
||||
* There is rarely need to call this externally: it is already triggered by events on the anchor.
|
||||
* To control when this is called, provide a good implementation of {@link AutocompletePolicy}.
|
||||
*/
|
||||
public void dismissPopup() {
|
||||
if (isPopupShowing()) {
|
||||
popup.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the popup is showing.
|
||||
* @return whether the popup is currently showing
|
||||
*/
|
||||
public boolean isPopupShowing() {
|
||||
return this.popup.isShowing();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switch to control the autocomplete behavior. When disabled, no popup is shown.
|
||||
* This is useful if you want to do runtime edits to the anchor text, without triggering
|
||||
* the popup.
|
||||
*
|
||||
* @param enabled whether to enable autocompletion
|
||||
*/
|
||||
public void setEnabled(boolean enabled) {
|
||||
disabled = !enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the gravity for the popup. Basically only {@link Gravity#START} and {@link Gravity#END}
|
||||
* do work.
|
||||
*
|
||||
* @param gravity gravity for the popup
|
||||
*/
|
||||
public void setGravity(int gravity) {
|
||||
popup.setGravity(gravity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls the vertical offset of the popup from the EditText anchor.
|
||||
*
|
||||
* @param offset offset in pixels.
|
||||
*/
|
||||
public void setOffsetFromAnchor(int offset) { popup.setVerticalOffset(offset); }
|
||||
|
||||
/**
|
||||
* Controls whether the popup should listen to clicks outside its boundaries.
|
||||
*
|
||||
* @param outsideTouchable true to listen to outside clicks
|
||||
*/
|
||||
public void setOutsideTouchable(boolean outsideTouchable) { popup.setOutsideTouchable(outsideTouchable); }
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
if (block || disabled) return;
|
||||
openBefore = isPopupShowing();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
if (block || disabled) return;
|
||||
if (openBefore && !isPopupShowing()) {
|
||||
return; // Copied from somewhere.
|
||||
}
|
||||
if (!(s instanceof Spannable)) {
|
||||
source.setText(new SpannableString(s));
|
||||
return;
|
||||
}
|
||||
Spannable sp = (Spannable) s;
|
||||
|
||||
int cursor = source.getSelectionEnd();
|
||||
log("onTextChanged: cursor end position is "+cursor);
|
||||
if (cursor == -1) { // No cursor present.
|
||||
dismissPopup(); return;
|
||||
}
|
||||
if (cursor != source.getSelectionStart()) {
|
||||
// Not sure about this. We should have no problems dealing with multi selections,
|
||||
// we just take the end...
|
||||
// dismissPopup(); return;
|
||||
}
|
||||
|
||||
boolean b = block;
|
||||
block = true; // policy might add spans or other stuff.
|
||||
if (isPopupShowing() && policy.shouldDismissPopup(sp, cursor)) {
|
||||
log("onTextChanged: dismissing");
|
||||
dismissPopup();
|
||||
} else if (isPopupShowing() || policy.shouldShowPopup(sp, cursor)) {
|
||||
// LOG.now("onTextChanged: updating with filter "+policy.getQuery(sp));
|
||||
showPopup(policy.getQuery(sp));
|
||||
}
|
||||
block = b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {}
|
||||
|
||||
@Override
|
||||
public void onSpanAdded(Spannable text, Object what, int start, int end) {}
|
||||
|
||||
@Override
|
||||
public void onSpanRemoved(Spannable text, Object what, int start, int end) {}
|
||||
|
||||
@Override
|
||||
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart, int nend) {
|
||||
if (disabled || block) return;
|
||||
if (what == Selection.SELECTION_END) {
|
||||
// Selection end changed from ostart to nstart. Trigger a check.
|
||||
log("onSpanChanged: selection end moved from "+ostart+" to "+nstart);
|
||||
log("onSpanChanged: block is "+block);
|
||||
boolean b = block;
|
||||
block = true;
|
||||
if (!isPopupShowing() && policy.shouldShowPopup(text, nstart)) {
|
||||
showPopup(policy.getQuery(text));
|
||||
}
|
||||
block = b;
|
||||
}
|
||||
}
|
||||
|
||||
private class Observer extends DataSetObserver implements Runnable {
|
||||
private Handler ui = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
// ??? Not sure this is needed...
|
||||
ui.post(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isPopupShowing()) {
|
||||
// Call show again to revisit width and height.
|
||||
popup.show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A very simple {@link AutocompletePolicy} implementation.
|
||||
* Popup is shown when text length is bigger than 0, and hidden when text is empty.
|
||||
* The query string is the whole text.
|
||||
*/
|
||||
public static class SimplePolicy implements AutocompletePolicy {
|
||||
@Override
|
||||
public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) {
|
||||
return text.length() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) {
|
||||
return text.length() == 0;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CharSequence getQuery(@NonNull Spannable text) {
|
||||
return text;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull Spannable text) {}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.text.Editable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Optional callback to be passed to {@link Autocomplete.Builder}.
|
||||
*/
|
||||
public interface AutocompleteCallback<T> {
|
||||
|
||||
/**
|
||||
* Called when an item inside your list is clicked.
|
||||
* This works if your presenter has dispatched a click event.
|
||||
* At this point you can edit the text, e.g. {@code editable.append(item.toString())}.
|
||||
*
|
||||
* @param editable editable text that you can work on
|
||||
* @param item item that was clicked
|
||||
* @return true if the action is valid and the popup can be dismissed
|
||||
*/
|
||||
boolean onPopupItemClicked(@NonNull Editable editable, @NonNull T item);
|
||||
|
||||
/**
|
||||
* Called when popup visibility state changes.
|
||||
*
|
||||
* @param shown true if the popup was just shown, false if it was just hidden
|
||||
*/
|
||||
void onPopupVisibilityChanged(boolean shown);
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.text.Spannable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* This interface controls when to show or hide the popup window, and, in the first case,
|
||||
* what text should be passed to the popup {@link AutocompletePresenter}.
|
||||
*
|
||||
* @see Autocomplete.SimplePolicy for the simplest possible implementation
|
||||
*/
|
||||
public interface AutocompletePolicy {
|
||||
|
||||
/**
|
||||
* Called to understand whether the popup should be shown. Some naive examples:
|
||||
* - Show when there's text: {@code return text.length() > 0}
|
||||
* - Show when last char is @: {@code return text.getCharAt(text.length()-1) == '@'}
|
||||
*
|
||||
* @param text current text, along with its Spans
|
||||
* @param cursorPos the position of the cursor
|
||||
* @return true if popup should be shown
|
||||
*/
|
||||
boolean shouldShowPopup(@NonNull Spannable text, int cursorPos);
|
||||
|
||||
/**
|
||||
* Called to understand whether a currently shown popup should be closed, maybe
|
||||
* because text is invalid. A reasonable implementation is
|
||||
* {@code return !shouldShowPopup(text, cursorPos)}.
|
||||
*
|
||||
* However this is defined so you can add or clear spans.
|
||||
*
|
||||
* @param text current text, along with its Spans
|
||||
* @param cursorPos the position of the cursor
|
||||
* @return true if popup should be hidden
|
||||
*/
|
||||
boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos);
|
||||
|
||||
/**
|
||||
* Called to understand which query should be passed to {@link AutocompletePresenter}
|
||||
* for a showing popup. If this is called, {@link #shouldShowPopup(Spannable, int)} just returned
|
||||
* true, or {@link #shouldDismissPopup(Spannable, int)} just returned false.
|
||||
*
|
||||
* This is useful to understand which part of the text should be passed to presenters.
|
||||
* For example, user might have typed '@john' to select a username, but you just want to
|
||||
* search for 'john'.
|
||||
*
|
||||
* For more complex cases, you can add inclusive Spans in {@link #shouldShowPopup(Spannable, int)},
|
||||
* and get the span position here.
|
||||
*
|
||||
* @param text current text, along with its Spans
|
||||
* @return the query for presenter
|
||||
*/
|
||||
@NonNull
|
||||
CharSequence getQuery(@NonNull Spannable text);
|
||||
|
||||
/**
|
||||
* Called when popup is dismissed. This can be used, for instance, to clear custom Spans
|
||||
* from the text.
|
||||
*
|
||||
* @param text text at the moment of dismissing
|
||||
*/
|
||||
void onDismiss(@NonNull Spannable text);
|
||||
}
|
521
library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java
vendored
Normal file
521
library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/AutocompletePopup.java
vendored
Normal file
|
@ -0,0 +1,521 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.Gravity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.PopupWindow;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StyleRes;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.widget.PopupWindowCompat;
|
||||
|
||||
/**
|
||||
* A simplified version of andriod.widget.ListPopupWindow, which is the class used by
|
||||
* AutocompleteTextView.
|
||||
*
|
||||
* Other than being simplified, this deals with Views rather than ListViews, so the content
|
||||
* can be whatever. Lots of logic (clicks, selections etc.) has been removed because we manage that
|
||||
* in {@link AutocompletePresenter}.
|
||||
*
|
||||
*/
|
||||
class AutocompletePopup {
|
||||
private Context mContext;
|
||||
private ViewGroup mView;
|
||||
private int mHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
private int mWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
private int mMaxHeight = Integer.MAX_VALUE;
|
||||
private int mMaxWidth = Integer.MAX_VALUE;
|
||||
private int mUserMaxHeight = Integer.MAX_VALUE;
|
||||
private int mUserMaxWidth = Integer.MAX_VALUE;
|
||||
private int mHorizontalOffset = 0;
|
||||
private int mVerticalOffset = 0;
|
||||
private boolean mVerticalOffsetSet;
|
||||
private int mGravity = Gravity.NO_GRAVITY;
|
||||
private boolean mAlwaysVisible = false;
|
||||
private boolean mOutsideTouchable = true;
|
||||
private View mAnchorView;
|
||||
private final Rect mTempRect = new Rect();
|
||||
private boolean mModal;
|
||||
private PopupWindow mPopup;
|
||||
|
||||
|
||||
/**
|
||||
* Create a new, empty popup window capable of displaying items from a ListAdapter.
|
||||
* Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
|
||||
*
|
||||
* @param context Context used for contained views.
|
||||
*/
|
||||
AutocompletePopup(@NonNull Context context) {
|
||||
super();
|
||||
mContext = context;
|
||||
mPopup = new PopupWindow(context);
|
||||
mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set whether this window should be modal when shown.
|
||||
*
|
||||
* <p>If a popup window is modal, it will receive all touch and key input.
|
||||
* If the user touches outside the popup window's content area the popup window
|
||||
* will be dismissed.
|
||||
* @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
|
||||
*/
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
void setModal(boolean modal) {
|
||||
mModal = modal;
|
||||
mPopup.setFocusable(modal);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the popup window will be modal when shown.
|
||||
* @return {@code true} if the popup window will be modal, {@code false} otherwise.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
boolean isModal() {
|
||||
return mModal;
|
||||
}
|
||||
|
||||
void setElevation(float elevationPx) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) mPopup.setElevation(elevationPx);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets whether the drop-down should remain visible under certain conditions.
|
||||
*
|
||||
* The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
|
||||
* of the size or content of the list. {@link #getBackground()} will fill any space
|
||||
* that is not used by the list.
|
||||
* @param dropDownAlwaysVisible Whether to keep the drop-down visible.
|
||||
*
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
|
||||
mAlwaysVisible = dropDownAlwaysVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Whether the drop-down is visible under special conditions.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
boolean isDropDownAlwaysVisible() {
|
||||
return mAlwaysVisible;
|
||||
}
|
||||
|
||||
void setOutsideTouchable(boolean outsideTouchable) {
|
||||
mOutsideTouchable = outsideTouchable;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
boolean isOutsideTouchable() {
|
||||
return mOutsideTouchable && !mAlwaysVisible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the operating mode for the soft input area.
|
||||
* @param mode The desired mode, see
|
||||
* {@link android.view.WindowManager.LayoutParams#softInputMode}
|
||||
* for the full list
|
||||
* @see android.view.WindowManager.LayoutParams#softInputMode
|
||||
* @see #getSoftInputMode()
|
||||
*/
|
||||
void setSoftInputMode(int mode) {
|
||||
mPopup.setSoftInputMode(mode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value in {@link #setSoftInputMode(int)}.
|
||||
* @see #setSoftInputMode(int)
|
||||
* @see android.view.WindowManager.LayoutParams#softInputMode
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
int getSoftInputMode() {
|
||||
return mPopup.getSoftInputMode();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The background drawable for the popup window.
|
||||
*/
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
@Nullable
|
||||
Drawable getBackground() {
|
||||
return mPopup.getBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets a drawable to be the background for the popup window.
|
||||
* @param d A drawable to set as the background.
|
||||
*/
|
||||
void setBackgroundDrawable(@Nullable Drawable d) {
|
||||
mPopup.setBackgroundDrawable(d);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set an animation style to use when the popup window is shown or dismissed.
|
||||
* @param animationStyle Animation style to use.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setAnimationStyle(@StyleRes int animationStyle) {
|
||||
mPopup.setAnimationStyle(animationStyle);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the animation style that will be used when the popup window is
|
||||
* shown or dismissed.
|
||||
* @return Animation style that will be used.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
@StyleRes
|
||||
int getAnimationStyle() {
|
||||
return mPopup.getAnimationStyle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the view that will be used to anchor this popup.
|
||||
* @return The popup's anchor view
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
View getAnchorView() {
|
||||
return mAnchorView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the popup's anchor view. This popup will always be positioned relative to
|
||||
* the anchor view when shown.
|
||||
* @param anchor The view to use as an anchor.
|
||||
*/
|
||||
void setAnchorView(@NonNull View anchor) {
|
||||
mAnchorView = anchor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the horizontal offset of this popup from its anchor view in pixels.
|
||||
* @param offset The horizontal offset of the popup from its anchor.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setHorizontalOffset(int offset) {
|
||||
mHorizontalOffset = offset;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the vertical offset of this popup from its anchor view in pixels.
|
||||
* @param offset The vertical offset of the popup from its anchor.
|
||||
*/
|
||||
void setVerticalOffset(int offset) {
|
||||
mVerticalOffset = offset;
|
||||
mVerticalOffsetSet = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the gravity of the dropdown list. This is commonly used to
|
||||
* set gravity to START or END for alignment with the anchor.
|
||||
* @param gravity Gravity value to use
|
||||
*/
|
||||
void setGravity(int gravity) {
|
||||
mGravity = gravity;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The width of the popup window in pixels.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
int getWidth() {
|
||||
return mWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the popup window in pixels. Can also be MATCH_PARENT
|
||||
* or WRAP_CONTENT.
|
||||
* @param width Width of the popup window.
|
||||
*/
|
||||
void setWidth(int width) {
|
||||
mWidth = width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the width of the popup window by the size of its content. The final width may be
|
||||
* larger to accommodate styled window dressing.
|
||||
* @param width Desired width of content in pixels.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setContentWidth(int width) {
|
||||
Drawable popupBackground = mPopup.getBackground();
|
||||
if (popupBackground != null) {
|
||||
popupBackground.getPadding(mTempRect);
|
||||
width += mTempRect.left + mTempRect.right;
|
||||
}
|
||||
setWidth(width);
|
||||
}
|
||||
|
||||
void setMaxWidth(int width) {
|
||||
if (width > 0) {
|
||||
mUserMaxWidth = width;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The height of the popup window in pixels.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
int getHeight() {
|
||||
return mHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the popup window in pixels. Can also be MATCH_PARENT.
|
||||
* @param height Height of the popup window.
|
||||
*/
|
||||
void setHeight(int height) {
|
||||
mHeight = height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the height of the popup window by the size of its content. The final height may be
|
||||
* larger to accommodate styled window dressing.
|
||||
* @param height Desired height of content in pixels.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
void setContentHeight(int height) {
|
||||
Drawable popupBackground = mPopup.getBackground();
|
||||
if (popupBackground != null) {
|
||||
popupBackground.getPadding(mTempRect);
|
||||
height += mTempRect.top + mTempRect.bottom;
|
||||
}
|
||||
setHeight(height);
|
||||
}
|
||||
|
||||
void setMaxHeight(int height) {
|
||||
if (height > 0) {
|
||||
mUserMaxHeight = height;
|
||||
}
|
||||
}
|
||||
|
||||
void setOnDismissListener(PopupWindow.OnDismissListener listener) {
|
||||
mPopup.setOnDismissListener(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the popup list. If the list is already showing, this method
|
||||
* will recalculate the popup's size and position.
|
||||
*/
|
||||
void show() {
|
||||
if (!ViewCompat.isAttachedToWindow(getAnchorView())) return;
|
||||
|
||||
int height = buildDropDown();
|
||||
final boolean noInputMethod = isInputMethodNotNeeded();
|
||||
int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
|
||||
PopupWindowCompat.setWindowLayoutType(mPopup, mDropDownWindowLayoutType);
|
||||
|
||||
if (mPopup.isShowing()) {
|
||||
// First pass for this special case, don't know why.
|
||||
if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
int tempWidth = mWidth == ViewGroup.LayoutParams.MATCH_PARENT ? ViewGroup.LayoutParams.MATCH_PARENT : 0;
|
||||
if (noInputMethod) {
|
||||
mPopup.setWidth(tempWidth);
|
||||
mPopup.setHeight(0);
|
||||
} else {
|
||||
mPopup.setWidth(tempWidth);
|
||||
mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
}
|
||||
}
|
||||
|
||||
// The call to PopupWindow's update method below can accept -1
|
||||
// for any value you do not want to update.
|
||||
|
||||
// Width.
|
||||
int widthSpec;
|
||||
if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
widthSpec = -1;
|
||||
} else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
widthSpec = getAnchorView().getWidth();
|
||||
} else {
|
||||
widthSpec = mWidth;
|
||||
}
|
||||
widthSpec = Math.min(widthSpec, mMaxWidth);
|
||||
widthSpec = (widthSpec < 0) ? - 1 : widthSpec;
|
||||
|
||||
// Height.
|
||||
int heightSpec;
|
||||
if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
} else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
heightSpec = height;
|
||||
} else {
|
||||
heightSpec = mHeight;
|
||||
}
|
||||
heightSpec = Math.min(heightSpec, mMaxHeight);
|
||||
heightSpec = (heightSpec < 0) ? - 1 : heightSpec;
|
||||
|
||||
// Update.
|
||||
mPopup.setOutsideTouchable(isOutsideTouchable());
|
||||
if (heightSpec == 0) {
|
||||
dismiss();
|
||||
} else {
|
||||
mPopup.update(getAnchorView(), mHorizontalOffset, mVerticalOffset, widthSpec, heightSpec);
|
||||
}
|
||||
|
||||
} else {
|
||||
int widthSpec;
|
||||
if (mWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
} else if (mWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
widthSpec = getAnchorView().getWidth();
|
||||
} else {
|
||||
widthSpec = mWidth;
|
||||
}
|
||||
widthSpec = Math.min(widthSpec, mMaxWidth);
|
||||
|
||||
int heightSpec;
|
||||
if (mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
} else if (mHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||
heightSpec = height;
|
||||
} else {
|
||||
heightSpec = mHeight;
|
||||
}
|
||||
heightSpec = Math.min(heightSpec, mMaxHeight);
|
||||
|
||||
// Set width and height.
|
||||
mPopup.setWidth(widthSpec);
|
||||
mPopup.setHeight(heightSpec);
|
||||
mPopup.setClippingEnabled(true);
|
||||
|
||||
// use outside touchable to dismiss drop down when touching outside of it, so
|
||||
// only set this if the dropdown is not always visible
|
||||
mPopup.setOutsideTouchable(isOutsideTouchable());
|
||||
PopupWindowCompat.showAsDropDown(mPopup, getAnchorView(), mHorizontalOffset, mVerticalOffset, mGravity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss the popup window.
|
||||
*/
|
||||
void dismiss() {
|
||||
mPopup.dismiss();
|
||||
mPopup.setContentView(null);
|
||||
mView = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Control how the popup operates with an input method: one of
|
||||
* INPUT_METHOD_FROM_FOCUSABLE, INPUT_METHOD_NEEDED,
|
||||
* or INPUT_METHOD_NOT_NEEDED.
|
||||
*
|
||||
* <p>If the popup is showing, calling this method will take effect only
|
||||
* the next time the popup is shown or through a manual call to the {@link #show()}
|
||||
* method.</p>
|
||||
*
|
||||
* @see #show()
|
||||
*/
|
||||
void setInputMethodMode(int mode) {
|
||||
mPopup.setInputMethodMode(mode);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return {@code true} if the popup is currently showing, {@code false} otherwise.
|
||||
*/
|
||||
boolean isShowing() {
|
||||
return mPopup.isShowing();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@code true} if this popup is configured to assume the user does not need
|
||||
* to interact with the IME while it is showing, {@code false} otherwise.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
boolean isInputMethodNotNeeded() {
|
||||
return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
|
||||
}
|
||||
|
||||
|
||||
void setView(ViewGroup view) {
|
||||
mView = view;
|
||||
mView.setFocusable(true);
|
||||
mView.setFocusableInTouchMode(true);
|
||||
ViewGroup dropDownView = mView;
|
||||
mPopup.setContentView(dropDownView);
|
||||
ViewGroup.LayoutParams params = mView.getLayoutParams();
|
||||
if (params != null) {
|
||||
if (params.height > 0) setHeight(params.height);
|
||||
if (params.width > 0) setWidth(params.width);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>Builds the popup window's content and returns the height the popup
|
||||
* should have. Returns -1 when the content already exists.</p>
|
||||
*
|
||||
* @return the content's wrap content height or -1 if content already exists
|
||||
*/
|
||||
private int buildDropDown() {
|
||||
int otherHeights = 0;
|
||||
|
||||
// getMaxAvailableHeight() subtracts the padding, so we put it back
|
||||
// to get the available height for the whole window.
|
||||
final int paddingVert;
|
||||
final int paddingHoriz;
|
||||
final Drawable background = mPopup.getBackground();
|
||||
if (background != null) {
|
||||
background.getPadding(mTempRect);
|
||||
paddingVert = mTempRect.top + mTempRect.bottom;
|
||||
paddingHoriz = mTempRect.left + mTempRect.right;
|
||||
|
||||
// If we don't have an explicit vertical offset, determine one from
|
||||
// the window background so that content will line up.
|
||||
if (!mVerticalOffsetSet) {
|
||||
mVerticalOffset = -mTempRect.top;
|
||||
}
|
||||
} else {
|
||||
mTempRect.setEmpty();
|
||||
paddingVert = 0;
|
||||
paddingHoriz = 0;
|
||||
}
|
||||
|
||||
// Redefine dimensions taking into account maxWidth and maxHeight.
|
||||
final boolean ignoreBottomDecorations = mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
|
||||
final int maxContentHeight = Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ?
|
||||
mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset, ignoreBottomDecorations) :
|
||||
mPopup.getMaxAvailableHeight(getAnchorView(), mVerticalOffset);
|
||||
final int maxContentWidth = mContext.getResources().getDisplayMetrics().widthPixels - paddingHoriz;
|
||||
|
||||
mMaxHeight = Math.min(maxContentHeight + paddingVert, mUserMaxHeight);
|
||||
mMaxWidth = Math.min(maxContentWidth + paddingHoriz, mUserMaxWidth);
|
||||
// if (mHeight > 0) mHeight = Math.min(mHeight, maxContentHeight);
|
||||
// if (mWidth > 0) mWidth = Math.min(mWidth, maxContentWidth);
|
||||
|
||||
if (mAlwaysVisible || mHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
|
||||
return mMaxHeight;
|
||||
}
|
||||
|
||||
final int childWidthSpec;
|
||||
switch (mWidth) {
|
||||
case ViewGroup.LayoutParams.WRAP_CONTENT:
|
||||
childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.AT_MOST); break;
|
||||
case ViewGroup.LayoutParams.MATCH_PARENT:
|
||||
childWidthSpec = View.MeasureSpec.makeMeasureSpec(maxContentWidth, View.MeasureSpec.EXACTLY); break;
|
||||
default:
|
||||
//noinspection Range
|
||||
childWidthSpec = View.MeasureSpec.makeMeasureSpec(mWidth, View.MeasureSpec.EXACTLY); break;
|
||||
}
|
||||
|
||||
// Add padding only if the list has items in it, that way we don't show
|
||||
// the popup if it is not needed. For this reason, we measure as wrap_content.
|
||||
mView.measure(childWidthSpec, View.MeasureSpec.makeMeasureSpec(maxContentHeight, View.MeasureSpec.AT_MOST));
|
||||
final int viewHeight = mView.getMeasuredHeight();
|
||||
if (viewHeight > 0) {
|
||||
otherHeights += paddingVert + mView.getPaddingTop() + mView.getPaddingBottom();
|
||||
}
|
||||
|
||||
return Math.min(viewHeight + otherHeights, mMaxHeight);
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.DataSetObserver;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Base class for presenting items inside a popup. This is abstract and must be implemented.
|
||||
*
|
||||
* Most important methods are {@link #getView()} and {@link #onQuery(CharSequence)}.
|
||||
*/
|
||||
public abstract class AutocompletePresenter<T> {
|
||||
|
||||
private Context context;
|
||||
private boolean isShowing;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public AutocompletePresenter(@NonNull Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* At this point the presenter is passed the {@link ClickProvider}.
|
||||
* The contract is that {@link ClickProvider#click(Object)} must be called when a list item
|
||||
* is clicked. This ensure that the autocomplete callback will receive the event.
|
||||
*
|
||||
* @param provider a click provider for this presenter.
|
||||
*/
|
||||
protected void registerClickProvider(ClickProvider<T> provider) {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful if you wish to change width/height based on content height.
|
||||
* The contract is to call {@link DataSetObserver#onChanged()} when your view has
|
||||
* changes.
|
||||
*
|
||||
* This is called after {@link #getView()}.
|
||||
*
|
||||
* @param observer the observer.
|
||||
*/
|
||||
protected void registerDataSetObserver(@NonNull DataSetObserver observer) {}
|
||||
|
||||
/**
|
||||
* Called each time the popup is shown. You are meant to inflate the view here.
|
||||
* You can get a LayoutInflater using {@link #getContext()}.
|
||||
*
|
||||
* @return a ViewGroup for the popup
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract ViewGroup getView();
|
||||
|
||||
/**
|
||||
* Provide the {@link PopupDimensions} for this popup. Called just once.
|
||||
* You can use fixed dimensions or {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and
|
||||
* {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}.
|
||||
*
|
||||
* @return a PopupDimensions object
|
||||
*/
|
||||
// Called at first to understand which dimensions to use for the popup.
|
||||
@NonNull
|
||||
protected PopupDimensions getPopupDimensions() {
|
||||
return new PopupDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform firther initialization here. Called after {@link #getView()},
|
||||
* each time the popup is shown.
|
||||
*/
|
||||
protected abstract void onViewShown();
|
||||
|
||||
/**
|
||||
* Called to update the view to filter results with the query.
|
||||
* It is called any time the popup is shown, and any time the text changes and query is updated.
|
||||
*
|
||||
* @param query query from the edit text, to filter our results
|
||||
*/
|
||||
protected abstract void onQuery(@Nullable CharSequence query);
|
||||
|
||||
/**
|
||||
* Called when the popup is hidden, to release resources.
|
||||
*/
|
||||
protected abstract void onViewHidden();
|
||||
|
||||
/**
|
||||
* @return this presenter context
|
||||
*/
|
||||
@NonNull
|
||||
protected final Context getContext() {
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return whether we are showing currently
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected final boolean isShowing() {
|
||||
return isShowing;
|
||||
}
|
||||
|
||||
final void showView() {
|
||||
isShowing = true;
|
||||
onViewShown();
|
||||
}
|
||||
|
||||
final void hideView() {
|
||||
isShowing = false;
|
||||
onViewHidden();
|
||||
}
|
||||
|
||||
public interface ClickProvider<T> {
|
||||
void click(@NonNull T item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides width, height, maxWidth and maxHeight for the popup.
|
||||
* @see #getPopupDimensions()
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static class PopupDimensions {
|
||||
public int width = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
public int height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
public int maxWidth = Integer.MAX_VALUE;
|
||||
public int maxHeight = Integer.MAX_VALUE;
|
||||
}
|
||||
}
|
184
library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java
vendored
Normal file
184
library/external/autocomplete/src/main/java/com/otaliastudios/autocomplete/CharPolicy.java
vendored
Normal file
|
@ -0,0 +1,184 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.text.Spannable;
|
||||
import android.text.Spanned;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
|
||||
/**
|
||||
* A special {@link AutocompletePolicy} for cases when you want to trigger the popup when a
|
||||
* certain character is shown.
|
||||
*
|
||||
* For instance, this might be the case for hashtags ('#') or usernames ('@') or whatever you wish.
|
||||
* Passing this to {@link Autocomplete.Builder} ensures the following behavior (assuming '@'):
|
||||
* - text "@john" : presenter will be passed the query "john"
|
||||
* - text "You should see this @j" : presenter will be passed the query "j"
|
||||
* - text "You should see this @john @m" : presenter will be passed the query "m"
|
||||
*/
|
||||
public class CharPolicy implements AutocompletePolicy {
|
||||
|
||||
private final static String TAG = CharPolicy.class.getSimpleName();
|
||||
private final static boolean DEBUG = false;
|
||||
|
||||
private static void log(@NonNull String log) {
|
||||
if (DEBUG) Log.e(TAG, log);
|
||||
}
|
||||
|
||||
private final char CH;
|
||||
private final int[] INT = new int[2];
|
||||
private boolean needSpaceBefore = true;
|
||||
|
||||
/**
|
||||
* Constructs a char policy for the given character.
|
||||
*
|
||||
* @param trigger the triggering character.
|
||||
*/
|
||||
public CharPolicy(char trigger) {
|
||||
CH = trigger;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a char policy for the given character.
|
||||
* You can choose whether a whitespace is needed before 'trigger'.
|
||||
*
|
||||
* @param trigger the triggering character.
|
||||
* @param needSpaceBefore whether we need a space before trigger
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public CharPolicy(char trigger, boolean needSpaceBefore) {
|
||||
CH = trigger;
|
||||
this.needSpaceBefore = needSpaceBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overriden to understand which characters are valid. The default implementation
|
||||
* returns true for any character except whitespaces.
|
||||
*
|
||||
* @param ch the character
|
||||
* @return whether it's valid part of a query
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
protected boolean isValidChar(char ch) {
|
||||
return !Character.isWhitespace(ch);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private int[] checkText(@NonNull Spannable text, int cursorPos) {
|
||||
final int spanEnd = cursorPos;
|
||||
char last = 'x';
|
||||
cursorPos -= 1; // If the cursor is at the end, we will have cursorPos = length. Go back by 1.
|
||||
while (cursorPos >= 0 && last != CH) {
|
||||
char ch = text.charAt(cursorPos);
|
||||
log("checkText: char is "+ch);
|
||||
if (isValidChar(ch)) {
|
||||
// We are going back
|
||||
log("checkText: char is valid");
|
||||
cursorPos -= 1;
|
||||
last = ch;
|
||||
} else {
|
||||
// We got a whitespace before getting a CH. This is invalid.
|
||||
log("checkText: char is not valid, returning NULL");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
cursorPos += 1; // + 1 because we end BEHIND the valid selection
|
||||
|
||||
// Start checking.
|
||||
if (cursorPos == 0 && last != CH) {
|
||||
// We got to the start of the string, and no CH was encountered. Nothing to do.
|
||||
log("checkText: got to start but no CH, returning NULL");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Additional checks for cursorPos - 1
|
||||
if (cursorPos > 0 && needSpaceBefore) {
|
||||
char ch = text.charAt(cursorPos-1);
|
||||
if (!Character.isWhitespace(ch)) {
|
||||
log("checkText: char before is not whitespace, returning NULL");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// All seems OK.
|
||||
final int spanStart = cursorPos + 1; // + 1 because we want to exclude CH from the query
|
||||
INT[0] = spanStart;
|
||||
INT[1] = spanEnd;
|
||||
log("checkText: found! cursorPos="+cursorPos);
|
||||
log("checkText: found! spanStart="+spanStart);
|
||||
log("checkText: found! spanEnd="+spanEnd);
|
||||
return INT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) {
|
||||
// Returning true if, right before cursorPos, we have a word starting with @.
|
||||
log("shouldShowPopup: text is "+text);
|
||||
log("shouldShowPopup: cursorPos is "+cursorPos);
|
||||
int[] show = checkText(text, cursorPos);
|
||||
if (show != null) {
|
||||
text.setSpan(new QuerySpan(), show[0], show[1], Spanned.SPAN_INCLUSIVE_INCLUSIVE);
|
||||
return true;
|
||||
}
|
||||
log("shouldShowPopup: returning false");
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) {
|
||||
log("shouldDismissPopup: text is "+text);
|
||||
log("shouldDismissPopup: cursorPos is "+cursorPos);
|
||||
boolean dismiss = checkText(text, cursorPos) == null;
|
||||
log("shouldDismissPopup: returning "+dismiss);
|
||||
return dismiss;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public CharSequence getQuery(@NonNull Spannable text) {
|
||||
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
|
||||
if (span == null || span.length == 0) {
|
||||
// Should never happen.
|
||||
log("getQuery: there's no span!");
|
||||
return "";
|
||||
}
|
||||
log("getQuery: found spans: "+span.length);
|
||||
QuerySpan sp = span[0];
|
||||
log("getQuery: span start is "+text.getSpanStart(sp));
|
||||
log("getQuery: span end is "+text.getSpanEnd(sp));
|
||||
CharSequence seq = text.subSequence(text.getSpanStart(sp), text.getSpanEnd(sp));
|
||||
log("getQuery: returning "+seq);
|
||||
return seq;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull Spannable text) {
|
||||
// Remove any span added by shouldShow. Should be useless, but anyway.
|
||||
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
|
||||
for (QuerySpan s : span) {
|
||||
text.removeSpan(s);
|
||||
}
|
||||
}
|
||||
|
||||
private static class QuerySpan {}
|
||||
|
||||
/**
|
||||
* Returns the current query out of the given Spannable.
|
||||
* @param text the anchor text
|
||||
* @return an int[] with query start and query end positions
|
||||
*/
|
||||
@Nullable
|
||||
public static int[] getQueryRange(@NonNull Spannable text) {
|
||||
QuerySpan[] span = text.getSpans(0, text.length(), QuerySpan.class);
|
||||
if (span == null || span.length == 0) return null;
|
||||
if (span.length > 1) {
|
||||
// Won't happen
|
||||
log("getQueryRange: ERR: MORE THAN ONE QuerySpan.");
|
||||
}
|
||||
QuerySpan sp = span[0];
|
||||
return new int[]{text.getSpanStart(sp), text.getSpanEnd(sp)};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,152 @@
|
|||
package com.otaliastudios.autocomplete;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.DataSetObserver;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.CallSuper;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Simple {@link AutocompletePresenter} implementation that hosts a {@link RecyclerView}.
|
||||
* Supports {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} natively.
|
||||
* The only contract is to
|
||||
*
|
||||
* - provide a {@link RecyclerView.Adapter} in {@link #instantiateAdapter()}
|
||||
* - call {@link #dispatchClick(Object)} when an object is clicked
|
||||
* - update your data during {@link #onQuery(CharSequence)}
|
||||
*
|
||||
* @param <T> your model object (the object displayed by the list)
|
||||
*/
|
||||
public abstract class RecyclerViewPresenter<T> extends AutocompletePresenter<T> {
|
||||
|
||||
private RecyclerView recycler;
|
||||
private ClickProvider<T> clicks;
|
||||
private Observer observer;
|
||||
|
||||
public RecyclerViewPresenter(@NonNull Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void registerClickProvider(@NonNull ClickProvider<T> provider) {
|
||||
this.clicks = provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected final void registerDataSetObserver(@NonNull DataSetObserver observer) {
|
||||
this.observer = new Observer(observer);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected ViewGroup getView() {
|
||||
recycler = new RecyclerView(getContext());
|
||||
RecyclerView.Adapter adapter = instantiateAdapter();
|
||||
recycler.setAdapter(adapter);
|
||||
recycler.setLayoutManager(instantiateLayoutManager());
|
||||
if (observer != null) {
|
||||
adapter.registerAdapterDataObserver(observer);
|
||||
observer = null;
|
||||
}
|
||||
return recycler;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onViewShown() {}
|
||||
|
||||
@CallSuper
|
||||
@Override
|
||||
protected void onViewHidden() {
|
||||
recycler = null;
|
||||
observer = null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Nullable
|
||||
protected final RecyclerView getRecyclerView() {
|
||||
return recycler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch click event to {@link AutocompleteCallback}.
|
||||
* Should be called when items are clicked.
|
||||
*
|
||||
* @param item the clicked item.
|
||||
*/
|
||||
protected final void dispatchClick(@NonNull T item) {
|
||||
if (clicks != null) clicks.click(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Request that the popup should recompute its dimensions based on a recent change in
|
||||
* the view being displayed.
|
||||
*
|
||||
* This is already managed internally for {@link RecyclerView} events.
|
||||
* Only use it for changes in other views that you have added to the popup,
|
||||
* and only if one of the dimensions for the popup is WRAP_CONTENT .
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
protected final void dispatchLayoutChange() {
|
||||
if (observer != null) observer.onChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide an adapter for the recycler.
|
||||
* This should be a fresh instance every time this is called.
|
||||
*
|
||||
* @return a new adapter.
|
||||
*/
|
||||
@NonNull
|
||||
protected abstract RecyclerView.Adapter instantiateAdapter();
|
||||
|
||||
/**
|
||||
* Provides a layout manager for the recycler.
|
||||
* This should be a fresh instance every time this is called.
|
||||
* Defaults to a vertical LinearLayoutManager, which is guaranteed to work well.
|
||||
*
|
||||
* @return a new layout manager.
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@NonNull
|
||||
protected RecyclerView.LayoutManager instantiateLayoutManager() {
|
||||
return new LinearLayoutManager(getContext(), LinearLayoutManager.VERTICAL, false);
|
||||
}
|
||||
|
||||
private final static class Observer extends RecyclerView.AdapterDataObserver {
|
||||
|
||||
private DataSetObserver root;
|
||||
|
||||
Observer(@NonNull DataSetObserver root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
root.onChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(int positionStart, int itemCount) {
|
||||
root.onChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
|
||||
root.onChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
root.onChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
root.onChanged();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ include ':library:external:jsonviewer'
|
|||
include ':library:external:diff-match-patch'
|
||||
include ':library:external:dialpad'
|
||||
include ':library:external:textdrawable'
|
||||
include ':library:external:autocomplete'
|
||||
|
||||
include ':library:rustCrypto'
|
||||
include ':matrix-sdk-android'
|
||||
|
|
|
@ -117,6 +117,7 @@ dependencies {
|
|||
implementation project(":library:external:jsonviewer")
|
||||
implementation project(":library:external:diff-match-patch")
|
||||
implementation project(":library:external:textdrawable")
|
||||
implementation project(":library:external:autocomplete")
|
||||
implementation project(":library:ui-strings")
|
||||
implementation project(":library:ui-styles")
|
||||
implementation project(":library:core-utils")
|
||||
|
@ -210,8 +211,6 @@ dependencies {
|
|||
// Alerter
|
||||
implementation 'com.github.tapadoo:alerter:7.2.4'
|
||||
|
||||
implementation 'com.otaliastudios:autocomplete:1.1.0'
|
||||
|
||||
// Shake detection
|
||||
implementation 'com.squareup:seismic:1.0.3'
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.otaliastudios.autocomplete.AutocompletePresenter
|
||||
|
||||
abstract class RecyclerViewPresenter<T>(context: Context?) : AutocompletePresenter<T>(context) {
|
||||
abstract class RecyclerViewPresenter<T>(context: Context) : AutocompletePresenter<T>(context) {
|
||||
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var clicks: ClickProvider<T>? = null
|
||||
|
|
|
@ -32,16 +32,15 @@ class CommandAutocompletePolicy @Inject constructor() : AutocompletePolicy {
|
|||
return ""
|
||||
}
|
||||
|
||||
override fun onDismiss(text: Spannable?) {
|
||||
override fun onDismiss(text: Spannable) {
|
||||
}
|
||||
|
||||
// Only if text which starts with '/' and without space
|
||||
override fun shouldShowPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||
return enabled && text?.startsWith("/") == true &&
|
||||
!text.contains(" ")
|
||||
override fun shouldShowPopup(text: Spannable, cursorPos: Int): Boolean {
|
||||
return enabled && text.startsWith("/") && !text.contains(" ")
|
||||
}
|
||||
|
||||
override fun shouldDismissPopup(text: Spannable?, cursorPos: Int): Boolean {
|
||||
override fun shouldDismissPopup(text: Spannable, cursorPos: Int): Boolean {
|
||||
return !shouldShowPopup(text, cursorPos)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue