• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.widget;
18 
19 import android.content.Context;
20 import android.content.res.TypedArray;
21 import android.database.DataSetObserver;
22 import android.graphics.Rect;
23 import android.graphics.drawable.Drawable;
24 import android.text.Editable;
25 import android.text.Selection;
26 import android.text.TextUtils;
27 import android.text.TextWatcher;
28 import android.util.AttributeSet;
29 import android.util.Log;
30 import android.view.KeyEvent;
31 import android.view.LayoutInflater;
32 import android.view.MotionEvent;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.WindowManager;
36 import android.view.inputmethod.CompletionInfo;
37 import android.view.inputmethod.InputMethodManager;
38 import android.view.inputmethod.EditorInfo;
39 
40 import com.android.internal.R;
41 
42 
43 /**
44  * <p>An editable text view that shows completion suggestions automatically
45  * while the user is typing. The list of suggestions is displayed in a drop
46  * down menu from which the user can choose an item to replace the content
47  * of the edit box with.</p>
48  *
49  * <p>The drop down can be dismissed at any time by pressing the back key or,
50  * if no item is selected in the drop down, by pressing the enter/dpad center
51  * key.</p>
52  *
53  * <p>The list of suggestions is obtained from a data adapter and appears
54  * only after a given number of characters defined by
55  * {@link #getThreshold() the threshold}.</p>
56  *
57  * <p>The following code snippet shows how to create a text view which suggests
58  * various countries names while the user is typing:</p>
59  *
60  * <pre class="prettyprint">
61  * public class CountriesActivity extends Activity {
62  *     protected void onCreate(Bundle icicle) {
63  *         super.onCreate(icicle);
64  *         setContentView(R.layout.countries);
65  *
66  *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
67  *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
68  *         AutoCompleteTextView textView = (AutoCompleteTextView)
69  *                 findViewById(R.id.countries_list);
70  *         textView.setAdapter(adapter);
71  *     }
72  *
73  *     private static final String[] COUNTRIES = new String[] {
74  *         "Belgium", "France", "Italy", "Germany", "Spain"
75  *     };
76  * }
77  * </pre>
78  *
79  * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-autocomplete.html">Auto Complete
80  * tutorial</a>.</p>
81  *
82  * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
83  * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
84  * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
85  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
86  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
87  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
88  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
89  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownVerticalOffset
90  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset
91  */
92 public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
93     static final boolean DEBUG = false;
94     static final String TAG = "AutoCompleteTextView";
95 
96     private static final int HINT_VIEW_ID = 0x17;
97 
98     /**
99      * This value controls the length of time that the user
100      * must leave a pointer down without scrolling to expand
101      * the autocomplete dropdown list to cover the IME.
102      */
103     private static final int EXPAND_LIST_TIMEOUT = 250;
104 
105     private CharSequence mHintText;
106     private int mHintResource;
107 
108     private ListAdapter mAdapter;
109     private Filter mFilter;
110     private int mThreshold;
111 
112     private PopupWindow mPopup;
113     private DropDownListView mDropDownList;
114     private int mDropDownVerticalOffset;
115     private int mDropDownHorizontalOffset;
116     private int mDropDownAnchorId;
117     private View mDropDownAnchorView;  // view is retrieved lazily from id once needed
118     private int mDropDownWidth;
119     private int mDropDownHeight;
120     private final Rect mTempRect = new Rect();
121 
122     private Drawable mDropDownListHighlight;
123 
124     private AdapterView.OnItemClickListener mItemClickListener;
125     private AdapterView.OnItemSelectedListener mItemSelectedListener;
126 
127     private final DropDownItemClickListener mDropDownItemClickListener =
128             new DropDownItemClickListener();
129 
130     private boolean mDropDownAlwaysVisible = false;
131 
132     private boolean mDropDownDismissedOnCompletion = true;
133 
134     private boolean mForceIgnoreOutsideTouch = false;
135 
136     private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
137     private boolean mOpenBefore;
138 
139     private Validator mValidator = null;
140 
141     private boolean mBlockCompletion;
142 
143     private ListSelectorHider mHideSelector;
144     private Runnable mShowDropDownRunnable;
145     private Runnable mResizePopupRunnable = new ResizePopupRunnable();
146 
147     private PassThroughClickListener mPassThroughClickListener;
148     private PopupDataSetObserver mObserver;
149 
AutoCompleteTextView(Context context)150     public AutoCompleteTextView(Context context) {
151         this(context, null);
152     }
153 
AutoCompleteTextView(Context context, AttributeSet attrs)154     public AutoCompleteTextView(Context context, AttributeSet attrs) {
155         this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
156     }
157 
AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle)158     public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
159         super(context, attrs, defStyle);
160 
161         mPopup = new PopupWindow(context, attrs,
162                 com.android.internal.R.attr.autoCompleteTextViewStyle);
163         mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
164 
165         TypedArray a =
166             context.obtainStyledAttributes(
167                 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0);
168 
169         mThreshold = a.getInt(
170                 R.styleable.AutoCompleteTextView_completionThreshold, 2);
171 
172         mHintText = a.getText(R.styleable.AutoCompleteTextView_completionHint);
173 
174         mDropDownListHighlight = a.getDrawable(
175                 R.styleable.AutoCompleteTextView_dropDownSelector);
176         mDropDownVerticalOffset = (int)
177                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f);
178         mDropDownHorizontalOffset = (int)
179                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f);
180 
181         // Get the anchor's id now, but the view won't be ready, so wait to actually get the
182         // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
183         // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return
184         // this TextView, as a default anchoring point.
185         mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor,
186                 View.NO_ID);
187 
188         // For dropdown width, the developer can specify a specific width, or MATCH_PARENT
189         // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
190         mDropDownWidth = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownWidth,
191                 ViewGroup.LayoutParams.WRAP_CONTENT);
192         mDropDownHeight = a.getLayoutDimension(R.styleable.AutoCompleteTextView_dropDownHeight,
193                 ViewGroup.LayoutParams.WRAP_CONTENT);
194 
195         mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
196                 R.layout.simple_dropdown_hint);
197 
198         // Always turn on the auto complete input type flag, since it
199         // makes no sense to use this widget without it.
200         int inputType = getInputType();
201         if ((inputType&EditorInfo.TYPE_MASK_CLASS)
202                 == EditorInfo.TYPE_CLASS_TEXT) {
203             inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
204             setRawInputType(inputType);
205         }
206 
207         a.recycle();
208 
209         setFocusable(true);
210 
211         addTextChangedListener(new MyWatcher());
212 
213         mPassThroughClickListener = new PassThroughClickListener();
214         super.setOnClickListener(mPassThroughClickListener);
215     }
216 
217     @Override
setOnClickListener(OnClickListener listener)218     public void setOnClickListener(OnClickListener listener) {
219         mPassThroughClickListener.mWrapped = listener;
220     }
221 
222     /**
223      * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
224      */
onClickImpl()225     private void onClickImpl() {
226         // If the dropdown is showing, bring the keyboard to the front
227         // when the user touches the text field.
228         if (mPopup.isShowing()) {
229             ensureImeVisible(true);
230         }
231     }
232 
233     /**
234      * <p>Sets the optional hint text that is displayed at the bottom of the
235      * the matching list.  This can be used as a cue to the user on how to
236      * best use the list, or to provide extra information.</p>
237      *
238      * @param hint the text to be displayed to the user
239      *
240      * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
241      */
setCompletionHint(CharSequence hint)242     public void setCompletionHint(CharSequence hint) {
243         mHintText = hint;
244     }
245 
246     /**
247      * <p>Returns the current width for the auto-complete drop down list. This can
248      * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
249      * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
250      *
251      * @return the width for the drop down list
252      *
253      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
254      */
getDropDownWidth()255     public int getDropDownWidth() {
256         return mDropDownWidth;
257     }
258 
259     /**
260      * <p>Sets the current width for the auto-complete drop down list. This can
261      * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
262      * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
263      *
264      * @param width the width to use
265      *
266      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
267      */
setDropDownWidth(int width)268     public void setDropDownWidth(int width) {
269         mDropDownWidth = width;
270     }
271 
272     /**
273      * <p>Returns the current height for the auto-complete drop down list. This can
274      * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
275      * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
276      * of the drop down's content.</p>
277      *
278      * @return the height for the drop down list
279      *
280      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
281      */
getDropDownHeight()282     public int getDropDownHeight() {
283         return mDropDownHeight;
284     }
285 
286     /**
287      * <p>Sets the current height for the auto-complete drop down list. This can
288      * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
289      * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
290      * of the drop down's content.</p>
291      *
292      * @param height the height to use
293      *
294      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
295      */
setDropDownHeight(int height)296     public void setDropDownHeight(int height) {
297         mDropDownHeight = height;
298     }
299 
300     /**
301      * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
302      *
303      * @return the view's id, or {@link View#NO_ID} if none specified
304      *
305      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
306      */
getDropDownAnchor()307     public int getDropDownAnchor() {
308         return mDropDownAnchorId;
309     }
310 
311     /**
312      * <p>Sets the view to which the auto-complete drop down list should anchor. The view
313      * corresponding to this id will not be loaded until the next time it is needed to avoid
314      * loading a view which is not yet instantiated.</p>
315      *
316      * @param id the id to anchor the drop down list view to
317      *
318      * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
319      */
setDropDownAnchor(int id)320     public void setDropDownAnchor(int id) {
321         mDropDownAnchorId = id;
322         mDropDownAnchorView = null;
323     }
324 
325     /**
326      * <p>Gets the background of the auto-complete drop-down list.</p>
327      *
328      * @return the background drawable
329      *
330      * @attr ref android.R.styleable#PopupWindow_popupBackground
331      */
getDropDownBackground()332     public Drawable getDropDownBackground() {
333         return mPopup.getBackground();
334     }
335 
336     /**
337      * <p>Sets the background of the auto-complete drop-down list.</p>
338      *
339      * @param d the drawable to set as the background
340      *
341      * @attr ref android.R.styleable#PopupWindow_popupBackground
342      */
setDropDownBackgroundDrawable(Drawable d)343     public void setDropDownBackgroundDrawable(Drawable d) {
344         mPopup.setBackgroundDrawable(d);
345     }
346 
347     /**
348      * <p>Sets the background of the auto-complete drop-down list.</p>
349      *
350      * @param id the id of the drawable to set as the background
351      *
352      * @attr ref android.R.styleable#PopupWindow_popupBackground
353      */
setDropDownBackgroundResource(int id)354     public void setDropDownBackgroundResource(int id) {
355         mPopup.setBackgroundDrawable(getResources().getDrawable(id));
356     }
357 
358     /**
359      * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
360      *
361      * @param offset the vertical offset
362      */
setDropDownVerticalOffset(int offset)363     public void setDropDownVerticalOffset(int offset) {
364         mDropDownVerticalOffset = offset;
365     }
366 
367     /**
368      * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
369      *
370      * @return the vertical offset
371      */
getDropDownVerticalOffset()372     public int getDropDownVerticalOffset() {
373         return mDropDownVerticalOffset;
374     }
375 
376     /**
377      * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
378      *
379      * @param offset the horizontal offset
380      */
setDropDownHorizontalOffset(int offset)381     public void setDropDownHorizontalOffset(int offset) {
382         mDropDownHorizontalOffset = offset;
383     }
384 
385     /**
386      * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
387      *
388      * @return the horizontal offset
389      */
getDropDownHorizontalOffset()390     public int getDropDownHorizontalOffset() {
391         return mDropDownHorizontalOffset;
392     }
393 
394      /**
395      * <p>Sets the animation style of the auto-complete drop-down list.</p>
396      *
397      * <p>If the drop-down is showing, calling this method will take effect only
398      * the next time the drop-down is shown.</p>
399      *
400      * @param animationStyle animation style to use when the drop-down appears
401      *      and disappears.  Set to -1 for the default animation, 0 for no
402      *      animation, or a resource identifier for an explicit animation.
403      *
404      * @hide Pending API council approval
405      */
setDropDownAnimationStyle(int animationStyle)406     public void setDropDownAnimationStyle(int animationStyle) {
407         mPopup.setAnimationStyle(animationStyle);
408     }
409 
410     /**
411      * <p>Returns the animation style that is used when the drop-down list appears and disappears
412      * </p>
413      *
414      * @return the animation style that is used when the drop-down list appears and disappears
415      *
416      * @hide Pending API council approval
417      */
getDropDownAnimationStyle()418     public int getDropDownAnimationStyle() {
419         return mPopup.getAnimationStyle();
420     }
421 
422     /**
423      * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
424      *
425      * @hide Pending API council approval
426      */
isDropDownAlwaysVisible()427     public boolean isDropDownAlwaysVisible() {
428         return mDropDownAlwaysVisible;
429     }
430 
431     /**
432      * Sets whether the drop-down should remain visible as long as there is there is
433      * {@link #enoughToFilter()}.  This is useful if an unknown number of results are expected
434      * to show up in the adapter sometime in the future.
435      *
436      * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
437      * of the size or content of the list.  {@link #getDropDownBackground()} will fill any space
438      * that is not used by the list.
439      *
440      * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
441      *
442      * @hide Pending API council approval
443      */
setDropDownAlwaysVisible(boolean dropDownAlwaysVisible)444     public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
445         mDropDownAlwaysVisible = dropDownAlwaysVisible;
446     }
447 
448     /**
449      * Checks whether the drop-down is dismissed when a suggestion is clicked.
450      *
451      * @hide Pending API council approval
452      */
isDropDownDismissedOnCompletion()453     public boolean isDropDownDismissedOnCompletion() {
454         return mDropDownDismissedOnCompletion;
455     }
456 
457     /**
458      * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
459      * true by default.
460      *
461      * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
462      *
463      * @hide Pending API council approval
464      */
setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion)465     public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
466         mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
467     }
468 
469     /**
470      * <p>Returns the number of characters the user must type before the drop
471      * down list is shown.</p>
472      *
473      * @return the minimum number of characters to type to show the drop down
474      *
475      * @see #setThreshold(int)
476      */
getThreshold()477     public int getThreshold() {
478         return mThreshold;
479     }
480 
481     /**
482      * <p>Specifies the minimum number of characters the user has to type in the
483      * edit box before the drop down list is shown.</p>
484      *
485      * <p>When <code>threshold</code> is less than or equals 0, a threshold of
486      * 1 is applied.</p>
487      *
488      * @param threshold the number of characters to type before the drop down
489      *                  is shown
490      *
491      * @see #getThreshold()
492      *
493      * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
494      */
setThreshold(int threshold)495     public void setThreshold(int threshold) {
496         if (threshold <= 0) {
497             threshold = 1;
498         }
499 
500         mThreshold = threshold;
501     }
502 
503     /**
504      * <p>Sets the listener that will be notified when the user clicks an item
505      * in the drop down list.</p>
506      *
507      * @param l the item click listener
508      */
setOnItemClickListener(AdapterView.OnItemClickListener l)509     public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
510         mItemClickListener = l;
511     }
512 
513     /**
514      * <p>Sets the listener that will be notified when the user selects an item
515      * in the drop down list.</p>
516      *
517      * @param l the item selected listener
518      */
setOnItemSelectedListener(AdapterView.OnItemSelectedListener l)519     public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
520         mItemSelectedListener = l;
521     }
522 
523     /**
524      * <p>Returns the listener that is notified whenever the user clicks an item
525      * in the drop down list.</p>
526      *
527      * @return the item click listener
528      *
529      * @deprecated Use {@link #getOnItemClickListener()} intead
530      */
531     @Deprecated
getItemClickListener()532     public AdapterView.OnItemClickListener getItemClickListener() {
533         return mItemClickListener;
534     }
535 
536     /**
537      * <p>Returns the listener that is notified whenever the user selects an
538      * item in the drop down list.</p>
539      *
540      * @return the item selected listener
541      *
542      * @deprecated Use {@link #getOnItemSelectedListener()} intead
543      */
544     @Deprecated
getItemSelectedListener()545     public AdapterView.OnItemSelectedListener getItemSelectedListener() {
546         return mItemSelectedListener;
547     }
548 
549     /**
550      * <p>Returns the listener that is notified whenever the user clicks an item
551      * in the drop down list.</p>
552      *
553      * @return the item click listener
554      */
getOnItemClickListener()555     public AdapterView.OnItemClickListener getOnItemClickListener() {
556         return mItemClickListener;
557     }
558 
559     /**
560      * <p>Returns the listener that is notified whenever the user selects an
561      * item in the drop down list.</p>
562      *
563      * @return the item selected listener
564      */
getOnItemSelectedListener()565     public AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
566         return mItemSelectedListener;
567     }
568 
569     /**
570      * <p>Returns a filterable list adapter used for auto completion.</p>
571      *
572      * @return a data adapter used for auto completion
573      */
getAdapter()574     public ListAdapter getAdapter() {
575         return mAdapter;
576     }
577 
578     /**
579      * <p>Changes the list of data used for auto completion. The provided list
580      * must be a filterable list adapter.</p>
581      *
582      * <p>The caller is still responsible for managing any resources used by the adapter.
583      * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
584      * A common case is the use of {@link android.widget.CursorAdapter}, which
585      * contains a {@link android.database.Cursor} that must be closed.  This can be done
586      * automatically (see
587      * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
588      * startManagingCursor()}),
589      * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p>
590      *
591      * @param adapter the adapter holding the auto completion data
592      *
593      * @see #getAdapter()
594      * @see android.widget.Filterable
595      * @see android.widget.ListAdapter
596      */
setAdapter(T adapter)597     public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
598         if (mObserver == null) {
599             mObserver = new PopupDataSetObserver();
600         } else if (mAdapter != null) {
601             mAdapter.unregisterDataSetObserver(mObserver);
602         }
603         mAdapter = adapter;
604         if (mAdapter != null) {
605             //noinspection unchecked
606             mFilter = ((Filterable) mAdapter).getFilter();
607             adapter.registerDataSetObserver(mObserver);
608         } else {
609             mFilter = null;
610         }
611 
612         if (mDropDownList != null) {
613             mDropDownList.setAdapter(mAdapter);
614         }
615     }
616 
617     @Override
onKeyPreIme(int keyCode, KeyEvent event)618     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
619         if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
620                 && !mDropDownAlwaysVisible) {
621             // special case for the back key, we do not even try to send it
622             // to the drop down list but instead, consume it immediately
623             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
624                 getKeyDispatcherState().startTracking(event, this);
625                 return true;
626             } else if (event.getAction() == KeyEvent.ACTION_UP) {
627                 getKeyDispatcherState().handleUpEvent(event);
628                 if (event.isTracking() && !event.isCanceled()) {
629                     dismissDropDown();
630                     return true;
631                 }
632             }
633         }
634         return super.onKeyPreIme(keyCode, event);
635     }
636 
637     @Override
onKeyUp(int keyCode, KeyEvent event)638     public boolean onKeyUp(int keyCode, KeyEvent event) {
639         if (isPopupShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
640             boolean consumed = mDropDownList.onKeyUp(keyCode, event);
641             if (consumed) {
642                 switch (keyCode) {
643                     // if the list accepts the key events and the key event
644                     // was a click, the text view gets the selected item
645                     // from the drop down as its content
646                     case KeyEvent.KEYCODE_ENTER:
647                     case KeyEvent.KEYCODE_DPAD_CENTER:
648                         performCompletion();
649                         return true;
650                 }
651             }
652         }
653         return super.onKeyUp(keyCode, event);
654     }
655 
656     @Override
onKeyDown(int keyCode, KeyEvent event)657     public boolean onKeyDown(int keyCode, KeyEvent event) {
658         // when the drop down is shown, we drive it directly
659         if (isPopupShowing()) {
660             // the key events are forwarded to the list in the drop down view
661             // note that ListView handles space but we don't want that to happen
662             // also if selection is not currently in the drop down, then don't
663             // let center or enter presses go there since that would cause it
664             // to select one of its items
665             if (keyCode != KeyEvent.KEYCODE_SPACE
666                     && (mDropDownList.getSelectedItemPosition() >= 0
667                             || (keyCode != KeyEvent.KEYCODE_ENTER
668                                     && keyCode != KeyEvent.KEYCODE_DPAD_CENTER))) {
669                 int curIndex = mDropDownList.getSelectedItemPosition();
670                 boolean consumed;
671 
672                 final boolean below = !mPopup.isAboveAnchor();
673 
674                 final ListAdapter adapter = mAdapter;
675 
676                 boolean allEnabled;
677                 int firstItem = Integer.MAX_VALUE;
678                 int lastItem = Integer.MIN_VALUE;
679 
680                 if (adapter != null) {
681                     allEnabled = adapter.areAllItemsEnabled();
682                     firstItem = allEnabled ? 0 :
683                             mDropDownList.lookForSelectablePosition(0, true);
684                     lastItem = allEnabled ? adapter.getCount() - 1 :
685                             mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
686                 }
687 
688                 if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
689                         (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
690                     // When the selection is at the top, we block the key
691                     // event to prevent focus from moving.
692                     clearListSelection();
693                     mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
694                     showDropDown();
695                     return true;
696                 } else {
697                     // WARNING: Please read the comment where mListSelectionHidden
698                     //          is declared
699                     mDropDownList.mListSelectionHidden = false;
700                 }
701 
702                 consumed = mDropDownList.onKeyDown(keyCode, event);
703                 if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
704 
705                 if (consumed) {
706                     // If it handled the key event, then the user is
707                     // navigating in the list, so we should put it in front.
708                     mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
709                     // Here's a little trick we need to do to make sure that
710                     // the list view is actually showing its focus indicator,
711                     // by ensuring it has focus and getting its window out
712                     // of touch mode.
713                     mDropDownList.requestFocusFromTouch();
714                     showDropDown();
715 
716                     switch (keyCode) {
717                         // avoid passing the focus from the text view to the
718                         // next component
719                         case KeyEvent.KEYCODE_ENTER:
720                         case KeyEvent.KEYCODE_DPAD_CENTER:
721                         case KeyEvent.KEYCODE_DPAD_DOWN:
722                         case KeyEvent.KEYCODE_DPAD_UP:
723                             return true;
724                     }
725                 } else {
726                     if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
727                         // when the selection is at the bottom, we block the
728                         // event to avoid going to the next focusable widget
729                         if (curIndex == lastItem) {
730                             return true;
731                         }
732                     } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
733                             curIndex == firstItem) {
734                         return true;
735                     }
736                 }
737             }
738         } else {
739             switch(keyCode) {
740             case KeyEvent.KEYCODE_DPAD_DOWN:
741                 performValidation();
742             }
743         }
744 
745         mLastKeyCode = keyCode;
746         boolean handled = super.onKeyDown(keyCode, event);
747         mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
748 
749         if (handled && isPopupShowing() && mDropDownList != null) {
750             clearListSelection();
751         }
752 
753         return handled;
754     }
755 
756     /**
757      * Returns <code>true</code> if the amount of text in the field meets
758      * or exceeds the {@link #getThreshold} requirement.  You can override
759      * this to impose a different standard for when filtering will be
760      * triggered.
761      */
enoughToFilter()762     public boolean enoughToFilter() {
763         if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
764                 + " threshold=" + mThreshold);
765         return getText().length() >= mThreshold;
766     }
767 
768     /**
769      * This is used to watch for edits to the text view.  Note that we call
770      * to methods on the auto complete text view class so that we can access
771      * private vars without going through thunks.
772      */
773     private class MyWatcher implements TextWatcher {
afterTextChanged(Editable s)774         public void afterTextChanged(Editable s) {
775             doAfterTextChanged();
776         }
beforeTextChanged(CharSequence s, int start, int count, int after)777         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
778             doBeforeTextChanged();
779         }
onTextChanged(CharSequence s, int start, int before, int count)780         public void onTextChanged(CharSequence s, int start, int before, int count) {
781         }
782     }
783 
doBeforeTextChanged()784     void doBeforeTextChanged() {
785         if (mBlockCompletion) return;
786 
787         // when text is changed, inserted or deleted, we attempt to show
788         // the drop down
789         mOpenBefore = isPopupShowing();
790         if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
791     }
792 
doAfterTextChanged()793     void doAfterTextChanged() {
794         if (mBlockCompletion) return;
795 
796         // if the list was open before the keystroke, but closed afterwards,
797         // then something in the keystroke processing (an input filter perhaps)
798         // called performCompletion() and we shouldn't do any more processing.
799         if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
800                 + " open=" + isPopupShowing());
801         if (mOpenBefore && !isPopupShowing()) {
802             return;
803         }
804 
805         // the drop down is shown only when a minimum number of characters
806         // was typed in the text view
807         if (enoughToFilter()) {
808             if (mFilter != null) {
809                 performFiltering(getText(), mLastKeyCode);
810             }
811         } else {
812             // drop down is automatically dismissed when enough characters
813             // are deleted from the text view
814             if (!mDropDownAlwaysVisible) dismissDropDown();
815             if (mFilter != null) {
816                 mFilter.filter(null);
817             }
818         }
819     }
820 
821     /**
822      * <p>Indicates whether the popup menu is showing.</p>
823      *
824      * @return true if the popup menu is showing, false otherwise
825      */
isPopupShowing()826     public boolean isPopupShowing() {
827         return mPopup.isShowing();
828     }
829 
830     /**
831      * <p>Converts the selected item from the drop down list into a sequence
832      * of character that can be used in the edit box.</p>
833      *
834      * @param selectedItem the item selected by the user for completion
835      *
836      * @return a sequence of characters representing the selected suggestion
837      */
convertSelectionToString(Object selectedItem)838     protected CharSequence convertSelectionToString(Object selectedItem) {
839         return mFilter.convertResultToString(selectedItem);
840     }
841 
842     /**
843      * <p>Clear the list selection.  This may only be temporary, as user input will often bring
844      * it back.
845      */
clearListSelection()846     public void clearListSelection() {
847         final DropDownListView list = mDropDownList;
848         if (list != null) {
849             // WARNING: Please read the comment where mListSelectionHidden is declared
850             list.mListSelectionHidden = true;
851             list.hideSelector();
852             list.requestLayout();
853         }
854     }
855 
856     /**
857      * Set the position of the dropdown view selection.
858      *
859      * @param position The position to move the selector to.
860      */
setListSelection(int position)861     public void setListSelection(int position) {
862         if (mPopup.isShowing() && (mDropDownList != null)) {
863             mDropDownList.mListSelectionHidden = false;
864             mDropDownList.setSelection(position);
865             // ListView.setSelection() will call requestLayout()
866         }
867     }
868 
869     /**
870      * Get the position of the dropdown view selection, if there is one.  Returns
871      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
872      * there is no selection.
873      *
874      * @return the position of the current selection, if there is one, or
875      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
876      *
877      * @see ListView#getSelectedItemPosition()
878      */
getListSelection()879     public int getListSelection() {
880         if (mPopup.isShowing() && (mDropDownList != null)) {
881             return mDropDownList.getSelectedItemPosition();
882         }
883         return ListView.INVALID_POSITION;
884     }
885 
886     /**
887      * <p>Starts filtering the content of the drop down list. The filtering
888      * pattern is the content of the edit box. Subclasses should override this
889      * method to filter with a different pattern, for instance a substring of
890      * <code>text</code>.</p>
891      *
892      * @param text the filtering pattern
893      * @param keyCode the last character inserted in the edit box; beware that
894      * this will be null when text is being added through a soft input method.
895      */
896     @SuppressWarnings({ "UnusedDeclaration" })
performFiltering(CharSequence text, int keyCode)897     protected void performFiltering(CharSequence text, int keyCode) {
898         mFilter.filter(text, this);
899     }
900 
901     /**
902      * <p>Performs the text completion by converting the selected item from
903      * the drop down list into a string, replacing the text box's content with
904      * this string and finally dismissing the drop down menu.</p>
905      */
performCompletion()906     public void performCompletion() {
907         performCompletion(null, -1, -1);
908     }
909 
910     @Override
onCommitCompletion(CompletionInfo completion)911     public void onCommitCompletion(CompletionInfo completion) {
912         if (isPopupShowing()) {
913             mBlockCompletion = true;
914             replaceText(completion.getText());
915             mBlockCompletion = false;
916 
917             if (mItemClickListener != null) {
918                 final DropDownListView list = mDropDownList;
919                 // Note that we don't have a View here, so we will need to
920                 // supply null.  Hopefully no existing apps crash...
921                 mItemClickListener.onItemClick(list, null, completion.getPosition(),
922                         completion.getId());
923             }
924         }
925     }
926 
performCompletion(View selectedView, int position, long id)927     private void performCompletion(View selectedView, int position, long id) {
928         if (isPopupShowing()) {
929             Object selectedItem;
930             if (position < 0) {
931                 selectedItem = mDropDownList.getSelectedItem();
932             } else {
933                 selectedItem = mAdapter.getItem(position);
934             }
935             if (selectedItem == null) {
936                 Log.w(TAG, "performCompletion: no selected item");
937                 return;
938             }
939 
940             mBlockCompletion = true;
941             replaceText(convertSelectionToString(selectedItem));
942             mBlockCompletion = false;
943 
944             if (mItemClickListener != null) {
945                 final DropDownListView list = mDropDownList;
946 
947                 if (selectedView == null || position < 0) {
948                     selectedView = list.getSelectedView();
949                     position = list.getSelectedItemPosition();
950                     id = list.getSelectedItemId();
951                 }
952                 mItemClickListener.onItemClick(list, selectedView, position, id);
953             }
954         }
955 
956         if (mDropDownDismissedOnCompletion && !mDropDownAlwaysVisible) {
957             dismissDropDown();
958         }
959     }
960 
961     /**
962      * Identifies whether the view is currently performing a text completion, so subclasses
963      * can decide whether to respond to text changed events.
964      */
isPerformingCompletion()965     public boolean isPerformingCompletion() {
966         return mBlockCompletion;
967     }
968 
969     /**
970      * Like {@link #setText(CharSequence)}, except that it can disable filtering.
971      *
972      * @param filter If <code>false</code>, no filtering will be performed
973      *        as a result of this call.
974      *
975      * @hide Pending API council approval.
976      */
setText(CharSequence text, boolean filter)977     public void setText(CharSequence text, boolean filter) {
978         if (filter) {
979             setText(text);
980         } else {
981             mBlockCompletion = true;
982             setText(text);
983             mBlockCompletion = false;
984         }
985     }
986 
987     /**
988      * <p>Performs the text completion by replacing the current text by the
989      * selected item. Subclasses should override this method to avoid replacing
990      * the whole content of the edit box.</p>
991      *
992      * @param text the selected suggestion in the drop down list
993      */
replaceText(CharSequence text)994     protected void replaceText(CharSequence text) {
995         clearComposingText();
996 
997         setText(text);
998         // make sure we keep the caret at the end of the text view
999         Editable spannable = getText();
1000         Selection.setSelection(spannable, spannable.length());
1001     }
1002 
1003     /** {@inheritDoc} */
onFilterComplete(int count)1004     public void onFilterComplete(int count) {
1005         updateDropDownForFilter(count);
1006 
1007     }
1008 
updateDropDownForFilter(int count)1009     private void updateDropDownForFilter(int count) {
1010         // Not attached to window, don't update drop-down
1011         if (getWindowVisibility() == View.GONE) return;
1012 
1013         /*
1014          * This checks enoughToFilter() again because filtering requests
1015          * are asynchronous, so the result may come back after enough text
1016          * has since been deleted to make it no longer appropriate
1017          * to filter.
1018          */
1019 
1020         if ((count > 0 || mDropDownAlwaysVisible) && enoughToFilter()) {
1021             if (hasFocus() && hasWindowFocus()) {
1022                 showDropDown();
1023             }
1024         } else if (!mDropDownAlwaysVisible) {
1025             dismissDropDown();
1026         }
1027     }
1028 
1029     @Override
onWindowFocusChanged(boolean hasWindowFocus)1030     public void onWindowFocusChanged(boolean hasWindowFocus) {
1031         super.onWindowFocusChanged(hasWindowFocus);
1032         if (!hasWindowFocus && !mDropDownAlwaysVisible) {
1033             dismissDropDown();
1034         }
1035     }
1036 
1037     @Override
onDisplayHint(int hint)1038     protected void onDisplayHint(int hint) {
1039         super.onDisplayHint(hint);
1040         switch (hint) {
1041             case INVISIBLE:
1042                 if (!mDropDownAlwaysVisible) {
1043                     dismissDropDown();
1044                 }
1045                 break;
1046         }
1047     }
1048 
1049     @Override
onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)1050     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
1051         super.onFocusChanged(focused, direction, previouslyFocusedRect);
1052         // Perform validation if the view is losing focus.
1053         if (!focused) {
1054             performValidation();
1055         }
1056         if (!focused && !mDropDownAlwaysVisible) {
1057             dismissDropDown();
1058         }
1059     }
1060 
1061     @Override
onAttachedToWindow()1062     protected void onAttachedToWindow() {
1063         super.onAttachedToWindow();
1064     }
1065 
1066     @Override
onDetachedFromWindow()1067     protected void onDetachedFromWindow() {
1068         dismissDropDown();
1069         super.onDetachedFromWindow();
1070     }
1071 
1072     /**
1073      * <p>Closes the drop down if present on screen.</p>
1074      */
dismissDropDown()1075     public void dismissDropDown() {
1076         InputMethodManager imm = InputMethodManager.peekInstance();
1077         if (imm != null) {
1078             imm.displayCompletions(this, null);
1079         }
1080         mPopup.dismiss();
1081         mPopup.setContentView(null);
1082         mDropDownList = null;
1083     }
1084 
1085     @Override
setFrame(final int l, int t, final int r, int b)1086     protected boolean setFrame(final int l, int t, final int r, int b) {
1087         boolean result = super.setFrame(l, t, r, b);
1088 
1089         if (mPopup.isShowing()) {
1090             showDropDown();
1091         }
1092 
1093         return result;
1094     }
1095 
1096     /**
1097      * <p>Used for lazy instantiation of the anchor view from the id we have. If the value of
1098      * the id is NO_ID or we can't find a view for the given id, we return this TextView as
1099      * the default anchoring point.</p>
1100      */
getDropDownAnchorView()1101     private View getDropDownAnchorView() {
1102         if (mDropDownAnchorView == null && mDropDownAnchorId != View.NO_ID) {
1103             mDropDownAnchorView = getRootView().findViewById(mDropDownAnchorId);
1104         }
1105         return mDropDownAnchorView == null ? this : mDropDownAnchorView;
1106     }
1107 
1108     /**
1109      * Issues a runnable to show the dropdown as soon as possible.
1110      *
1111      * @hide internal used only by SearchDialog
1112      */
showDropDownAfterLayout()1113     public void showDropDownAfterLayout() {
1114         post(mShowDropDownRunnable);
1115     }
1116 
1117     /**
1118      * Ensures that the drop down is not obscuring the IME.
1119      * @param visible whether the ime should be in front. If false, the ime is pushed to
1120      * the background.
1121      * @hide internal used only here and SearchDialog
1122      */
ensureImeVisible(boolean visible)1123     public void ensureImeVisible(boolean visible) {
1124         mPopup.setInputMethodMode(visible
1125                 ? PopupWindow.INPUT_METHOD_NEEDED : PopupWindow.INPUT_METHOD_NOT_NEEDED);
1126         showDropDown();
1127     }
1128 
1129     /**
1130      * @hide internal used only here and SearchDialog
1131      */
isInputMethodNotNeeded()1132     public boolean isInputMethodNotNeeded() {
1133         return mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1134     }
1135 
1136     /**
1137      * <p>Displays the drop down on screen.</p>
1138      */
showDropDown()1139     public void showDropDown() {
1140         int height = buildDropDown();
1141 
1142         int widthSpec = 0;
1143         int heightSpec = 0;
1144 
1145         boolean noInputMethod = isInputMethodNotNeeded();
1146 
1147         if (mPopup.isShowing()) {
1148             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
1149                 // The call to PopupWindow's update method below can accept -1 for any
1150                 // value you do not want to update.
1151                 widthSpec = -1;
1152             } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
1153                 widthSpec = getDropDownAnchorView().getWidth();
1154             } else {
1155                 widthSpec = mDropDownWidth;
1156             }
1157 
1158             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1159                 // The call to PopupWindow's update method below can accept -1 for any
1160                 // value you do not want to update.
1161                 heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
1162                 if (noInputMethod) {
1163                     mPopup.setWindowLayoutMode(
1164                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
1165                                     ViewGroup.LayoutParams.MATCH_PARENT : 0, 0);
1166                 } else {
1167                     mPopup.setWindowLayoutMode(
1168                             mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
1169                                     ViewGroup.LayoutParams.MATCH_PARENT : 0,
1170                             ViewGroup.LayoutParams.MATCH_PARENT);
1171                 }
1172             } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
1173                 heightSpec = height;
1174             } else {
1175                 heightSpec = mDropDownHeight;
1176             }
1177 
1178             mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
1179 
1180             mPopup.update(getDropDownAnchorView(), mDropDownHorizontalOffset,
1181                     mDropDownVerticalOffset, widthSpec, heightSpec);
1182         } else {
1183             if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
1184                 widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
1185             } else {
1186                 if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
1187                     mPopup.setWidth(getDropDownAnchorView().getWidth());
1188                 } else {
1189                     mPopup.setWidth(mDropDownWidth);
1190                 }
1191             }
1192 
1193             if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1194                 heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
1195             } else {
1196                 if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
1197                     mPopup.setHeight(height);
1198                 } else {
1199                     mPopup.setHeight(mDropDownHeight);
1200                 }
1201             }
1202 
1203             mPopup.setWindowLayoutMode(widthSpec, heightSpec);
1204             mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
1205 
1206             // use outside touchable to dismiss drop down when touching outside of it, so
1207             // only set this if the dropdown is not always visible
1208             mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
1209             mPopup.setTouchInterceptor(new PopupTouchInterceptor());
1210             mPopup.showAsDropDown(getDropDownAnchorView(),
1211                     mDropDownHorizontalOffset, mDropDownVerticalOffset);
1212             mDropDownList.setSelection(ListView.INVALID_POSITION);
1213             clearListSelection();
1214             post(mHideSelector);
1215         }
1216     }
1217 
1218     /**
1219      * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
1220      * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
1221      * ignore outside touch even when the drop down is not set to always visible.
1222      *
1223      * @hide used only by SearchDialog
1224      */
setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch)1225     public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
1226         mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
1227     }
1228 
1229     /**
1230      * <p>Builds the popup window's content and returns the height the popup
1231      * should have. Returns -1 when the content already exists.</p>
1232      *
1233      * @return the content's height or -1 if content already exists
1234      */
buildDropDown()1235     private int buildDropDown() {
1236         ViewGroup dropDownView;
1237         int otherHeights = 0;
1238 
1239         final ListAdapter adapter = mAdapter;
1240         if (adapter != null) {
1241             InputMethodManager imm = InputMethodManager.peekInstance();
1242             if (imm != null) {
1243                 final int count = Math.min(adapter.getCount(), 20);
1244                 CompletionInfo[] completions = new CompletionInfo[count];
1245                 int realCount = 0;
1246 
1247                 for (int i = 0; i < count; i++) {
1248                     if (adapter.isEnabled(i)) {
1249                         realCount++;
1250                         Object item = adapter.getItem(i);
1251                         long id = adapter.getItemId(i);
1252                         completions[i] = new CompletionInfo(id, i,
1253                                 convertSelectionToString(item));
1254                     }
1255                 }
1256 
1257                 if (realCount != count) {
1258                     CompletionInfo[] tmp = new CompletionInfo[realCount];
1259                     System.arraycopy(completions, 0, tmp, 0, realCount);
1260                     completions = tmp;
1261                 }
1262 
1263                 imm.displayCompletions(this, completions);
1264             }
1265         }
1266 
1267         if (mDropDownList == null) {
1268             Context context = getContext();
1269 
1270             mHideSelector = new ListSelectorHider();
1271 
1272             /**
1273              * This Runnable exists for the sole purpose of checking if the view layout has got
1274              * completed and if so call showDropDown to display the drop down. This is used to show
1275              * the drop down as soon as possible after user opens up the search dialog, without
1276              * waiting for the normal UI pipeline to do it's job which is slower than this method.
1277              */
1278             mShowDropDownRunnable = new Runnable() {
1279                 public void run() {
1280                     // View layout should be all done before displaying the drop down.
1281                     View view = getDropDownAnchorView();
1282                     if (view != null && view.getWindowToken() != null) {
1283                         showDropDown();
1284                     }
1285                 }
1286             };
1287 
1288             mDropDownList = new DropDownListView(context);
1289             mDropDownList.setSelector(mDropDownListHighlight);
1290             mDropDownList.setAdapter(adapter);
1291             mDropDownList.setVerticalFadingEdgeEnabled(true);
1292             mDropDownList.setOnItemClickListener(mDropDownItemClickListener);
1293             mDropDownList.setFocusable(true);
1294             mDropDownList.setFocusableInTouchMode(true);
1295             mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
1296                 public void onItemSelected(AdapterView<?> parent, View view,
1297                         int position, long id) {
1298 
1299                     if (position != -1) {
1300                         DropDownListView dropDownList = mDropDownList;
1301 
1302                         if (dropDownList != null) {
1303                             dropDownList.mListSelectionHidden = false;
1304                         }
1305                     }
1306                 }
1307 
1308                 public void onNothingSelected(AdapterView<?> parent) {
1309                 }
1310             });
1311             mDropDownList.setOnScrollListener(new PopupScrollListener());
1312 
1313             if (mItemSelectedListener != null) {
1314                 mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
1315             }
1316 
1317             dropDownView = mDropDownList;
1318 
1319             View hintView = getHintView(context);
1320             if (hintView != null) {
1321                 // if an hint has been specified, we accomodate more space for it and
1322                 // add a text view in the drop down menu, at the bottom of the list
1323                 LinearLayout hintContainer = new LinearLayout(context);
1324                 hintContainer.setOrientation(LinearLayout.VERTICAL);
1325 
1326                 LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
1327                         ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
1328                 );
1329                 hintContainer.addView(dropDownView, hintParams);
1330                 hintContainer.addView(hintView);
1331 
1332                 // measure the hint's height to find how much more vertical space
1333                 // we need to add to the drop down's height
1334                 int widthSpec = MeasureSpec.makeMeasureSpec(getWidth(), MeasureSpec.AT_MOST);
1335                 int heightSpec = MeasureSpec.UNSPECIFIED;
1336                 hintView.measure(widthSpec, heightSpec);
1337 
1338                 hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
1339                 otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
1340                         + hintParams.bottomMargin;
1341 
1342                 dropDownView = hintContainer;
1343             }
1344 
1345             mPopup.setContentView(dropDownView);
1346         } else {
1347             dropDownView = (ViewGroup) mPopup.getContentView();
1348             final View view = dropDownView.findViewById(HINT_VIEW_ID);
1349             if (view != null) {
1350                 LinearLayout.LayoutParams hintParams =
1351                         (LinearLayout.LayoutParams) view.getLayoutParams();
1352                 otherHeights = view.getMeasuredHeight() + hintParams.topMargin
1353                         + hintParams.bottomMargin;
1354             }
1355         }
1356 
1357         // Max height available on the screen for a popup.
1358         boolean ignoreBottomDecorations =
1359                 mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
1360         final int maxHeight = mPopup.getMaxAvailableHeight(
1361                 getDropDownAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
1362 
1363         // getMaxAvailableHeight() subtracts the padding, so we put it back,
1364         // to get the available height for the whole window
1365         int padding = 0;
1366         Drawable background = mPopup.getBackground();
1367         if (background != null) {
1368             background.getPadding(mTempRect);
1369             padding = mTempRect.top + mTempRect.bottom;
1370         }
1371 
1372         if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
1373             return maxHeight + padding;
1374         }
1375 
1376         final int listContent = mDropDownList.measureHeightOfChildren(MeasureSpec.UNSPECIFIED,
1377                 0, ListView.NO_POSITION, maxHeight - otherHeights, 2);
1378         // add padding only if the list has items in it, that way we don't show
1379         // the popup if it is not needed
1380         if (listContent > 0) otherHeights += padding;
1381 
1382         return listContent + otherHeights;
1383     }
1384 
getHintView(Context context)1385     private View getHintView(Context context) {
1386         if (mHintText != null && mHintText.length() > 0) {
1387             final TextView hintView = (TextView) LayoutInflater.from(context).inflate(
1388                     mHintResource, null).findViewById(com.android.internal.R.id.text1);
1389             hintView.setText(mHintText);
1390             hintView.setId(HINT_VIEW_ID);
1391             return hintView;
1392         } else {
1393             return null;
1394         }
1395     }
1396 
1397     /**
1398      * Sets the validator used to perform text validation.
1399      *
1400      * @param validator The validator used to validate the text entered in this widget.
1401      *
1402      * @see #getValidator()
1403      * @see #performValidation()
1404      */
setValidator(Validator validator)1405     public void setValidator(Validator validator) {
1406         mValidator = validator;
1407     }
1408 
1409     /**
1410      * Returns the Validator set with {@link #setValidator},
1411      * or <code>null</code> if it was not set.
1412      *
1413      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
1414      * @see #performValidation()
1415      */
getValidator()1416     public Validator getValidator() {
1417         return mValidator;
1418     }
1419 
1420     /**
1421      * If a validator was set on this view and the current string is not valid,
1422      * ask the validator to fix it.
1423      *
1424      * @see #getValidator()
1425      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
1426      */
performValidation()1427     public void performValidation() {
1428         if (mValidator == null) return;
1429 
1430         CharSequence text = getText();
1431 
1432         if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
1433             setText(mValidator.fixText(text));
1434         }
1435     }
1436 
1437     /**
1438      * Returns the Filter obtained from {@link Filterable#getFilter},
1439      * or <code>null</code> if {@link #setAdapter} was not called with
1440      * a Filterable.
1441      */
getFilter()1442     protected Filter getFilter() {
1443         return mFilter;
1444     }
1445 
1446     private class ListSelectorHider implements Runnable {
run()1447         public void run() {
1448             clearListSelection();
1449         }
1450     }
1451 
1452     private class ResizePopupRunnable implements Runnable {
run()1453         public void run() {
1454             mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
1455             showDropDown();
1456         }
1457     }
1458 
1459     private class PopupTouchInterceptor implements OnTouchListener {
onTouch(View v, MotionEvent event)1460         public boolean onTouch(View v, MotionEvent event) {
1461             final int action = event.getAction();
1462             if (action == MotionEvent.ACTION_DOWN &&
1463                     mPopup != null && mPopup.isShowing()) {
1464                 postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
1465             } else if (action == MotionEvent.ACTION_UP) {
1466                 removeCallbacks(mResizePopupRunnable);
1467             }
1468             return false;
1469         }
1470     }
1471 
1472     private class PopupScrollListener implements ListView.OnScrollListener {
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)1473         public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
1474                 int totalItemCount) {
1475 
1476         }
1477 
onScrollStateChanged(AbsListView view, int scrollState)1478         public void onScrollStateChanged(AbsListView view, int scrollState) {
1479             if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
1480                     !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
1481                 removeCallbacks(mResizePopupRunnable);
1482                 mResizePopupRunnable.run();
1483             }
1484         }
1485     }
1486 
1487     private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
onItemClick(AdapterView parent, View v, int position, long id)1488         public void onItemClick(AdapterView parent, View v, int position, long id) {
1489             performCompletion(v, position, id);
1490         }
1491     }
1492 
1493     /**
1494      * <p>Wrapper class for a ListView. This wrapper hijacks the focus to
1495      * make sure the list uses the appropriate drawables and states when
1496      * displayed on screen within a drop down. The focus is never actually
1497      * passed to the drop down; the list only looks focused.</p>
1498      */
1499     private static class DropDownListView extends ListView {
1500         /*
1501          * WARNING: This is a workaround for a touch mode issue.
1502          *
1503          * Touch mode is propagated lazily to windows. This causes problems in
1504          * the following scenario:
1505          * - Type something in the AutoCompleteTextView and get some results
1506          * - Move down with the d-pad to select an item in the list
1507          * - Move up with the d-pad until the selection disappears
1508          * - Type more text in the AutoCompleteTextView *using the soft keyboard*
1509          *   and get new results; you are now in touch mode
1510          * - The selection comes back on the first item in the list, even though
1511          *   the list is supposed to be in touch mode
1512          *
1513          * Using the soft keyboard triggers the touch mode change but that change
1514          * is propagated to our window only after the first list layout, therefore
1515          * after the list attempts to resurrect the selection.
1516          *
1517          * The trick to work around this issue is to pretend the list is in touch
1518          * mode when we know that the selection should not appear, that is when
1519          * we know the user moved the selection away from the list.
1520          *
1521          * This boolean is set to true whenever we explicitely hide the list's
1522          * selection and reset to false whenver we know the user moved the
1523          * selection back to the list.
1524          *
1525          * When this boolean is true, isInTouchMode() returns true, otherwise it
1526          * returns super.isInTouchMode().
1527          */
1528         private boolean mListSelectionHidden;
1529 
1530         /**
1531          * <p>Creates a new list view wrapper.</p>
1532          *
1533          * @param context this view's context
1534          */
DropDownListView(Context context)1535         public DropDownListView(Context context) {
1536             super(context, null, com.android.internal.R.attr.dropDownListViewStyle);
1537         }
1538 
1539         /**
1540          * <p>Avoids jarring scrolling effect by ensuring that list elements
1541          * made of a text view fit on a single line.</p>
1542          *
1543          * @param position the item index in the list to get a view for
1544          * @return the view for the specified item
1545          */
1546         @Override
obtainView(int position, boolean[] isScrap)1547         View obtainView(int position, boolean[] isScrap) {
1548             View view = super.obtainView(position, isScrap);
1549 
1550             if (view instanceof TextView) {
1551                 ((TextView) view).setHorizontallyScrolling(true);
1552             }
1553 
1554             return view;
1555         }
1556 
1557         @Override
isInTouchMode()1558         public boolean isInTouchMode() {
1559             // WARNING: Please read the comment where mListSelectionHidden is declared
1560             return mListSelectionHidden || super.isInTouchMode();
1561         }
1562 
1563         /**
1564          * <p>Returns the focus state in the drop down.</p>
1565          *
1566          * @return true always
1567          */
1568         @Override
hasWindowFocus()1569         public boolean hasWindowFocus() {
1570             return true;
1571         }
1572 
1573         /**
1574          * <p>Returns the focus state in the drop down.</p>
1575          *
1576          * @return true always
1577          */
1578         @Override
isFocused()1579         public boolean isFocused() {
1580             return true;
1581         }
1582 
1583         /**
1584          * <p>Returns the focus state in the drop down.</p>
1585          *
1586          * @return true always
1587          */
1588         @Override
hasFocus()1589         public boolean hasFocus() {
1590             return true;
1591         }
1592 
onCreateDrawableState(int extraSpace)1593         protected int[] onCreateDrawableState(int extraSpace) {
1594             int[] res = super.onCreateDrawableState(extraSpace);
1595             //noinspection ConstantIfStatement
1596             if (false) {
1597                 StringBuilder sb = new StringBuilder("Created drawable state: [");
1598                 for (int i=0; i<res.length; i++) {
1599                     if (i > 0) sb.append(", ");
1600                     sb.append("0x");
1601                     sb.append(Integer.toHexString(res[i]));
1602                 }
1603                 sb.append("]");
1604                 Log.i(TAG, sb.toString());
1605             }
1606             return res;
1607         }
1608     }
1609 
1610     /**
1611      * This interface is used to make sure that the text entered in this TextView complies to
1612      * a certain format.  Since there is no foolproof way to prevent the user from leaving
1613      * this View with an incorrect value in it, all we can do is try to fix it ourselves
1614      * when this happens.
1615      */
1616     public interface Validator {
1617         /**
1618          * Validates the specified text.
1619          *
1620          * @return true If the text currently in the text editor is valid.
1621          *
1622          * @see #fixText(CharSequence)
1623          */
isValid(CharSequence text)1624         boolean isValid(CharSequence text);
1625 
1626         /**
1627          * Corrects the specified text to make it valid.
1628          *
1629          * @param invalidText A string that doesn't pass validation: isValid(invalidText)
1630          *        returns false
1631          *
1632          * @return A string based on invalidText such as invoking isValid() on it returns true.
1633          *
1634          * @see #isValid(CharSequence)
1635          */
fixText(CharSequence invalidText)1636         CharSequence fixText(CharSequence invalidText);
1637     }
1638 
1639     /**
1640      * Allows us a private hook into the on click event without preventing users from setting
1641      * their own click listener.
1642      */
1643     private class PassThroughClickListener implements OnClickListener {
1644 
1645         private View.OnClickListener mWrapped;
1646 
1647         /** {@inheritDoc} */
onClick(View v)1648         public void onClick(View v) {
1649             onClickImpl();
1650 
1651             if (mWrapped != null) mWrapped.onClick(v);
1652         }
1653     }
1654 
1655     private class PopupDataSetObserver extends DataSetObserver {
1656         @Override
onChanged()1657         public void onChanged() {
1658             if (isPopupShowing()) {
1659                 // This will resize the popup to fit the new adapter's content
1660                 showDropDown();
1661             } else if (mAdapter != null) {
1662                 // If the popup is not showing already, showing it will cause
1663                 // the list of data set observers attached to the adapter to
1664                 // change. We can't do it from here, because we are in the middle
1665                 // of iterating throught he list of observers.
1666                 post(new Runnable() {
1667                     public void run() {
1668                         final ListAdapter adapter = mAdapter;
1669                         if (adapter != null) {
1670                             updateDropDownForFilter(adapter.getCount());
1671                         }
1672                     }
1673                 });
1674             }
1675         }
1676 
1677         @Override
onInvalidated()1678         public void onInvalidated() {
1679             if (!mDropDownAlwaysVisible) {
1680                 // There's no data to display so make sure we're not showing
1681                 // the drop down and its list
1682                 dismissDropDown();
1683             }
1684         }
1685     }
1686 }
1687