Home | History | Annotate | Download | only in widget
      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.View;
     33 import android.view.ViewGroup;
     34 import android.view.WindowManager;
     35 import android.view.inputmethod.CompletionInfo;
     36 import android.view.inputmethod.EditorInfo;
     37 import android.view.inputmethod.InputMethodManager;
     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  * <p>See the <a href="{@docRoot}resources/tutorials/views/hello-autocomplete.html">Auto Complete
     79  * tutorial</a>.</p>
     80  *
     81  * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
     82  * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
     83  * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
     84  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
     85  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
     86  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
     87  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
     88  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownVerticalOffset
     89  * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHorizontalOffset
     90  */
     91 public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
     92     static final boolean DEBUG = false;
     93     static final String TAG = "AutoCompleteTextView";
     94 
     95     static final int EXPAND_MAX = 3;
     96 
     97     private CharSequence mHintText;
     98     private TextView mHintView;
     99     private int mHintResource;
    100 
    101     private ListAdapter mAdapter;
    102     private Filter mFilter;
    103     private int mThreshold;
    104 
    105     private ListPopupWindow mPopup;
    106     private int mDropDownAnchorId;
    107 
    108     private AdapterView.OnItemClickListener mItemClickListener;
    109     private AdapterView.OnItemSelectedListener mItemSelectedListener;
    110 
    111     private boolean mDropDownDismissedOnCompletion = true;
    112 
    113     private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    114     private boolean mOpenBefore;
    115 
    116     private Validator mValidator = null;
    117 
    118     // Set to true when text is set directly and no filtering shall be performed
    119     private boolean mBlockCompletion;
    120 
    121     // When set, an update in the underlying adapter will update the result list popup.
    122     // Set to false when the list is hidden to prevent asynchronous updates to popup the list again.
    123     private boolean mPopupCanBeUpdated = true;
    124 
    125     private PassThroughClickListener mPassThroughClickListener;
    126     private PopupDataSetObserver mObserver;
    127 
    128     public AutoCompleteTextView(Context context) {
    129         this(context, null);
    130     }
    131 
    132     public AutoCompleteTextView(Context context, AttributeSet attrs) {
    133         this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
    134     }
    135 
    136     public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyle) {
    137         super(context, attrs, defStyle);
    138 
    139         mPopup = new ListPopupWindow(context, attrs,
    140                 com.android.internal.R.attr.autoCompleteTextViewStyle);
    141         mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
    142         mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
    143 
    144         TypedArray a =
    145             context.obtainStyledAttributes(
    146                 attrs, com.android.internal.R.styleable.AutoCompleteTextView, defStyle, 0);
    147 
    148         mThreshold = a.getInt(
    149                 R.styleable.AutoCompleteTextView_completionThreshold, 2);
    150 
    151         mPopup.setListSelector(a.getDrawable(R.styleable.AutoCompleteTextView_dropDownSelector));
    152         mPopup.setVerticalOffset((int)
    153                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownVerticalOffset, 0.0f));
    154         mPopup.setHorizontalOffset((int)
    155                 a.getDimension(R.styleable.AutoCompleteTextView_dropDownHorizontalOffset, 0.0f));
    156 
    157         // Get the anchor's id now, but the view won't be ready, so wait to actually get the
    158         // view and store it in mDropDownAnchorView lazily in getDropDownAnchorView later.
    159         // Defaults to NO_ID, in which case the getDropDownAnchorView method will simply return
    160         // this TextView, as a default anchoring point.
    161         mDropDownAnchorId = a.getResourceId(R.styleable.AutoCompleteTextView_dropDownAnchor,
    162                 View.NO_ID);
    163 
    164         // For dropdown width, the developer can specify a specific width, or MATCH_PARENT
    165         // (for full screen width) or WRAP_CONTENT (to match the width of the anchored view).
    166         mPopup.setWidth(a.getLayoutDimension(
    167                 R.styleable.AutoCompleteTextView_dropDownWidth,
    168                 ViewGroup.LayoutParams.WRAP_CONTENT));
    169         mPopup.setHeight(a.getLayoutDimension(
    170                 R.styleable.AutoCompleteTextView_dropDownHeight,
    171                 ViewGroup.LayoutParams.WRAP_CONTENT));
    172 
    173         mHintResource = a.getResourceId(R.styleable.AutoCompleteTextView_completionHintView,
    174                 R.layout.simple_dropdown_hint);
    175 
    176         mPopup.setOnItemClickListener(new DropDownItemClickListener());
    177         setCompletionHint(a.getText(R.styleable.AutoCompleteTextView_completionHint));
    178 
    179         // Always turn on the auto complete input type flag, since it
    180         // makes no sense to use this widget without it.
    181         int inputType = getInputType();
    182         if ((inputType&EditorInfo.TYPE_MASK_CLASS)
    183                 == EditorInfo.TYPE_CLASS_TEXT) {
    184             inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
    185             setRawInputType(inputType);
    186         }
    187 
    188         a.recycle();
    189 
    190         setFocusable(true);
    191 
    192         addTextChangedListener(new MyWatcher());
    193 
    194         mPassThroughClickListener = new PassThroughClickListener();
    195         super.setOnClickListener(mPassThroughClickListener);
    196     }
    197 
    198     @Override
    199     public void setOnClickListener(OnClickListener listener) {
    200         mPassThroughClickListener.mWrapped = listener;
    201     }
    202 
    203     /**
    204      * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
    205      */
    206     private void onClickImpl() {
    207         // If the dropdown is showing, bring the keyboard to the front
    208         // when the user touches the text field.
    209         if (isPopupShowing()) {
    210             ensureImeVisible(true);
    211         }
    212     }
    213 
    214     /**
    215      * <p>Sets the optional hint text that is displayed at the bottom of the
    216      * the matching list.  This can be used as a cue to the user on how to
    217      * best use the list, or to provide extra information.</p>
    218      *
    219      * @param hint the text to be displayed to the user
    220      *
    221      * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
    222      */
    223     public void setCompletionHint(CharSequence hint) {
    224         mHintText = hint;
    225         if (hint != null) {
    226             if (mHintView == null) {
    227                 final TextView hintView = (TextView) LayoutInflater.from(getContext()).inflate(
    228                         mHintResource, null).findViewById(com.android.internal.R.id.text1);
    229                 hintView.setText(mHintText);
    230                 mHintView = hintView;
    231                 mPopup.setPromptView(hintView);
    232             } else {
    233                 mHintView.setText(hint);
    234             }
    235         } else {
    236             mPopup.setPromptView(null);
    237             mHintView = null;
    238         }
    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#MATCH_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      */
    250     public int getDropDownWidth() {
    251         return mPopup.getWidth();
    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#MATCH_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      */
    263     public void setDropDownWidth(int width) {
    264         mPopup.setWidth(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#MATCH_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      */
    277     public int getDropDownHeight() {
    278         return mPopup.getHeight();
    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#MATCH_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      */
    291     public void setDropDownHeight(int height) {
    292         mPopup.setHeight(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      */
    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      */
    315     public void setDropDownAnchor(int id) {
    316         mDropDownAnchorId = id;
    317         mPopup.setAnchorView(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      */
    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      */
    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      */
    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      */
    358     public void setDropDownVerticalOffset(int offset) {
    359         mPopup.setVerticalOffset(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      */
    367     public int getDropDownVerticalOffset() {
    368         return mPopup.getVerticalOffset();
    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      */
    376     public void setDropDownHorizontalOffset(int offset) {
    377         mPopup.setHorizontalOffset(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      */
    385     public int getDropDownHorizontalOffset() {
    386         return mPopup.getHorizontalOffset();
    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      */
    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      */
    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      */
    422     public boolean isDropDownAlwaysVisible() {
    423         return mPopup.isDropDownAlwaysVisible();
    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      */
    439     public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
    440         mPopup.setDropDownAlwaysVisible(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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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
    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
    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      */
    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      */
    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      */
    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      */
    592     public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
    593         if (mObserver == null) {
    594             mObserver = new PopupDataSetObserver();
    595         } else if (mAdapter != null) {
    596             mAdapter.unregisterDataSetObserver(mObserver);
    597         }
    598         mAdapter = adapter;
    599         if (mAdapter != null) {
    600             //noinspection unchecked
    601             mFilter = ((Filterable) mAdapter).getFilter();
    602             adapter.registerDataSetObserver(mObserver);
    603         } else {
    604             mFilter = null;
    605         }
    606 
    607         mPopup.setAdapter(mAdapter);
    608     }
    609 
    610     @Override
    611     public boolean onKeyPreIme(int keyCode, KeyEvent event) {
    612         if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
    613                 && !mPopup.isDropDownAlwaysVisible()) {
    614             // special case for the back key, we do not even try to send it
    615             // to the drop down list but instead, consume it immediately
    616             if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
    617                 KeyEvent.DispatcherState state = getKeyDispatcherState();
    618                 if (state != null) {
    619                     state.startTracking(event, this);
    620                 }
    621                 return true;
    622             } else if (event.getAction() == KeyEvent.ACTION_UP) {
    623                 KeyEvent.DispatcherState state = getKeyDispatcherState();
    624                 if (state != null) {
    625                     state.handleUpEvent(event);
    626                 }
    627                 if (event.isTracking() && !event.isCanceled()) {
    628                     dismissDropDown();
    629                     return true;
    630                 }
    631             }
    632         }
    633         return super.onKeyPreIme(keyCode, event);
    634     }
    635 
    636     @Override
    637     public boolean onKeyUp(int keyCode, KeyEvent event) {
    638         boolean consumed = mPopup.onKeyUp(keyCode, event);
    639         if (consumed) {
    640             switch (keyCode) {
    641             // if the list accepts the key events and the key event
    642             // was a click, the text view gets the selected item
    643             // from the drop down as its content
    644             case KeyEvent.KEYCODE_ENTER:
    645             case KeyEvent.KEYCODE_DPAD_CENTER:
    646             case KeyEvent.KEYCODE_TAB:
    647                 if (event.hasNoModifiers()) {
    648                     performCompletion();
    649                 }
    650                 return true;
    651             }
    652         }
    653 
    654         if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
    655             performCompletion();
    656             return true;
    657         }
    658 
    659         return super.onKeyUp(keyCode, event);
    660     }
    661 
    662     @Override
    663     public boolean onKeyDown(int keyCode, KeyEvent event) {
    664         if (mPopup.onKeyDown(keyCode, event)) {
    665             return true;
    666         }
    667 
    668         if (!isPopupShowing()) {
    669             switch(keyCode) {
    670             case KeyEvent.KEYCODE_DPAD_DOWN:
    671                 if (event.hasNoModifiers()) {
    672                     performValidation();
    673                 }
    674             }
    675         }
    676 
    677         if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
    678             return true;
    679         }
    680 
    681         mLastKeyCode = keyCode;
    682         boolean handled = super.onKeyDown(keyCode, event);
    683         mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
    684 
    685         if (handled && isPopupShowing()) {
    686             clearListSelection();
    687         }
    688 
    689         return handled;
    690     }
    691 
    692     /**
    693      * Returns <code>true</code> if the amount of text in the field meets
    694      * or exceeds the {@link #getThreshold} requirement.  You can override
    695      * this to impose a different standard for when filtering will be
    696      * triggered.
    697      */
    698     public boolean enoughToFilter() {
    699         if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
    700                 + " threshold=" + mThreshold);
    701         return getText().length() >= mThreshold;
    702     }
    703 
    704     /**
    705      * This is used to watch for edits to the text view.  Note that we call
    706      * to methods on the auto complete text view class so that we can access
    707      * private vars without going through thunks.
    708      */
    709     private class MyWatcher implements TextWatcher {
    710         public void afterTextChanged(Editable s) {
    711             doAfterTextChanged();
    712         }
    713         public void beforeTextChanged(CharSequence s, int start, int count, int after) {
    714             doBeforeTextChanged();
    715         }
    716         public void onTextChanged(CharSequence s, int start, int before, int count) {
    717         }
    718     }
    719 
    720     void doBeforeTextChanged() {
    721         if (mBlockCompletion) return;
    722 
    723         // when text is changed, inserted or deleted, we attempt to show
    724         // the drop down
    725         mOpenBefore = isPopupShowing();
    726         if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
    727     }
    728 
    729     void doAfterTextChanged() {
    730         if (mBlockCompletion) return;
    731 
    732         // if the list was open before the keystroke, but closed afterwards,
    733         // then something in the keystroke processing (an input filter perhaps)
    734         // called performCompletion() and we shouldn't do any more processing.
    735         if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
    736                 + " open=" + isPopupShowing());
    737         if (mOpenBefore && !isPopupShowing()) {
    738             return;
    739         }
    740 
    741         // the drop down is shown only when a minimum number of characters
    742         // was typed in the text view
    743         if (enoughToFilter()) {
    744             if (mFilter != null) {
    745                 mPopupCanBeUpdated = true;
    746                 performFiltering(getText(), mLastKeyCode);
    747             }
    748         } else {
    749             // drop down is automatically dismissed when enough characters
    750             // are deleted from the text view
    751             if (!mPopup.isDropDownAlwaysVisible()) {
    752                 dismissDropDown();
    753             }
    754             if (mFilter != null) {
    755                 mFilter.filter(null);
    756             }
    757         }
    758     }
    759 
    760     /**
    761      * <p>Indicates whether the popup menu is showing.</p>
    762      *
    763      * @return true if the popup menu is showing, false otherwise
    764      */
    765     public boolean isPopupShowing() {
    766         return mPopup.isShowing();
    767     }
    768 
    769     /**
    770      * <p>Converts the selected item from the drop down list into a sequence
    771      * of character that can be used in the edit box.</p>
    772      *
    773      * @param selectedItem the item selected by the user for completion
    774      *
    775      * @return a sequence of characters representing the selected suggestion
    776      */
    777     protected CharSequence convertSelectionToString(Object selectedItem) {
    778         return mFilter.convertResultToString(selectedItem);
    779     }
    780 
    781     /**
    782      * <p>Clear the list selection.  This may only be temporary, as user input will often bring
    783      * it back.
    784      */
    785     public void clearListSelection() {
    786         mPopup.clearListSelection();
    787     }
    788 
    789     /**
    790      * Set the position of the dropdown view selection.
    791      *
    792      * @param position The position to move the selector to.
    793      */
    794     public void setListSelection(int position) {
    795         mPopup.setSelection(position);
    796     }
    797 
    798     /**
    799      * Get the position of the dropdown view selection, if there is one.  Returns
    800      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
    801      * there is no selection.
    802      *
    803      * @return the position of the current selection, if there is one, or
    804      * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
    805      *
    806      * @see ListView#getSelectedItemPosition()
    807      */
    808     public int getListSelection() {
    809         return mPopup.getSelectedItemPosition();
    810     }
    811 
    812     /**
    813      * <p>Starts filtering the content of the drop down list. The filtering
    814      * pattern is the content of the edit box. Subclasses should override this
    815      * method to filter with a different pattern, for instance a substring of
    816      * <code>text</code>.</p>
    817      *
    818      * @param text the filtering pattern
    819      * @param keyCode the last character inserted in the edit box; beware that
    820      * this will be null when text is being added through a soft input method.
    821      */
    822     @SuppressWarnings({ "UnusedDeclaration" })
    823     protected void performFiltering(CharSequence text, int keyCode) {
    824         mFilter.filter(text, this);
    825     }
    826 
    827     /**
    828      * <p>Performs the text completion by converting the selected item from
    829      * the drop down list into a string, replacing the text box's content with
    830      * this string and finally dismissing the drop down menu.</p>
    831      */
    832     public void performCompletion() {
    833         performCompletion(null, -1, -1);
    834     }
    835 
    836     @Override
    837     public void onCommitCompletion(CompletionInfo completion) {
    838         if (isPopupShowing()) {
    839             mPopup.performItemClick(completion.getPosition());
    840         }
    841     }
    842 
    843     private void performCompletion(View selectedView, int position, long id) {
    844         if (isPopupShowing()) {
    845             Object selectedItem;
    846             if (position < 0) {
    847                 selectedItem = mPopup.getSelectedItem();
    848             } else {
    849                 selectedItem = mAdapter.getItem(position);
    850             }
    851             if (selectedItem == null) {
    852                 Log.w(TAG, "performCompletion: no selected item");
    853                 return;
    854             }
    855 
    856             mBlockCompletion = true;
    857             replaceText(convertSelectionToString(selectedItem));
    858             mBlockCompletion = false;
    859 
    860             if (mItemClickListener != null) {
    861                 final ListPopupWindow list = mPopup;
    862 
    863                 if (selectedView == null || position < 0) {
    864                     selectedView = list.getSelectedView();
    865                     position = list.getSelectedItemPosition();
    866                     id = list.getSelectedItemId();
    867                 }
    868                 mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
    869             }
    870         }
    871 
    872         if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
    873             dismissDropDown();
    874         }
    875     }
    876 
    877     /**
    878      * Identifies whether the view is currently performing a text completion, so subclasses
    879      * can decide whether to respond to text changed events.
    880      */
    881     public boolean isPerformingCompletion() {
    882         return mBlockCompletion;
    883     }
    884 
    885     /**
    886      * Like {@link #setText(CharSequence)}, except that it can disable filtering.
    887      *
    888      * @param filter If <code>false</code>, no filtering will be performed
    889      *        as a result of this call.
    890      *
    891      * @hide Pending API council approval.
    892      */
    893     public void setText(CharSequence text, boolean filter) {
    894         if (filter) {
    895             setText(text);
    896         } else {
    897             mBlockCompletion = true;
    898             setText(text);
    899             mBlockCompletion = false;
    900         }
    901     }
    902 
    903     /**
    904      * <p>Performs the text completion by replacing the current text by the
    905      * selected item. Subclasses should override this method to avoid replacing
    906      * the whole content of the edit box.</p>
    907      *
    908      * @param text the selected suggestion in the drop down list
    909      */
    910     protected void replaceText(CharSequence text) {
    911         clearComposingText();
    912 
    913         setText(text);
    914         // make sure we keep the caret at the end of the text view
    915         Editable spannable = getText();
    916         Selection.setSelection(spannable, spannable.length());
    917     }
    918 
    919     /** {@inheritDoc} */
    920     public void onFilterComplete(int count) {
    921         updateDropDownForFilter(count);
    922     }
    923 
    924     private void updateDropDownForFilter(int count) {
    925         // Not attached to window, don't update drop-down
    926         if (getWindowVisibility() == View.GONE) return;
    927 
    928         /*
    929          * This checks enoughToFilter() again because filtering requests
    930          * are asynchronous, so the result may come back after enough text
    931          * has since been deleted to make it no longer appropriate
    932          * to filter.
    933          */
    934 
    935         final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
    936         final boolean enoughToFilter = enoughToFilter();
    937         if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) {
    938             if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) {
    939                 showDropDown();
    940             }
    941         } else if (!dropDownAlwaysVisible && isPopupShowing()) {
    942             dismissDropDown();
    943             // When the filter text is changed, the first update from the adapter may show an empty
    944             // count (when the query is being performed on the network). Future updates when some
    945             // content has been retrieved should still be able to update the list.
    946             mPopupCanBeUpdated = true;
    947         }
    948     }
    949 
    950     @Override
    951     public void onWindowFocusChanged(boolean hasWindowFocus) {
    952         super.onWindowFocusChanged(hasWindowFocus);
    953         if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
    954             dismissDropDown();
    955         }
    956     }
    957 
    958     @Override
    959     protected void onDisplayHint(int hint) {
    960         super.onDisplayHint(hint);
    961         switch (hint) {
    962             case INVISIBLE:
    963                 if (!mPopup.isDropDownAlwaysVisible()) {
    964                     dismissDropDown();
    965                 }
    966                 break;
    967         }
    968     }
    969 
    970     @Override
    971     protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
    972         super.onFocusChanged(focused, direction, previouslyFocusedRect);
    973         // Perform validation if the view is losing focus.
    974         if (!focused) {
    975             performValidation();
    976         }
    977         if (!focused && !mPopup.isDropDownAlwaysVisible()) {
    978             dismissDropDown();
    979         }
    980     }
    981 
    982     @Override
    983     protected void onAttachedToWindow() {
    984         super.onAttachedToWindow();
    985     }
    986 
    987     @Override
    988     protected void onDetachedFromWindow() {
    989         dismissDropDown();
    990         super.onDetachedFromWindow();
    991     }
    992 
    993     /**
    994      * <p>Closes the drop down if present on screen.</p>
    995      */
    996     public void dismissDropDown() {
    997         InputMethodManager imm = InputMethodManager.peekInstance();
    998         if (imm != null) {
    999             imm.displayCompletions(this, null);
   1000         }
   1001         mPopup.dismiss();
   1002         mPopupCanBeUpdated = false;
   1003     }
   1004 
   1005     @Override
   1006     protected boolean setFrame(final int l, int t, final int r, int b) {
   1007         boolean result = super.setFrame(l, t, r, b);
   1008 
   1009         if (isPopupShowing()) {
   1010             showDropDown();
   1011         }
   1012 
   1013         return result;
   1014     }
   1015 
   1016     /**
   1017      * Issues a runnable to show the dropdown as soon as possible.
   1018      *
   1019      * @hide internal used only by SearchDialog
   1020      */
   1021     public void showDropDownAfterLayout() {
   1022         mPopup.postShow();
   1023     }
   1024 
   1025     /**
   1026      * Ensures that the drop down is not obscuring the IME.
   1027      * @param visible whether the ime should be in front. If false, the ime is pushed to
   1028      * the background.
   1029      * @hide internal used only here and SearchDialog
   1030      */
   1031     public void ensureImeVisible(boolean visible) {
   1032         mPopup.setInputMethodMode(visible
   1033                 ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
   1034         showDropDown();
   1035     }
   1036 
   1037     /**
   1038      * @hide internal used only here and SearchDialog
   1039      */
   1040     public boolean isInputMethodNotNeeded() {
   1041         return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED;
   1042     }
   1043 
   1044     /**
   1045      * <p>Displays the drop down on screen.</p>
   1046      */
   1047     public void showDropDown() {
   1048         buildImeCompletions();
   1049 
   1050         if (mPopup.getAnchorView() == null) {
   1051             if (mDropDownAnchorId != View.NO_ID) {
   1052                 mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
   1053             } else {
   1054                 mPopup.setAnchorView(this);
   1055             }
   1056         }
   1057         if (!isPopupShowing()) {
   1058             // Make sure the list does not obscure the IME when shown for the first time.
   1059             mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED);
   1060             mPopup.setListItemExpandMax(EXPAND_MAX);
   1061         }
   1062         mPopup.show();
   1063         mPopup.getListView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
   1064     }
   1065 
   1066     /**
   1067      * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
   1068      * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
   1069      * ignore outside touch even when the drop down is not set to always visible.
   1070      *
   1071      * @hide used only by SearchDialog
   1072      */
   1073     public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
   1074         mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch);
   1075     }
   1076 
   1077     private void buildImeCompletions() {
   1078         final ListAdapter adapter = mAdapter;
   1079         if (adapter != null) {
   1080             InputMethodManager imm = InputMethodManager.peekInstance();
   1081             if (imm != null) {
   1082                 final int count = Math.min(adapter.getCount(), 20);
   1083                 CompletionInfo[] completions = new CompletionInfo[count];
   1084                 int realCount = 0;
   1085 
   1086                 for (int i = 0; i < count; i++) {
   1087                     if (adapter.isEnabled(i)) {
   1088                         realCount++;
   1089                         Object item = adapter.getItem(i);
   1090                         long id = adapter.getItemId(i);
   1091                         completions[i] = new CompletionInfo(id, i, convertSelectionToString(item));
   1092                     }
   1093                 }
   1094 
   1095                 if (realCount != count) {
   1096                     CompletionInfo[] tmp = new CompletionInfo[realCount];
   1097                     System.arraycopy(completions, 0, tmp, 0, realCount);
   1098                     completions = tmp;
   1099                 }
   1100 
   1101                 imm.displayCompletions(this, completions);
   1102             }
   1103         }
   1104     }
   1105 
   1106     /**
   1107      * Sets the validator used to perform text validation.
   1108      *
   1109      * @param validator The validator used to validate the text entered in this widget.
   1110      *
   1111      * @see #getValidator()
   1112      * @see #performValidation()
   1113      */
   1114     public void setValidator(Validator validator) {
   1115         mValidator = validator;
   1116     }
   1117 
   1118     /**
   1119      * Returns the Validator set with {@link #setValidator},
   1120      * or <code>null</code> if it was not set.
   1121      *
   1122      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
   1123      * @see #performValidation()
   1124      */
   1125     public Validator getValidator() {
   1126         return mValidator;
   1127     }
   1128 
   1129     /**
   1130      * If a validator was set on this view and the current string is not valid,
   1131      * ask the validator to fix it.
   1132      *
   1133      * @see #getValidator()
   1134      * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
   1135      */
   1136     public void performValidation() {
   1137         if (mValidator == null) return;
   1138 
   1139         CharSequence text = getText();
   1140 
   1141         if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
   1142             setText(mValidator.fixText(text));
   1143         }
   1144     }
   1145 
   1146     /**
   1147      * Returns the Filter obtained from {@link Filterable#getFilter},
   1148      * or <code>null</code> if {@link #setAdapter} was not called with
   1149      * a Filterable.
   1150      */
   1151     protected Filter getFilter() {
   1152         return mFilter;
   1153     }
   1154 
   1155     private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
   1156         public void onItemClick(AdapterView parent, View v, int position, long id) {
   1157             performCompletion(v, position, id);
   1158         }
   1159     }
   1160 
   1161     /**
   1162      * This interface is used to make sure that the text entered in this TextView complies to
   1163      * a certain format.  Since there is no foolproof way to prevent the user from leaving
   1164      * this View with an incorrect value in it, all we can do is try to fix it ourselves
   1165      * when this happens.
   1166      */
   1167     public interface Validator {
   1168         /**
   1169          * Validates the specified text.
   1170          *
   1171          * @return true If the text currently in the text editor is valid.
   1172          *
   1173          * @see #fixText(CharSequence)
   1174          */
   1175         boolean isValid(CharSequence text);
   1176 
   1177         /**
   1178          * Corrects the specified text to make it valid.
   1179          *
   1180          * @param invalidText A string that doesn't pass validation: isValid(invalidText)
   1181          *        returns false
   1182          *
   1183          * @return A string based on invalidText such as invoking isValid() on it returns true.
   1184          *
   1185          * @see #isValid(CharSequence)
   1186          */
   1187         CharSequence fixText(CharSequence invalidText);
   1188     }
   1189 
   1190     /**
   1191      * Allows us a private hook into the on click event without preventing users from setting
   1192      * their own click listener.
   1193      */
   1194     private class PassThroughClickListener implements OnClickListener {
   1195 
   1196         private View.OnClickListener mWrapped;
   1197 
   1198         /** {@inheritDoc} */
   1199         public void onClick(View v) {
   1200             onClickImpl();
   1201 
   1202             if (mWrapped != null) mWrapped.onClick(v);
   1203         }
   1204     }
   1205 
   1206     private class PopupDataSetObserver extends DataSetObserver {
   1207         @Override
   1208         public void onChanged() {
   1209             if (mAdapter != null) {
   1210                 // If the popup is not showing already, showing it will cause
   1211                 // the list of data set observers attached to the adapter to
   1212                 // change. We can't do it from here, because we are in the middle
   1213                 // of iterating through the list of observers.
   1214                 post(new Runnable() {
   1215                     public void run() {
   1216                         final ListAdapter adapter = mAdapter;
   1217                         if (adapter != null) {
   1218                             // This will re-layout, thus resetting mDataChanged, so that the
   1219                             // listView click listener stays responsive
   1220                             updateDropDownForFilter(adapter.getCount());
   1221                         }
   1222                     }
   1223                 });
   1224             }
   1225         }
   1226     }
   1227 }
   1228