DropDownListView.java revision be91ad5e21d917c3e9d5eff3bbd30d6df76c8213
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
17package android.widget;
18
19
20import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
21
22import android.annotation.NonNull;
23import android.content.Context;
24import android.view.MotionEvent;
25import android.view.View;
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 */
35public 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     * Creates a new list view wrapper.
79     *
80     * @param context this view's context
81     */
82    public DropDownListView(@NonNull Context context, boolean hijackFocus) {
83        this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
84    }
85
86    /**
87     * Creates a new list view wrapper.
88     *
89     * @param context this view's context
90     */
91    public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) {
92        super(context, null, defStyleAttr);
93        mHijackFocus = hijackFocus;
94        // TODO: Add an API to control this
95        setCacheColorHint(0); // Transparent, since the background drawable could be anything.
96    }
97
98    @Override
99    boolean shouldShowSelector() {
100        return isHovered() || super.shouldShowSelector();
101    }
102
103    @Override
104    public boolean onHoverEvent(@NonNull MotionEvent ev) {
105        // Allow the super class to handle hover state management first.
106        final boolean handled = super.onHoverEvent(ev);
107
108        final int action = ev.getActionMasked();
109        if (action == MotionEvent.ACTION_HOVER_ENTER
110                || action == MotionEvent.ACTION_HOVER_MOVE) {
111            final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
112            if (position != INVALID_POSITION && position != mSelectedPosition) {
113                final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
114                if (hoveredItem.isEnabled()) {
115                    // Force a focus so that the proper selector state gets used when we update.
116                    requestFocus();
117
118                    positionSelector(position, hoveredItem);
119                    setSelectedPositionInt(position);
120                    setNextSelectedPositionInt(position);
121                }
122                updateSelectorState();
123            }
124        } else {
125            // Do not cancel the selected position if the selection is visible by other reasons.
126            if (!super.shouldShowSelector()) {
127                setSelectedPositionInt(INVALID_POSITION);
128                setNextSelectedPositionInt(INVALID_POSITION);
129            }
130        }
131
132        return handled;
133    }
134
135    /**
136     * Handles forwarded events.
137     *
138     * @param activePointerId id of the pointer that activated forwarding
139     * @return whether the event was handled
140     */
141    public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) {
142        boolean handledEvent = true;
143        boolean clearPressedItem = false;
144
145        final int actionMasked = event.getActionMasked();
146        switch (actionMasked) {
147            case MotionEvent.ACTION_CANCEL:
148                handledEvent = false;
149                break;
150            case MotionEvent.ACTION_UP:
151                handledEvent = false;
152                // $FALL-THROUGH$
153            case MotionEvent.ACTION_MOVE:
154                final int activeIndex = event.findPointerIndex(activePointerId);
155                if (activeIndex < 0) {
156                    handledEvent = false;
157                    break;
158                }
159
160                final int x = (int) event.getX(activeIndex);
161                final int y = (int) event.getY(activeIndex);
162                final int position = pointToPosition(x, y);
163                if (position == INVALID_POSITION) {
164                    clearPressedItem = true;
165                    break;
166                }
167
168                final View child = getChildAt(position - getFirstVisiblePosition());
169                setPressedItem(child, position, x, y);
170                handledEvent = true;
171
172                if (actionMasked == MotionEvent.ACTION_UP) {
173                    final long id = getItemIdAtPosition(position);
174                    performItemClick(child, position, id);
175                }
176                break;
177        }
178
179        // Failure to handle the event cancels forwarding.
180        if (!handledEvent || clearPressedItem) {
181            clearPressedItem();
182        }
183
184        // Manage automatic scrolling.
185        if (handledEvent) {
186            if (mScrollHelper == null) {
187                mScrollHelper = new AbsListViewAutoScroller(this);
188            }
189            mScrollHelper.setEnabled(true);
190            mScrollHelper.onTouch(this, event);
191        } else if (mScrollHelper != null) {
192            mScrollHelper.setEnabled(false);
193        }
194
195        return handledEvent;
196    }
197
198    /**
199     * Sets whether the list selection is hidden, as part of a workaround for a touch mode issue
200     * (see the declaration for mListSelectionHidden).
201     * @param listSelectionHidden
202     */
203    public void setListSelectionHidden(boolean listSelectionHidden) {
204        this.mListSelectionHidden = listSelectionHidden;
205    }
206
207    private void clearPressedItem() {
208        mDrawsInPressedState = false;
209        setPressed(false);
210        updateSelectorState();
211
212        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
213        if (motionView != null) {
214            motionView.setPressed(false);
215        }
216    }
217
218    private void setPressedItem(@NonNull View child, int position, float x, float y) {
219        mDrawsInPressedState = true;
220
221        // Ordering is essential. First, update the container's pressed state.
222        drawableHotspotChanged(x, y);
223        if (!isPressed()) {
224            setPressed(true);
225        }
226
227        // Next, run layout if we need to stabilize child positions.
228        if (mDataChanged) {
229            layoutChildren();
230        }
231
232        // Manage the pressed view based on motion position. This allows us to
233        // play nicely with actual touch and scroll events.
234        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
235        if (motionView != null && motionView != child && motionView.isPressed()) {
236            motionView.setPressed(false);
237        }
238        mMotionPosition = position;
239
240        // Offset for child coordinates.
241        final float childX = x - child.getLeft();
242        final float childY = y - child.getTop();
243        child.drawableHotspotChanged(childX, childY);
244        if (!child.isPressed()) {
245            child.setPressed(true);
246        }
247
248        // Ensure that keyboard focus starts from the last touched position.
249        setSelectedPositionInt(position);
250        positionSelectorLikeTouch(position, child, x, y);
251
252        // Refresh the drawable state to reflect the new pressed state,
253        // which will also update the selector state.
254        refreshDrawableState();
255    }
256
257    @Override
258    boolean touchModeDrawsInPressedState() {
259        return mDrawsInPressedState || super.touchModeDrawsInPressedState();
260    }
261
262    /**
263     * Avoids jarring scrolling effect by ensuring that list elements
264     * made of a text view fit on a single line.
265     *
266     * @param position the item index in the list to get a view for
267     * @return the view for the specified item
268     */
269    @Override
270    View obtainView(int position, boolean[] isScrap) {
271        View view = super.obtainView(position, isScrap);
272
273        if (view instanceof TextView) {
274            ((TextView) view).setHorizontallyScrolling(true);
275        }
276
277        return view;
278    }
279
280    @Override
281    public boolean isInTouchMode() {
282        // WARNING: Please read the comment where mListSelectionHidden is declared
283        return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
284    }
285
286    /**
287     * Returns the focus state in the drop down.
288     *
289     * @return true always if hijacking focus
290     */
291    @Override
292    public boolean hasWindowFocus() {
293        return mHijackFocus || super.hasWindowFocus();
294    }
295
296    /**
297     * Returns the focus state in the drop down.
298     *
299     * @return true always if hijacking focus
300     */
301    @Override
302    public boolean isFocused() {
303        return mHijackFocus || super.isFocused();
304    }
305
306    /**
307     * Returns the focus state in the drop down.
308     *
309     * @return true always if hijacking focus
310     */
311    @Override
312    public boolean hasFocus() {
313        return mHijackFocus || super.hasFocus();
314    }
315}