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