• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.database.DataSetObserver;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.text.Editable;
25 import android.text.Selection;
26 import android.text.TextUtils;
27 import android.text.TextWatcher;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.view.KeyEvent;
31 import android.view.LayoutInflater;
32 import android.view.View;
33 import android.view.ViewGroup;
34 import android.view.WindowManager;
35 import android.view.inputmethod.CompletionInfo;
36 import android.view.inputmethod.EditorInfo;
37 import android.view.inputmethod.InputMethodManager;
38 
39 import com.android.internal.R;
40 
41 
42 /**
43  * <p>An editable text view that shows completion suggestions automatically
44  * while the user is typing. The list of suggestions is displayed in a drop
45  * down menu from which the user can choose an item to replace the content
46  * of the edit box with.</p>
47  *
48  * <p>The drop down can be dismissed at any time by pressing the back key or,
49  * if no item is selected in the drop down, by pressing the enter/dpad center
50  * key.</p>
51  *
52  * <p>The list of suggestions is obtained from a data adapter and appears
53  * only after a given number of characters defined by
54  * {@link #getThreshold() the threshold}.</p>
55  *
56  * <p>The following code snippet shows how to create a text view which suggests
57  * various countries names while the user is typing:</p>
58  *
59  * <pre class="prettyprint">
60  * public class CountriesActivity extends Activity {
61  *     protected void onCreate(Bundle icicle) {
62  *         super.onCreate(icicle);
63  *         setContentView(R.layout.countries);
64  *
65  *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
66  *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
67  *         AutoCompleteTextView textView = (AutoCompleteTextView)
68  *                 findViewById(R.id.countries_list);
69  *         textView.setAdapter(adapter);
70  *     }
71  *
72  *     private static final String[] COUNTRIES = new String[] {
73  *         "Belgium", "France", "Italy", "Germany", "Spain"
74  *     };
75  * }
76  * </pre>
77  *
78  * <p>See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
79  * guide.</p>
80  *
81  * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
82  * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
83  * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
84  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
85  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
86  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
87  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
88  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownVerticalOffset
89  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset
90  */
91 public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
92     static final boolean DEBUG = false;
93     static final String TAG = "AutoCompleteTextView";
94 
95     static final int EXPAND_MAX = 3;
96 
97     private CharSequence mHintText;
98     private TextView mHintView;
99     private int mHintResource;
100 
101     private ListAdapter mAdapter;
102     private Filter mFilter;
103     private int mThreshold;
104 
105     private ListPopupWindow mPopup;
106     private int mDropDownAnchorId;
107 
108     private AdapterView.OnItemClickListener mItemClickListener;
109     private AdapterView.OnItemSelectedListener mItemSelectedListener;
110 
111     private boolean mDropDownDismissedOnCompletion = true;
112 
113     private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
114     private boolean mOpenBefore;
115 
116     private Validator mValidator = null;
117 
118     // Set to true when text is set directly and no filtering shall be performed
119     private boolean mBlockCompletion;
120 
121     // When set, an update in the underlying adapter will update the result list popup.
122     // Set to false when the list is hidden to prevent asynchronous updates to popup the list again.
123     private boolean mPopupCanBeUpdated = true;
124 
125     private PassThroughClickListener mPassThroughClickListener;
126     private PopupDataSetObserver mObserver;
127 
AutoCompleteTextView(Context context)128     public AutoCompleteTextView(Context context) {
129         this(context, null);
130     }
131 
AutoCompleteTextView(Context context, AttributeSet attrs)132     public AutoCompleteTextView(Context context, AttributeSet attrs) {
133         this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
134     }
135 
AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle)136     public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
137         super(context, attrs, defStyle);
138 
139         mPopup = new ListPopupWindow(context, attrs,
140                 com.android.internal.R.attr.autoCompleteTextViewStyle);
141         mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
142         mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
143 
144         TypedArray a =
145             context.obtainStyledAttributes(
146                 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0);
147 
148         mThreshold = a.getInt(
149                 R.styleable.AutoCompleteTextView_completionThreshold, 2);
150 
151         mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector));
152         mPopup.setVerticalOffset((int)
153                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f));
154         mPopup.setHorizontalOffset((int)
155                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f));
156 
157         // Get the anchor's id now, but the view won't be ready, so wait to actually get the
158         // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
159         // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return
160         // this TextView, as a default anchoring point.
161         mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor,
162                 View.NO_ID);
163 
164         // For dropdown width, the developer can specify a specific width, or MATCH_PARENT
165         // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
166         mPopup.setWidth(a.getLayoutDimension(
167                 R.styleable.AutoCompleteTextView_dropDownWidth,
168                 ViewGroup.LayoutParams.WRAP_CONTENT));
169         mPopup.setHeight(a.getLayoutDimension(
170                 R.styleable.AutoCompleteTextView_dropDownHeight,
171                 ViewGroup.LayoutParams.WRAP_CONTENT));
172 
173         mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
174                 R.layout.simple_dropdown_hint);
175 
176         mPopup.setOnItemClickListener(new DropDownItemClickListener());
177         setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint));
178 
179         // Always turn on the auto complete input type flag, since it
180         // makes no sense to use this widget without it.
181         int inputType = getInputType();
182         if ((inputType&EditorInfo.TYPE_MASK_CLASS)
183                 == EditorInfo.TYPE_CLASS_TEXT) {
184             inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
185             setRawInputType(inputType);
186         }
187 
188         a.recycle();
189 
190         setFocusable(true);
191 
192         addTextChangedListener(new MyWatcher());
193 
194         mPassThroughClickListener = new PassThroughClickListener();
195         super.setOnClickListener(mPassThroughClickListener);
196     }
197 
198     @Override
setOnClickListener(OnClickListener listener)199     public void setOnClickListener(OnClickListener listener) {
200         mPassThroughClickListener.mWrapped = listener;
201     }
202 
203     /**
204      * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
205      */
onClickImpl()206     private void onClickImpl() {
207         // If the dropdown is showing, bring the keyboard to the front
208         // when the user touches the text field.
209         if (isPopupShowing()) {
210             ensureImeVisible(true);
211         }
212     }
213 
214     /**
215      * <p>Sets the optional hint text that is displayed at the bottom of the
216      * the matching list.  This can be used as a cue to the user on how to
217      * best use the list, or to provide extra information.</p>
218      *
219      * @param hint the text to be displayed to the user
220      *
221      * @see #getCompletionHint()
222      *
223      * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
224      */
setCompletionHint(CharSequence hint)225     public void setCompletionHint(CharSequence hint) {
226         mHintText = hint;
227         if (hint != null) {
228             if (mHintView == null) {
229                 final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate(
230                         mHintResource, null).findViewById(com.android.internal.R.id.text1);
231                 hintView.setText(mHintText);
232                 mHintView = hintView;
233                 mPopup.setPromptView(hintView);
234             } else {
235                 mHintView.setText(hint);
236             }
237         } else {
238             mPopup.setPromptView(null);
239             mHintView = null;
240         }
241     }
242 
243     /**
244      * Gets the optional hint text displayed at the bottom of the the matching list.
245      *
246      * @return The hint text, if any
247      *
248      * @see #setCompletionHint(CharSequence)
249      *
250      * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
251      */
getCompletionHint()252     public CharSequence getCompletionHint() {
253         return mHintText;
254     }
255 
256     /**
257      * <p>Returns the current width for the auto-complete drop down list. This can
258      * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
259      * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
260      *
261      * @return the width for the drop down list
262      *
263      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
264      */
getDropDownWidth()265     public int getDropDownWidth() {
266         return mPopup.getWidth();
267     }
268 
269     /**
270      * <p>Sets the current width for the auto-complete drop down list. This can
271      * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
272      * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
273      *
274      * @param width the width to use
275      *
276      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
277      */
setDropDownWidth(int width)278     public void setDropDownWidth(int width) {
279         mPopup.setWidth(width);
280     }
281 
282     /**
283      * <p>Returns the current height for the auto-complete drop down list. This can
284      * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
285      * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
286      * of the drop down's content.</p>
287      *
288      * @return the height for the drop down list
289      *
290      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
291      */
getDropDownHeight()292     public int getDropDownHeight() {
293         return mPopup.getHeight();
294     }
295 
296     /**
297      * <p>Sets the current height for the auto-complete drop down list. This can
298      * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
299      * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
300      * of the drop down's content.</p>
301      *
302      * @param height the height to use
303      *
304      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
305      */
setDropDownHeight(int height)306     public void setDropDownHeight(int height) {
307         mPopup.setHeight(height);
308     }
309 
310     /**
311      * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
312      *
313      * @return the view's id, or {@link View#NO_ID} if none specified
314      *
315      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
316      */
getDropDownAnchor()317     public int getDropDownAnchor() {
318         return mDropDownAnchorId;
319     }
320 
321     /**
322      * <p>Sets the view to which the auto-complete drop down list should anchor. The view
323      * corresponding to this id will not be loaded until the next time it is needed to avoid
324      * loading a view which is not yet instantiated.</p>
325      *
326      * @param id the id to anchor the drop down list view to
327      *
328      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
329      */
setDropDownAnchor(int id)330     public void setDropDownAnchor(int id) {
331         mDropDownAnchorId = id;
332         mPopup.setAnchorView(null);
333     }
334 
335     /**
336      * <p>Gets the background of the auto-complete drop-down list.</p>
337      *
338      * @return the background drawable
339      *
340      * @attr ref android.R.styleable#PopupWindow_popupBackground
341      */
getDropDownBackground()342     public Drawable getDropDownBackground() {
343         return mPopup.getBackground();
344     }
345 
346     /**
347      * <p>Sets the background of the auto-complete drop-down list.</p>
348      *
349      * @param d the drawable to set as the background
350      *
351      * @attr ref android.R.styleable#PopupWindow_popupBackground
352      */
setDropDownBackgroundDrawable(Drawable d)353     public void setDropDownBackgroundDrawable(Drawable d) {
354         mPopup.setBackgroundDrawable(d);
355     }
356 
357     /**
358      * <p>Sets the background of the auto-complete drop-down list.</p>
359      *
360      * @param id the id of the drawable to set as the background
361      *
362      * @attr ref android.R.styleable#PopupWindow_popupBackground
363      */
setDropDownBackgroundResource(int id)364     public void setDropDownBackgroundResource(int id) {
365         mPopup.setBackgroundDrawable(getResources().getDrawable(id));
366     }
367 
368     /**
369      * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
370      *
371      * @param offset the vertical offset
372      */
setDropDownVerticalOffset(int offset)373     public void setDropDownVerticalOffset(int offset) {
374         mPopup.setVerticalOffset(offset);
375     }
376 
377     /**
378      * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
379      *
380      * @return the vertical offset
381      */
getDropDownVerticalOffset()382     public int getDropDownVerticalOffset() {
383         return mPopup.getVerticalOffset();
384     }
385 
386     /**
387      * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
388      *
389      * @param offset the horizontal offset
390      */
setDropDownHorizontalOffset(int offset)391     public void setDropDownHorizontalOffset(int offset) {
392         mPopup.setHorizontalOffset(offset);
393     }
394 
395     /**
396      * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
397      *
398      * @return the horizontal offset
399      */
getDropDownHorizontalOffset()400     public int getDropDownHorizontalOffset() {
401         return mPopup.getHorizontalOffset();
402     }
403 
404      /**
405      * <p>Sets the animation style of the auto-complete drop-down list.</p>
406      *
407      * <p>If the drop-down is showing, calling this method will take effect only
408      * the next time the drop-down is shown.</p>
409      *
410      * @param animationStyle animation style to use when the drop-down appears
411      *      and disappears.  Set to -1 for the default animation, 0 for no
412      *      animation, or a resource identifier for an explicit animation.
413      *
414      * @hide Pending API council approval
415      */
setDropDownAnimationStyle(int animationStyle)416     public void setDropDownAnimationStyle(int animationStyle) {
417         mPopup.setAnimationStyle(animationStyle);
418     }
419 
420     /**
421      * <p>Returns the animation style that is used when the drop-down list appears and disappears
422      * </p>
423      *
424      * @return the animation style that is used when the drop-down list appears and disappears
425      *
426      * @hide Pending API council approval
427      */
getDropDownAnimationStyle()428     public int getDropDownAnimationStyle() {
429         return mPopup.getAnimationStyle();
430     }
431 
432     /**
433      * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
434      *
435      * @hide Pending API council approval
436      */
isDropDownAlwaysVisible()437     public boolean isDropDownAlwaysVisible() {
438         return mPopup.isDropDownAlwaysVisible();
439     }
440 
441     /**
442      * Sets whether the drop-down should remain visible as long as there is there is
443      * {@link #enoughToFilter()}.  This is useful if an unknown number of results are expected
444      * to show up in the adapter sometime in the future.
445      *
446      * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
447      * of the size or content of the list.  {@link #getDropDownBackground()} will fill any space
448      * that is not used by the list.
449      *
450      * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
451      *
452      * @hide Pending API council approval
453      */
setDropDownAlwaysVisible(boolean dropDownAlwaysVisible)454     public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
455         mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible);
456     }
457 
458     /**
459      * Checks whether the drop-down is dismissed when a suggestion is clicked.
460      *
461      * @hide Pending API council approval
462      */
isDropDownDismissedOnCompletion()463     public boolean isDropDownDismissedOnCompletion() {
464         return mDropDownDismissedOnCompletion;
465     }
466 
467     /**
468      * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
469      * true by default.
470      *
471      * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
472      *
473      * @hide Pending API council approval
474      */
setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion)475     public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
476         mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
477     }
478 
479     /**
480      * <p>Returns the number of characters the user must type before the drop
481      * down list is shown.</p>
482      *
483      * @return the minimum number of characters to type to show the drop down
484      *
485      * @see #setThreshold(int)
486      *
487      * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
488      */
getThreshold()489     public int getThreshold() {
490         return mThreshold;
491     }
492 
493     /**
494      * <p>Specifies the minimum number of characters the user has to type in the
495      * edit box before the drop down list is shown.</p>
496      *
497      * <p>When <code>threshold</code> is less than or equals 0, a threshold of
498      * 1 is applied.</p>
499      *
500      * @param threshold the number of characters to type before the drop down
501      *                  is shown
502      *
503      * @see #getThreshold()
504      *
505      * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
506      */
setThreshold(int threshold)507     public void setThreshold(int threshold) {
508         if (threshold <= 0) {
509             threshold = 1;
510         }
511 
512         mThreshold = threshold;
513     }
514 
515     /**
516      * <p>Sets the listener that will be notified when the user clicks an item
517      * in the drop down list.</p>
518      *
519      * @param l the item click listener
520      */
setOnItemClickListener(AdapterView.OnItemClickListener l)521     public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
522         mItemClickListener = l;
523     }
524 
525     /**
526      * <p>Sets the listener that will be notified when the user selects an item
527      * in the drop down list.</p>
528      *
529      * @param l the item selected listener
530      */
setOnItemSelectedListener(AdapterView.OnItemSelectedListener l)531     public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
532         mItemSelectedListener = l;
533     }
534 
535     /**
536      * <p>Returns the listener that is notified whenever the user clicks an item
537      * in the drop down list.</p>
538      *
539      * @return the item click listener
540      *
541      * @deprecated Use {@link #getOnItemClickListener()} intead
542      */
543     @Deprecated
getItemClickListener()544     public AdapterView.OnItemClickListener getItemClickListener() {
545         return mItemClickListener;
546     }
547 
548     /**
549      * <p>Returns the listener that is notified whenever the user selects an
550      * item in the drop down list.</p>
551      *
552      * @return the item selected listener
553      *
554      * @deprecated Use {@link #getOnItemSelectedListener()} intead
555      */
556     @Deprecated
getItemSelectedListener()557     public AdapterView.OnItemSelectedListener getItemSelectedListener() {
558         return mItemSelectedListener;
559     }
560 
561     /**
562      * <p>Returns the listener that is notified whenever the user clicks an item
563      * in the drop down list.</p>
564      *
565      * @return the item click listener
566      */
getOnItemClickListener()567     public AdapterView.OnItemClickListener getOnItemClickListener() {
568         return mItemClickListener;
569     }
570 
571     /**
572      * <p>Returns the listener that is notified whenever the user selects an
573      * item in the drop down list.</p>
574      *
575      * @return the item selected listener
576      */
getOnItemSelectedListener()577     public AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
578         return mItemSelectedListener;
579     }
580 
581     /**
582      * Set a listener that will be invoked whenever the AutoCompleteTextView's
583      * list of completions is dismissed.
584      * @param dismissListener Listener to invoke when completions are dismissed
585      */
setOnDismissListener(final OnDismissListener dismissListener)586     public void setOnDismissListener(final OnDismissListener dismissListener) {
587         PopupWindow.OnDismissListener wrappedListener = null;
588         if (dismissListener != null) {
589             wrappedListener = new PopupWindow.OnDismissListener() {
590                 @Override public void onDismiss() {
591                     dismissListener.onDismiss();
592                 }
593             };
594         }
595         mPopup.setOnDismissListener(wrappedListener);
596     }
597 
598     /**
599      * <p>Returns a filterable list adapter used for auto completion.</p>
600      *
601      * @return a data adapter used for auto completion
602      */
getAdapter()603     public ListAdapter getAdapter() {
604         return mAdapter;
605     }
606 
607     /**
608      * <p>Changes the list of data used for auto completion. The provided list
609      * must be a filterable list adapter.</p>
610      *
611      * <p>The caller is still responsible for managing any resources used by the adapter.
612      * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
613      * A common case is the use of {@link android.widget.CursorAdapter}, which
614      * contains a {@link android.database.Cursor} that must be closed.  This can be done
615      * automatically (see
616      * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
617      * startManagingCursor()}),
618      * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p>
619      *
620      * @param adapter the adapter holding the auto completion data
621      *
622      * @see #getAdapter()
623      * @see android.widget.Filterable
624      * @see android.widget.ListAdapter
625      */
setAdapter(T adapter)626     public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
627         if (mObserver == null) {
628             mObserver = new PopupDataSetObserver();
629         } else if (mAdapter != null) {
630             mAdapter.unregisterDataSetObserver(mObserver);
631         }
632         mAdapter = adapter;
633         if (mAdapter != null) {
634             //noinspection unchecked
635             mFilter = ((Filterable) mAdapter).getFilter();
636             adapter.registerDataSetObserver(mObserver);
637         } else {
638             mFilter = null;
639         }
640 
641         mPopup.setAdapter(mAdapter);
642     }
643 
644     @Override
onKeyPreIme(int keyCode, KeyEvent event)645     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
646         if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
647                 && !mPopup.isDropDownAlwaysVisible()) {
648             // special case for the back key, we do not even try to send it
649             // to the drop down list but instead, consume it immediately
650             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
651                 KeyEvent.DispatcherState state = getKeyDispatcherState();
652                 if (state != null) {
653                     state.startTracking(event, this);
654                 }
655                 return true;
656             } else if (event.getAction() == KeyEvent.ACTION_UP) {
657                 KeyEvent.DispatcherState state = getKeyDispatcherState();
658                 if (state != null) {
659                     state.handleUpEvent(event);
660                 }
661                 if (event.isTracking() && !event.isCanceled()) {
662                     dismissDropDown();
663                     return true;
664                 }
665             }
666         }
667         return super.onKeyPreIme(keyCode, event);
668     }
669 
670     @Override
onKeyUp(int keyCode, KeyEvent event)671     public boolean onKeyUp(int keyCode, KeyEvent event) {
672         boolean consumed = mPopup.onKeyUp(keyCode, event);
673         if (consumed) {
674             switch (keyCode) {
675             // if the list accepts the key events and the key event
676             // was a click, the text view gets the selected item
677             // from the drop down as its content
678             case KeyEvent.KEYCODE_ENTER:
679             case KeyEvent.KEYCODE_DPAD_CENTER:
680             case KeyEvent.KEYCODE_TAB:
681                 if (event.hasNoModifiers()) {
682                     performCompletion();
683                 }
684                 return true;
685             }
686         }
687 
688         if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
689             performCompletion();
690             return true;
691         }
692 
693         return super.onKeyUp(keyCode, event);
694     }
695 
696     @Override
onKeyDown(int keyCode, KeyEvent event)697     public boolean onKeyDown(int keyCode, KeyEvent event) {
698         if (mPopup.onKeyDown(keyCode, event)) {
699             return true;
700         }
701 
702         if (!isPopupShowing()) {
703             switch(keyCode) {
704             case KeyEvent.KEYCODE_DPAD_DOWN:
705                 if (event.hasNoModifiers()) {
706                     performValidation();
707                 }
708             }
709         }
710 
711         if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
712             return true;
713         }
714 
715         mLastKeyCode = keyCode;
716         boolean handled = super.onKeyDown(keyCode, event);
717         mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
718 
719         if (handled && isPopupShowing()) {
720             clearListSelection();
721         }
722 
723         return handled;
724     }
725 
726     /**
727      * Returns <code>true</code> if the amount of text in the field meets
728      * or exceeds the {@link #getThreshold} requirement.  You can override
729      * this to impose a different standard for when filtering will be
730      * triggered.
731      */
enoughToFilter()732     public boolean enoughToFilter() {
733         if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
734                 + " threshold=" + mThreshold);
735         return getText().length() >= mThreshold;
736     }
737 
738     /**
739      * This is used to watch for edits to the text view.  Note that we call
740      * to methods on the auto complete text view class so that we can access
741      * private vars without going through thunks.
742      */
743     private class MyWatcher implements TextWatcher {
afterTextChanged(Editable s)744         public void afterTextChanged(Editable s) {
745             doAfterTextChanged();
746         }
beforeTextChanged(CharSequence s, int start, int count, int after)747         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
748             doBeforeTextChanged();
749         }
onTextChanged(CharSequence s, int start, int before, int count)750         public void onTextChanged(CharSequence s, int start, int before, int count) {
751         }
752     }
753 
doBeforeTextChanged()754     void doBeforeTextChanged() {
755         if (mBlockCompletion) return;
756 
757         // when text is changed, inserted or deleted, we attempt to show
758         // the drop down
759         mOpenBefore = isPopupShowing();
760         if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
761     }
762 
doAfterTextChanged()763     void doAfterTextChanged() {
764         if (mBlockCompletion) return;
765 
766         // if the list was open before the keystroke, but closed afterwards,
767         // then something in the keystroke processing (an input filter perhaps)
768         // called performCompletion() and we shouldn't do any more processing.
769         if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
770                 + " open=" + isPopupShowing());
771         if (mOpenBefore && !isPopupShowing()) {
772             return;
773         }
774 
775         // the drop down is shown only when a minimum number of characters
776         // was typed in the text view
777         if (enoughToFilter()) {
778             if (mFilter != null) {
779                 mPopupCanBeUpdated = true;
780                 performFiltering(getText(), mLastKeyCode);
781             }
782         } else {
783             // drop down is automatically dismissed when enough characters
784             // are deleted from the text view
785             if (!mPopup.isDropDownAlwaysVisible()) {
786                 dismissDropDown();
787             }
788             if (mFilter != null) {
789                 mFilter.filter(null);
790             }
791         }
792     }
793 
794     /**
795      * <p>Indicates whether the popup menu is showing.</p>
796      *
797      * @return true if the popup menu is showing, false otherwise
798      */
isPopupShowing()799     public boolean isPopupShowing() {
800         return mPopup.isShowing();
801     }
802 
803     /**
804      * <p>Converts the selected item from the drop down list into a sequence
805      * of character that can be used in the edit box.</p>
806      *
807      * @param selectedItem the item selected by the user for completion
808      *
809      * @return a sequence of characters representing the selected suggestion
810      */
convertSelectionToString(Object selectedItem)811     protected CharSequence convertSelectionToString(Object selectedItem) {
812         return mFilter.convertResultToString(selectedItem);
813     }
814 
815     /**
816      * <p>Clear the list selection.  This may only be temporary, as user input will often bring
817      * it back.
818      */
clearListSelection()819     public void clearListSelection() {
820         mPopup.clearListSelection();
821     }
822 
823     /**
824      * Set the position of the dropdown view selection.
825      *
826      * @param position The position to move the selector to.
827      */
setListSelection(int position)828     public void setListSelection(int position) {
829         mPopup.setSelection(position);
830     }
831 
832     /**
833      * Get the position of the dropdown view selection, if there is one.  Returns
834      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
835      * there is no selection.
836      *
837      * @return the position of the current selection, if there is one, or
838      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
839      *
840      * @see ListView#getSelectedItemPosition()
841      */
getListSelection()842     public int getListSelection() {
843         return mPopup.getSelectedItemPosition();
844     }
845 
846     /**
847      * <p>Starts filtering the content of the drop down list. The filtering
848      * pattern is the content of the edit box. Subclasses should override this
849      * method to filter with a different pattern, for instance a substring of
850      * <code>text</code>.</p>
851      *
852      * @param text the filtering pattern
853      * @param keyCode the last character inserted in the edit box; beware that
854      * this will be null when text is being added through a soft input method.
855      */
856     @SuppressWarnings({ "UnusedDeclaration" })
performFiltering(CharSequence text, int keyCode)857     protected void performFiltering(CharSequence text, int keyCode) {
858         mFilter.filter(text, this);
859     }
860 
861     /**
862      * <p>Performs the text completion by converting the selected item from
863      * the drop down list into a string, replacing the text box's content with
864      * this string and finally dismissing the drop down menu.</p>
865      */
performCompletion()866     public void performCompletion() {
867         performCompletion(null, -1, -1);
868     }
869 
870     @Override
onCommitCompletion(CompletionInfo completion)871     public void onCommitCompletion(CompletionInfo completion) {
872         if (isPopupShowing()) {
873             mPopup.performItemClick(completion.getPosition());
874         }
875     }
876 
performCompletion(View selectedView, int position, long id)877     private void performCompletion(View selectedView, int position, long id) {
878         if (isPopupShowing()) {
879             Object selectedItem;
880             if (position < 0) {
881                 selectedItem = mPopup.getSelectedItem();
882             } else {
883                 selectedItem = mAdapter.getItem(position);
884             }
885             if (selectedItem == null) {
886                 Log.w(TAG, "performCompletion: no selected item");
887                 return;
888             }
889 
890             mBlockCompletion = true;
891             replaceText(convertSelectionToString(selectedItem));
892             mBlockCompletion = false;
893 
894             if (mItemClickListener != null) {
895                 final ListPopupWindow list = mPopup;
896 
897                 if (selectedView == null || position < 0) {
898                     selectedView = list.getSelectedView();
899                     position = list.getSelectedItemPosition();
900                     id = list.getSelectedItemId();
901                 }
902                 mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
903             }
904         }
905 
906         if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
907             dismissDropDown();
908         }
909     }
910 
911     /**
912      * Identifies whether the view is currently performing a text completion, so subclasses
913      * can decide whether to respond to text changed events.
914      */
isPerformingCompletion()915     public boolean isPerformingCompletion() {
916         return mBlockCompletion;
917     }
918 
919     /**
920      * Like {@link #setText(CharSequence)}, except that it can disable filtering.
921      *
922      * @param filter If <code>false</code>, no filtering will be performed
923      *        as a result of this call.
924      */
setText(CharSequence text, boolean filter)925     public void setText(CharSequence text, boolean filter) {
926         if (filter) {
927             setText(text);
928         } else {
929             mBlockCompletion = true;
930             setText(text);
931             mBlockCompletion = false;
932         }
933     }
934 
935     /**
936      * <p>Performs the text completion by replacing the current text by the
937      * selected item. Subclasses should override this method to avoid replacing
938      * the whole content of the edit box.</p>
939      *
940      * @param text the selected suggestion in the drop down list
941      */
replaceText(CharSequence text)942     protected void replaceText(CharSequence text) {
943         clearComposingText();
944 
945         setText(text);
946         // make sure we keep the caret at the end of the text view
947         Editable spannable = getText();
948         Selection.setSelection(spannable, spannable.length());
949     }
950 
951     /** {@inheritDoc} */
onFilterComplete(int count)952     public void onFilterComplete(int count) {
953         updateDropDownForFilter(count);
954     }
955 
updateDropDownForFilter(int count)956     private void updateDropDownForFilter(int count) {
957         // Not attached to window, don't update drop-down
958         if (getWindowVisibility() == View.GONE) return;
959 
960         /*
961          * This checks enoughToFilter() again because filtering requests
962          * are asynchronous, so the result may come back after enough text
963          * has since been deleted to make it no longer appropriate
964          * to filter.
965          */
966 
967         final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
968         final boolean enoughToFilter = enoughToFilter();
969         if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) {
970             if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) {
971                 showDropDown();
972             }
973         } else if (!dropDownAlwaysVisible && isPopupShowing()) {
974             dismissDropDown();
975             // When the filter text is changed, the first update from the adapter may show an empty
976             // count (when the query is being performed on the network). Future updates when some
977             // content has been retrieved should still be able to update the list.
978             mPopupCanBeUpdated = true;
979         }
980     }
981 
982     @Override
onWindowFocusChanged(boolean hasWindowFocus)983     public void onWindowFocusChanged(boolean hasWindowFocus) {
984         super.onWindowFocusChanged(hasWindowFocus);
985         if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
986             dismissDropDown();
987         }
988     }
989 
990     @Override
onDisplayHint(int hint)991     protected void onDisplayHint(int hint) {
992         super.onDisplayHint(hint);
993         switch (hint) {
994             case INVISIBLE:
995                 if (!mPopup.isDropDownAlwaysVisible()) {
996                     dismissDropDown();
997                 }
998                 break;
999         }
1000     }
1001 
1002     @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)1003     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1004         super.onFocusChanged(focused, direction, previouslyFocusedRect);
1005         // Perform validation if the view is losing focus.
1006         if (!focused) {
1007             performValidation();
1008         }
1009         if (!focused && !mPopup.isDropDownAlwaysVisible()) {
1010             dismissDropDown();
1011         }
1012     }
1013 
1014     @Override
onAttachedToWindow()1015     protected void onAttachedToWindow() {
1016         super.onAttachedToWindow();
1017     }
1018 
1019     @Override
onDetachedFromWindow()1020     protected void onDetachedFromWindow() {
1021         dismissDropDown();
1022         super.onDetachedFromWindow();
1023     }
1024 
1025     /**
1026      * <p>Closes the drop down if present on screen.</p>
1027      */
dismissDropDown()1028     public void dismissDropDown() {
1029         InputMethodManager imm = InputMethodManager.peekInstance();
1030         if (imm != null) {
1031             imm.displayCompletions(this, null);
1032         }
1033         mPopup.dismiss();
1034         mPopupCanBeUpdated = false;
1035     }
1036 
1037     @Override
setFrame(final int l, int t, final int r, int b)1038     protected boolean setFrame(final int l, int t, final int r, int b) {
1039         boolean result = super.setFrame(l, t, r, b);
1040 
1041         if (isPopupShowing()) {
1042             showDropDown();
1043         }
1044 
1045         return result;
1046     }
1047 
1048     /**
1049      * Issues a runnable to show the dropdown as soon as possible.
1050      *
1051      * @hide internal used only by SearchDialog
1052      */
showDropDownAfterLayout()1053     public void showDropDownAfterLayout() {
1054         mPopup.postShow();
1055     }
1056 
1057     /**
1058      * Ensures that the drop down is not obscuring the IME.
1059      * @param visible whether the ime should be in front. If false, the ime is pushed to
1060      * the background.
1061      * @hide internal used only here and SearchDialog
1062      */
ensureImeVisible(boolean visible)1063     public void ensureImeVisible(boolean visible) {
1064         mPopup.setInputMethodMode(visible
1065                 ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
1066         if (mPopup.isDropDownAlwaysVisible() || (mFilter != null && enoughToFilter())) {
1067             showDropDown();
1068         }
1069     }
1070 
1071     /**
1072      * @hide internal used only here and SearchDialog
1073      */
isInputMethodNotNeeded()1074     public boolean isInputMethodNotNeeded() {
1075         return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED;
1076     }
1077 
1078     /**
1079      * <p>Displays the drop down on screen.</p>
1080      */
showDropDown()1081     public void showDropDown() {
1082         buildImeCompletions();
1083 
1084         if (mPopup.getAnchorView() == null) {
1085             if (mDropDownAnchorId != View.NO_ID) {
1086                 mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
1087             } else {
1088                 mPopup.setAnchorView(this);
1089             }
1090         }
1091         if (!isPopupShowing()) {
1092             // Make sure the list does not obscure the IME when shown for the first time.
1093             mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED);
1094             mPopup.setListItemExpandMax(EXPAND_MAX);
1095         }
1096         mPopup.show();
1097         mPopup.getListView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
1098     }
1099 
1100     /**
1101      * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
1102      * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
1103      * ignore outside touch even when the drop down is not set to always visible.
1104      *
1105      * @hide used only by SearchDialog
1106      */
setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch)1107     public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
1108         mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch);
1109     }
1110 
buildImeCompletions()1111     private void buildImeCompletions() {
1112         final ListAdapter adapter = mAdapter;
1113         if (adapter != null) {
1114             InputMethodManager imm = InputMethodManager.peekInstance();
1115             if (imm != null) {
1116                 final int count = Math.min(adapter.getCount(), 20);
1117                 CompletionInfo[] completions = new CompletionInfo[count];
1118                 int realCount = 0;
1119 
1120                 for (int i = 0; i < count; i++) {
1121                     if (adapter.isEnabled(i)) {
1122                         Object item = adapter.getItem(i);
1123                         long id = adapter.getItemId(i);
1124                         completions[realCount] = new CompletionInfo(id, realCount,
1125                                 convertSelectionToString(item));
1126                         realCount++;
1127                     }
1128                 }
1129 
1130                 if (realCount != count) {
1131                     CompletionInfo[] tmp = new CompletionInfo[realCount];
1132                     System.arraycopy(completions, 0, tmp, 0, realCount);
1133                     completions = tmp;
1134                 }
1135 
1136                 imm.displayCompletions(this, completions);
1137             }
1138         }
1139     }
1140 
1141     /**
1142      * Sets the validator used to perform text validation.
1143      *
1144      * @param validator The validator used to validate the text entered in this widget.
1145      *
1146      * @see #getValidator()
1147      * @see #performValidation()
1148      */
setValidator(Validator validator)1149     public void setValidator(Validator validator) {
1150         mValidator = validator;
1151     }
1152 
1153     /**
1154      * Returns the Validator set with {@link #setValidator},
1155      * or <code>null</code> if it was not set.
1156      *
1157      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
1158      * @see #performValidation()
1159      */
getValidator()1160     public Validator getValidator() {
1161         return mValidator;
1162     }
1163 
1164     /**
1165      * If a validator was set on this view and the current string is not valid,
1166      * ask the validator to fix it.
1167      *
1168      * @see #getValidator()
1169      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
1170      */
performValidation()1171     public void performValidation() {
1172         if (mValidator == null) return;
1173 
1174         CharSequence text = getText();
1175 
1176         if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
1177             setText(mValidator.fixText(text));
1178         }
1179     }
1180 
1181     /**
1182      * Returns the Filter obtained from {@link Filterable#getFilter},
1183      * or <code>null</code> if {@link #setAdapter} was not called with
1184      * a Filterable.
1185      */
getFilter()1186     protected Filter getFilter() {
1187         return mFilter;
1188     }
1189 
1190     private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
onItemClick(AdapterView parent, View v, int position, long id)1191         public void onItemClick(AdapterView parent, View v, int position, long id) {
1192             performCompletion(v, position, id);
1193         }
1194     }
1195 
1196     /**
1197      * This interface is used to make sure that the text entered in this TextView complies to
1198      * a certain format.  Since there is no foolproof way to prevent the user from leaving
1199      * this View with an incorrect value in it, all we can do is try to fix it ourselves
1200      * when this happens.
1201      */
1202     public interface Validator {
1203         /**
1204          * Validates the specified text.
1205          *
1206          * @return true If the text currently in the text editor is valid.
1207          *
1208          * @see #fixText(CharSequence)
1209          */
isValid(CharSequence text)1210         boolean isValid(CharSequence text);
1211 
1212         /**
1213          * Corrects the specified text to make it valid.
1214          *
1215          * @param invalidText A string that doesn't pass validation: isValid(invalidText)
1216          *        returns false
1217          *
1218          * @return A string based on invalidText such as invoking isValid() on it returns true.
1219          *
1220          * @see #isValid(CharSequence)
1221          */
fixText(CharSequence invalidText)1222         CharSequence fixText(CharSequence invalidText);
1223     }
1224 
1225     /**
1226      * Listener to respond to the AutoCompleteTextView's completion list being dismissed.
1227      * @see AutoCompleteTextView#setOnDismissListener(OnDismissListener)
1228      */
1229     public interface OnDismissListener {
1230         /**
1231          * This method will be invoked whenever the AutoCompleteTextView's list
1232          * of completion options has been dismissed and is no longer available
1233          * for user interaction.
1234          */
onDismiss()1235         void onDismiss();
1236     }
1237 
1238     /**
1239      * Allows us a private hook into the on click event without preventing users from setting
1240      * their own click listener.
1241      */
1242     private class PassThroughClickListener implements OnClickListener {
1243 
1244         private View.OnClickListener mWrapped;
1245 
1246         /** {@inheritDoc} */
onClick(View v)1247         public void onClick(View v) {
1248             onClickImpl();
1249 
1250             if (mWrapped != null) mWrapped.onClick(v);
1251         }
1252     }
1253 
1254     private class PopupDataSetObserver extends DataSetObserver {
1255         @Override
onChanged()1256         public void onChanged() {
1257             if (mAdapter != null) {
1258                 // If the popup is not showing already, showing it will cause
1259                 // the list of data set observers attached to the adapter to
1260                 // change. We can't do it from here, because we are in the middle
1261                 // of iterating through the list of observers.
1262                 post(new Runnable() {
1263                     public void run() {
1264                         final ListAdapter adapter = mAdapter;
1265                         if (adapter != null) {
1266                             // This will re-layout, thus resetting mDataChanged, so that the
1267                             // listView click listener stays responsive
1268                             updateDropDownForFilter(adapter.getCount());
1269                         }
1270                     }
1271                 });
1272             }
1273         }
1274     }
1275 }
1276