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