Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2015 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 
     20 import android.annotation.NonNull;
     21 import android.content.Context;
     22 import android.view.MotionEvent;
     23 import android.view.View;
     24 
     25 import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
     26 
     27 /**
     28  * Wrapper class for a ListView. This wrapper can hijack the focus to
     29  * make sure the list uses the appropriate drawables and states when
     30  * displayed on screen within a drop down. The focus is never actually
     31  * passed to the drop down in this mode; the list only looks focused.
     32  *
     33  * @hide
     34  */
     35 public class DropDownListView extends ListView {
     36     /*
     37      * WARNING: This is a workaround for a touch mode issue.
     38      *
     39      * Touch mode is propagated lazily to windows. This causes problems in
     40      * the following scenario:
     41      * - Type something in the AutoCompleteTextView and get some results
     42      * - Move down with the d-pad to select an item in the list
     43      * - Move up with the d-pad until the selection disappears
     44      * - Type more text in the AutoCompleteTextView *using the soft keyboard*
     45      *   and get new results; you are now in touch mode
     46      * - The selection comes back on the first item in the list, even though
     47      *   the list is supposed to be in touch mode
     48      *
     49      * Using the soft keyboard triggers the touch mode change but that change
     50      * is propagated to our window only after the first list layout, therefore
     51      * after the list attempts to resurrect the selection.
     52      *
     53      * The trick to work around this issue is to pretend the list is in touch
     54      * mode when we know that the selection should not appear, that is when
     55      * we know the user moved the selection away from the list.
     56      *
     57      * This boolean is set to true whenever we explicitly hide the list's
     58      * selection and reset to false whenever we know the user moved the
     59      * selection back to the list.
     60      *
     61      * When this boolean is true, isInTouchMode() returns true, otherwise it
     62      * returns super.isInTouchMode().
     63      */
     64     private boolean mListSelectionHidden;
     65 
     66     /**
     67      * True if this wrapper should fake focus.
     68      */
     69     private boolean mHijackFocus;
     70 
     71     /** Whether to force drawing of the pressed state selector. */
     72     private boolean mDrawsInPressedState;
     73 
     74     /** Helper for drag-to-open auto scrolling. */
     75     private AbsListViewAutoScroller mScrollHelper;
     76 
     77     /**
     78      * Runnable posted when we are awaiting hover event resolution. When set,
     79      * drawable state changes are postponed.
     80      */
     81     private ResolveHoverRunnable mResolveHoverRunnable;
     82 
     83     /**
     84      * Creates a new list view wrapper.
     85      *
     86      * @param context this view's context
     87      */
     88     public DropDownListView(@NonNull Context context, boolean hijackFocus) {
     89         this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
     90     }
     91 
     92     /**
     93      * Creates a new list view wrapper.
     94      *
     95      * @param context this view's context
     96      */
     97     public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) {
     98         super(context, null, defStyleAttr);
     99         mHijackFocus = hijackFocus;
    100         // TODO: Add an API to control this
    101         setCacheColorHint(0); // Transparent, since the background drawable could be anything.
    102     }
    103 
    104     @Override
    105     boolean shouldShowSelector() {
    106         return isHovered() || super.shouldShowSelector();
    107     }
    108 
    109     @Override
    110     public boolean onTouchEvent(MotionEvent ev) {
    111         if (mResolveHoverRunnable != null) {
    112             // Resolved hover event as hover => touch transition.
    113             mResolveHoverRunnable.cancel();
    114         }
    115 
    116         return super.onTouchEvent(ev);
    117     }
    118 
    119     @Override
    120     public boolean onHoverEvent(@NonNull MotionEvent ev) {
    121         final int action = ev.getActionMasked();
    122         if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) {
    123             // This may be transitioning to TOUCH_DOWN. Postpone drawable state
    124             // updates until either the next frame or the next touch event.
    125             mResolveHoverRunnable = new ResolveHoverRunnable();
    126             mResolveHoverRunnable.post();
    127         }
    128 
    129         // Allow the super class to handle hover state management first.
    130         final boolean handled = super.onHoverEvent(ev);
    131 
    132         if (action == MotionEvent.ACTION_HOVER_ENTER
    133                 || action == MotionEvent.ACTION_HOVER_MOVE) {
    134             final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
    135             if (position != INVALID_POSITION && position != mSelectedPosition) {
    136                 final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
    137                 if (hoveredItem.isEnabled()) {
    138                     // Force a focus so that the proper selector state gets
    139                     // used when we update.
    140                     requestFocus();
    141 
    142                     positionSelector(position, hoveredItem);
    143                     setSelectedPositionInt(position);
    144                     setNextSelectedPositionInt(position);
    145                 }
    146                 updateSelectorState();
    147             }
    148         } else {
    149             // Do not cancel the selected position if the selection is visible
    150             // by other means.
    151             if (!super.shouldShowSelector()) {
    152                 setSelectedPositionInt(INVALID_POSITION);
    153                 setNextSelectedPositionInt(INVALID_POSITION);
    154             }
    155         }
    156 
    157         return handled;
    158     }
    159 
    160     @Override
    161     protected void drawableStateChanged() {
    162         if (mResolveHoverRunnable == null) {
    163             super.drawableStateChanged();
    164         }
    165     }
    166 
    167     /**
    168      * Handles forwarded events.
    169      *
    170      * @param activePointerId id of the pointer that activated forwarding
    171      * @return whether the event was handled
    172      */
    173     public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) {
    174         boolean handledEvent = true;
    175         boolean clearPressedItem = false;
    176 
    177         final int actionMasked = event.getActionMasked();
    178         switch (actionMasked) {
    179             case MotionEvent.ACTION_CANCEL:
    180                 handledEvent = false;
    181                 break;
    182             case MotionEvent.ACTION_UP:
    183                 handledEvent = false;
    184                 // $FALL-THROUGH$
    185             case MotionEvent.ACTION_MOVE:
    186                 final int activeIndex = event.findPointerIndex(activePointerId);
    187                 if (activeIndex < 0) {
    188                     handledEvent = false;
    189                     break;
    190                 }
    191 
    192                 final int x = (int) event.getX(activeIndex);
    193                 final int y = (int) event.getY(activeIndex);
    194                 final int position = pointToPosition(x, y);
    195                 if (position == INVALID_POSITION) {
    196                     clearPressedItem = true;
    197                     break;
    198                 }
    199 
    200                 final View child = getChildAt(position - getFirstVisiblePosition());
    201                 setPressedItem(child, position, x, y);
    202                 handledEvent = true;
    203 
    204                 if (actionMasked == MotionEvent.ACTION_UP) {
    205                     final long id = getItemIdAtPosition(position);
    206                     performItemClick(child, position, id);
    207                 }
    208                 break;
    209         }
    210 
    211         // Failure to handle the event cancels forwarding.
    212         if (!handledEvent || clearPressedItem) {
    213             clearPressedItem();
    214         }
    215 
    216         // Manage automatic scrolling.
    217         if (handledEvent) {
    218             if (mScrollHelper == null) {
    219                 mScrollHelper = new AbsListViewAutoScroller(this);
    220             }
    221             mScrollHelper.setEnabled(true);
    222             mScrollHelper.onTouch(this, event);
    223         } else if (mScrollHelper != null) {
    224             mScrollHelper.setEnabled(false);
    225         }
    226 
    227         return handledEvent;
    228     }
    229 
    230     /**
    231      * Sets whether the list selection is hidden, as part of a workaround for a
    232      * touch mode issue (see the declaration for mListSelectionHidden).
    233      *
    234      * @param hideListSelection {@code true} to hide list selection,
    235      *                          {@code false} to show
    236      */
    237     public void setListSelectionHidden(boolean hideListSelection) {
    238         mListSelectionHidden = hideListSelection;
    239     }
    240 
    241     private void clearPressedItem() {
    242         mDrawsInPressedState = false;
    243         setPressed(false);
    244         updateSelectorState();
    245 
    246         final View motionView = getChildAt(mMotionPosition - mFirstPosition);
    247         if (motionView != null) {
    248             motionView.setPressed(false);
    249         }
    250     }
    251 
    252     private void setPressedItem(@NonNull View child, int position, float x, float y) {
    253         mDrawsInPressedState = true;
    254 
    255         // Ordering is essential. First, update the container's pressed state.
    256         drawableHotspotChanged(x, y);
    257         if (!isPressed()) {
    258             setPressed(true);
    259         }
    260 
    261         // Next, run layout if we need to stabilize child positions.
    262         if (mDataChanged) {
    263             layoutChildren();
    264         }
    265 
    266         // Manage the pressed view based on motion position. This allows us to
    267         // play nicely with actual touch and scroll events.
    268         final View motionView = getChildAt(mMotionPosition - mFirstPosition);
    269         if (motionView != null && motionView != child && motionView.isPressed()) {
    270             motionView.setPressed(false);
    271         }
    272         mMotionPosition = position;
    273 
    274         // Offset for child coordinates.
    275         final float childX = x - child.getLeft();
    276         final float childY = y - child.getTop();
    277         child.drawableHotspotChanged(childX, childY);
    278         if (!child.isPressed()) {
    279             child.setPressed(true);
    280         }
    281 
    282         // Ensure that keyboard focus starts from the last touched position.
    283         setSelectedPositionInt(position);
    284         positionSelectorLikeTouch(position, child, x, y);
    285 
    286         // Refresh the drawable state to reflect the new pressed state,
    287         // which will also update the selector state.
    288         refreshDrawableState();
    289     }
    290 
    291     @Override
    292     boolean touchModeDrawsInPressedState() {
    293         return mDrawsInPressedState || super.touchModeDrawsInPressedState();
    294     }
    295 
    296     /**
    297      * Avoids jarring scrolling effect by ensuring that list elements
    298      * made of a text view fit on a single line.
    299      *
    300      * @param position the item index in the list to get a view for
    301      * @return the view for the specified item
    302      */
    303     @Override
    304     View obtainView(int position, boolean[] isScrap) {
    305         View view = super.obtainView(position, isScrap);
    306 
    307         if (view instanceof TextView) {
    308             ((TextView) view).setHorizontallyScrolling(true);
    309         }
    310 
    311         return view;
    312     }
    313 
    314     @Override
    315     public boolean isInTouchMode() {
    316         // WARNING: Please read the comment where mListSelectionHidden is declared
    317         return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
    318     }
    319 
    320     /**
    321      * Returns the focus state in the drop down.
    322      *
    323      * @return true always if hijacking focus
    324      */
    325     @Override
    326     public boolean hasWindowFocus() {
    327         return mHijackFocus || super.hasWindowFocus();
    328     }
    329 
    330     /**
    331      * Returns the focus state in the drop down.
    332      *
    333      * @return true always if hijacking focus
    334      */
    335     @Override
    336     public boolean isFocused() {
    337         return mHijackFocus || super.isFocused();
    338     }
    339 
    340     /**
    341      * Returns the focus state in the drop down.
    342      *
    343      * @return true always if hijacking focus
    344      */
    345     @Override
    346     public boolean hasFocus() {
    347         return mHijackFocus || super.hasFocus();
    348     }
    349 
    350     /**
    351      * Runnable that forces hover event resolution and updates drawable state.
    352      */
    353     private class ResolveHoverRunnable implements Runnable {
    354         @Override
    355         public void run() {
    356             // Resolved hover event as standard hover exit.
    357             mResolveHoverRunnable = null;
    358             drawableStateChanged();
    359         }
    360 
    361         public void cancel() {
    362             mResolveHoverRunnable = null;
    363             removeCallbacks(this);
    364         }
    365 
    366         public void post() {
    367             DropDownListView.this.post(this);
    368         }
    369     }
    370 }