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