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