package de.blau.android.views; import android.content.Context; import android.support.v7.widget.AppCompatAutoCompleteTextView; import android.text.Editable; import android.text.SpannableString; import android.text.Spanned; import android.text.TextUtils; import android.text.method.QwertyKeyListener; import android.util.AttributeSet; import android.util.Log; import android.view.View; import android.widget.Filter; import de.blau.android.R; /** * Custom version of AutoCompleteTextView with switchable behaviour of MultiAutoCompleteTextView * for supporting OSM lists (aka ; separated values) * * Hack: offsets dropdown to the left by the views height and make it wider by the same amount * Includes multi-autocomplete logic enabled by setting a tokenizer * * @author simon * * Includes code from MultiAutoCompleteTextView.java * Copyright (C) 2007 The Android Open Source Project, Licensed under the Apache License, Version 2.0 * */ public class CustomAutoCompleteTextView extends AppCompatAutoCompleteTextView { private static final String DEBUG_TAG = CustomAutoCompleteTextView.class.getName(); private Tokenizer mTokenizer = null; private int parentWidth = -1; public CustomAutoCompleteTextView(Context context) { this(context,null); } public CustomAutoCompleteTextView(Context context, AttributeSet attrs) { this(context, attrs, R.attr.autoCompleteTextViewStyle); } public CustomAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); // set a default onClickListener that displays the dropdown OnClickListener autocompleteOnClick = new OnClickListener() { @Override public void onClick(View v) { if (v.hasFocus()) { Log.d(DEBUG_TAG,"onClick"); ((CustomAutoCompleteTextView)v).showDropDown(); } } }; setOnClickListener(autocompleteOnClick); } @Override public void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // Log.d(DEBUG_TAG, "onSizeChanged"); if (w == 0 && h == 0) { return; } // Log.d(DEBUG_TAG,"w=" + w +" h="+h); // this is not really satisfactory if (parentWidth == -1) { // upps return; } int ddw = parentWidth - w; setDropDownHorizontalOffset(-ddw); setDropDownWidth(ddw); } public void setParentWidth(int parentWidth) { this.parentWidth = parentWidth; } /** * Sets the Tokenizer that will be used to determine the relevant * range of the text where the user is typing. */ public void setTokenizer(Tokenizer t) { mTokenizer = t; } /** * Instead of filtering on the entire contents of the edit box, * this subclass method filters on the range from * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} * if the length of that range meets or exceeds {@link #getThreshold}. */ @Override protected void performFiltering(CharSequence text, int keyCode) { if (mTokenizer==null) { super.performFiltering(text, keyCode); return; } if (enoughToFilter()) { int end = super.getSelectionEnd(); int start = mTokenizer.findTokenStart(text, end); performFiltering(text, start, end, keyCode); } else { dismissDropDown(); Filter f = getFilter(); if (f != null) { f.filter(null); } } } /** * Instead of filtering whenever the total length of the text * exceeds the threshhold, this subclass filters only when the * length of the range from * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} * meets or exceeds {@link #getThreshold}. */ @Override public boolean enoughToFilter() { if (mTokenizer==null) { return super.enoughToFilter(); } Editable text = super.getText(); int end = getSelectionEnd(); if (end < 0) { return false; } int start = mTokenizer.findTokenStart(text, end); return end - start >= getThreshold(); } /** * Instead of validating the entire text, this subclass method validates * each token of the text individually. Empty tokens are removed. */ @Override public void performValidation() { if (mTokenizer==null) { super.performValidation(); return; } Validator v = getValidator(); if (v == null) { return; } Editable e = getText(); int i = getText().length(); while (i > 0) { int start = mTokenizer.findTokenStart(e, i); int end = mTokenizer.findTokenEnd(e, start); CharSequence sub = e.subSequence(start, end); if (TextUtils.isEmpty(sub)) { e.replace(start, i, ""); } else if (!v.isValid(sub)) { e.replace(start, i, mTokenizer.terminateToken(v.fixText(sub))); } i = start; } } /** * <p>Starts filtering the content of the drop down list. The filtering * pattern is the specified range of text from the edit box. Subclasses may * override this method to filter with a different pattern, for * instance a smaller substring of <code>text</code>.</p> */ private void performFiltering(CharSequence text, int start, int end, int keyCode) { Log.d(DEBUG_TAG,"performFiltering 2"); getFilter().filter(text.subSequence(start, end), this); } /** * <p>Performs the text completion by replacing the range from * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the * the result of passing <code>text</code> through * {@link Tokenizer#terminateToken}. * In addition, the replaced region will be marked as an AutoText * substition so that if the user immediately presses DEL, the * completion will be undone. * Subclasses may override this method to do some different * insertion of the content into the edit box.</p> * * @param text the selected suggestion in the drop down list */ @Override protected void replaceText(CharSequence text) { if (mTokenizer==null) { super.replaceText(text); } } /** * setText is final and can't be overridden * @param text */ public void setOrReplaceText(String text) { if (mTokenizer==null) { super.setText(text); return; } Log.d(DEBUG_TAG,"etOrReplaceText " + text); clearComposingText(); int end = getSelectionEnd(); int start = mTokenizer.findTokenStart(super.getText(), end); Editable editable = super.getText(); String original = TextUtils.substring(editable, start, end); QwertyKeyListener.markAsReplaced(editable, start, end, original); editable.replace(start, end, mTokenizer.terminateToken(text)); } public interface Tokenizer { /** * Returns the start of the token that ends at offset * <code>cursor</code> within <code>text</code>. */ int findTokenStart(CharSequence text, int cursor); /** * Returns the end of the token (minus trailing punctuation) * that begins at offset <code>cursor</code> within <code>text</code>. */ int findTokenEnd(CharSequence text, int cursor); /** * Returns <code>text</code>, modified, if necessary, to ensure that * it ends with a token terminator (for example a space or comma). */ CharSequence terminateToken(CharSequence text); } /** * This simple Tokenizer can be used for lists where the items are * separated by a single character and one or more spaces. */ public static class SingleCharTokenizer implements Tokenizer { final static char DEFAULT = ';'; char separator = DEFAULT; /** * default constructor */ public SingleCharTokenizer() { } /** * Constructor for potential different separator characters * @param s */ public SingleCharTokenizer(final char s) { separator = s; } public int findTokenStart(CharSequence text, int cursor) { int i = cursor; while (i > 0 && text.charAt(i - 1) != separator) { i--; } while (i < cursor && text.charAt(i) == ' ') { i++; } return i; } public int findTokenEnd(CharSequence text, int cursor) { int i = cursor; int len = text.length(); while (i < len) { if (text.charAt(i) == separator) { return i; } else { i++; } } return len; } public CharSequence terminateToken(CharSequence text) { int i = text.length(); while (i > 0 && text.charAt(i - 1) == ' ') { i--; } if (i > 0 && text.charAt(i - 1) == separator) { return text; } else { if (text instanceof Spanned) { SpannableString sp = new SpannableString(text + String.valueOf(separator) + " "); TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0); return sp; } else { return text + String.valueOf(separator) + " "; } } } } }