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