[go: nahoru, domu]

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