1/*
2 * Copyright (C) 2014 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.support.v7.widget;
18
19import static android.support.annotation.RestrictTo.Scope.LIBRARY_GROUP;
20
21import android.content.Context;
22import android.graphics.Canvas;
23import android.graphics.Rect;
24import android.graphics.drawable.Drawable;
25import android.support.annotation.RestrictTo;
26import android.support.v4.graphics.drawable.DrawableCompat;
27import android.support.v7.graphics.drawable.DrawableWrapper;
28import android.util.AttributeSet;
29import android.view.MotionEvent;
30import android.view.View;
31import android.view.ViewGroup;
32import android.widget.AbsListView;
33import android.widget.ListAdapter;
34import android.widget.ListView;
35
36import java.lang.reflect.Field;
37
38/**
39 * This class contains a number of useful things for ListView. Mainly used by
40 * {@link android.support.v7.widget.ListPopupWindow}.
41 *
42 * @hide
43 */
44@RestrictTo(LIBRARY_GROUP)
45public class ListViewCompat extends ListView {
46
47    public static final int INVALID_POSITION = -1;
48    public static final int NO_POSITION = -1;
49
50    private static final int[] STATE_SET_NOTHING = new int[] { 0 };
51
52    final Rect mSelectorRect = new Rect();
53    int mSelectionLeftPadding = 0;
54    int mSelectionTopPadding = 0;
55    int mSelectionRightPadding = 0;
56    int mSelectionBottomPadding = 0;
57
58    protected int mMotionPosition;
59
60    private Field mIsChildViewEnabled;
61
62    private GateKeeperDrawable mSelector;
63
64    public ListViewCompat(Context context) {
65        this(context, null);
66    }
67
68    public ListViewCompat(Context context, AttributeSet attrs) {
69        this(context, attrs, 0);
70    }
71
72    public ListViewCompat(Context context, AttributeSet attrs, int defStyleAttr) {
73        super(context, attrs, defStyleAttr);
74
75        try {
76            mIsChildViewEnabled = AbsListView.class.getDeclaredField("mIsChildViewEnabled");
77            mIsChildViewEnabled.setAccessible(true);
78        } catch (NoSuchFieldException e) {
79            e.printStackTrace();
80        }
81    }
82
83    @Override
84    public void setSelector(Drawable sel) {
85        mSelector = sel != null ? new GateKeeperDrawable(sel) : null;
86        super.setSelector(mSelector);
87
88        final Rect padding = new Rect();
89        if (sel != null) {
90            sel.getPadding(padding);
91        }
92
93        mSelectionLeftPadding = padding.left;
94        mSelectionTopPadding = padding.top;
95        mSelectionRightPadding = padding.right;
96        mSelectionBottomPadding = padding.bottom;
97    }
98
99    @Override
100    protected void drawableStateChanged() {
101        super.drawableStateChanged();
102
103        setSelectorEnabled(true);
104        updateSelectorStateCompat();
105    }
106
107    @Override
108    protected void dispatchDraw(Canvas canvas) {
109        final boolean drawSelectorOnTop = false;
110        if (!drawSelectorOnTop) {
111            drawSelectorCompat(canvas);
112        }
113
114        super.dispatchDraw(canvas);
115    }
116
117    @Override
118    public boolean onTouchEvent(MotionEvent ev) {
119        switch (ev.getAction()) {
120            case MotionEvent.ACTION_DOWN:
121                mMotionPosition = pointToPosition((int) ev.getX(), (int) ev.getY());
122                break;
123        }
124        return super.onTouchEvent(ev);
125    }
126
127    protected void updateSelectorStateCompat() {
128        Drawable selector = getSelector();
129        if (selector != null && shouldShowSelectorCompat()) {
130            selector.setState(getDrawableState());
131        }
132    }
133
134    protected boolean shouldShowSelectorCompat() {
135        return touchModeDrawsInPressedStateCompat() && isPressed();
136    }
137
138    protected boolean touchModeDrawsInPressedStateCompat() {
139        return false;
140    }
141
142    protected void drawSelectorCompat(Canvas canvas) {
143        if (!mSelectorRect.isEmpty()) {
144            final Drawable selector = getSelector();
145            if (selector != null) {
146                selector.setBounds(mSelectorRect);
147                selector.draw(canvas);
148            }
149        }
150    }
151
152    /**
153     * Find a position that can be selected (i.e., is not a separator).
154     *
155     * @param position The starting position to look at.
156     * @param lookDown Whether to look down for other positions.
157     * @return The next selectable position starting at position and then searching either up or
158     *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
159     */
160    public int lookForSelectablePosition(int position, boolean lookDown) {
161        final ListAdapter adapter = getAdapter();
162        if (adapter == null || isInTouchMode()) {
163            return INVALID_POSITION;
164        }
165
166        final int count = adapter.getCount();
167        if (!getAdapter().areAllItemsEnabled()) {
168            if (lookDown) {
169                position = Math.max(0, position);
170                while (position < count && !adapter.isEnabled(position)) {
171                    position++;
172                }
173            } else {
174                position = Math.min(position, count - 1);
175                while (position >= 0 && !adapter.isEnabled(position)) {
176                    position--;
177                }
178            }
179
180            if (position < 0 || position >= count) {
181                return INVALID_POSITION;
182            }
183            return position;
184        } else {
185            if (position < 0 || position >= count) {
186                return INVALID_POSITION;
187            }
188            return position;
189        }
190    }
191
192    protected void positionSelectorLikeTouchCompat(int position, View sel, float x, float y) {
193        positionSelectorLikeFocusCompat(position, sel);
194
195        Drawable selector = getSelector();
196        if (selector != null && position != INVALID_POSITION) {
197            DrawableCompat.setHotspot(selector, x, y);
198        }
199    }
200
201    protected void positionSelectorLikeFocusCompat(int position, View sel) {
202        // If we're changing position, update the visibility since the selector
203        // is technically being detached from the previous selection.
204        final Drawable selector = getSelector();
205        final boolean manageState = selector != null && position != INVALID_POSITION;
206        if (manageState) {
207            selector.setVisible(false, false);
208        }
209
210        positionSelectorCompat(position, sel);
211
212        if (manageState) {
213            final Rect bounds = mSelectorRect;
214            final float x = bounds.exactCenterX();
215            final float y = bounds.exactCenterY();
216            selector.setVisible(getVisibility() == VISIBLE, false);
217            DrawableCompat.setHotspot(selector, x, y);
218        }
219    }
220
221    protected void positionSelectorCompat(int position, View sel) {
222        final Rect selectorRect = mSelectorRect;
223        selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
224
225        // Adjust for selection padding.
226        selectorRect.left -= mSelectionLeftPadding;
227        selectorRect.top -= mSelectionTopPadding;
228        selectorRect.right += mSelectionRightPadding;
229        selectorRect.bottom += mSelectionBottomPadding;
230
231        try {
232            // AbsListView.mIsChildViewEnabled controls the selector's state so we need to
233            // modify its value
234            final boolean isChildViewEnabled = mIsChildViewEnabled.getBoolean(this);
235            if (sel.isEnabled() != isChildViewEnabled) {
236                mIsChildViewEnabled.set(this, !isChildViewEnabled);
237                if (position != INVALID_POSITION) {
238                    refreshDrawableState();
239                }
240            }
241        } catch (IllegalAccessException e) {
242            e.printStackTrace();
243        }
244    }
245
246    /**
247     * Measures the height of the given range of children (inclusive) and returns the height
248     * with this ListView's padding and divider heights included. If maxHeight is provided, the
249     * measuring will stop when the current height reaches maxHeight.
250     *
251     * @param widthMeasureSpec             The width measure spec to be given to a child's
252     *                                     {@link View#measure(int, int)}.
253     * @param startPosition                The position of the first child to be shown.
254     * @param endPosition                  The (inclusive) position of the last child to be
255     *                                     shown. Specify {@link #NO_POSITION} if the last child
256     *                                     should be the last available child from the adapter.
257     * @param maxHeight                    The maximum height that will be returned (if all the
258     *                                     children don't fit in this value, this value will be
259     *                                     returned).
260     * @param disallowPartialChildPosition In general, whether the returned height should only
261     *                                     contain entire children. This is more powerful--it is
262     *                                     the first inclusive position at which partial
263     *                                     children will not be allowed. Example: it looks nice
264     *                                     to have at least 3 completely visible children, and
265     *                                     in portrait this will most likely fit; but in
266     *                                     landscape there could be times when even 2 children
267     *                                     can not be completely shown, so a value of 2
268     *                                     (remember, inclusive) would be good (assuming
269     *                                     startPosition is 0).
270     * @return The height of this ListView with the given children.
271     */
272    public int measureHeightOfChildrenCompat(int widthMeasureSpec, int startPosition,
273            int endPosition, final int maxHeight,
274            int disallowPartialChildPosition) {
275
276        final int paddingTop = getListPaddingTop();
277        final int paddingBottom = getListPaddingBottom();
278        final int paddingLeft = getListPaddingLeft();
279        final int paddingRight = getListPaddingRight();
280        final int reportedDividerHeight = getDividerHeight();
281        final Drawable divider = getDivider();
282
283        final ListAdapter adapter = getAdapter();
284
285        if (adapter == null) {
286            return paddingTop + paddingBottom;
287        }
288
289        // Include the padding of the list
290        int returnedHeight = paddingTop + paddingBottom;
291        final int dividerHeight = ((reportedDividerHeight > 0) && divider != null)
292                ? reportedDividerHeight : 0;
293
294        // The previous height value that was less than maxHeight and contained
295        // no partial children
296        int prevHeightWithoutPartialChild = 0;
297
298        View child = null;
299        int viewType = 0;
300        int count = adapter.getCount();
301        for (int i = 0; i < count; i++) {
302            int newType = adapter.getItemViewType(i);
303            if (newType != viewType) {
304                child = null;
305                viewType = newType;
306            }
307            child = adapter.getView(i, child, this);
308
309            // Compute child height spec
310            int heightMeasureSpec;
311            ViewGroup.LayoutParams childLp = child.getLayoutParams();
312
313            if (childLp == null) {
314                childLp = generateDefaultLayoutParams();
315                child.setLayoutParams(childLp);
316            }
317
318            if (childLp.height > 0) {
319                heightMeasureSpec = MeasureSpec.makeMeasureSpec(childLp.height,
320                        MeasureSpec.EXACTLY);
321            } else {
322                heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
323            }
324            child.measure(widthMeasureSpec, heightMeasureSpec);
325
326            // Since this view was measured directly against the parent measure
327            // spec, we must measure it again before reuse.
328            child.forceLayout();
329
330            if (i > 0) {
331                // Count the divider for all but one child
332                returnedHeight += dividerHeight;
333            }
334
335            returnedHeight += child.getMeasuredHeight();
336
337            if (returnedHeight >= maxHeight) {
338                // We went over, figure out which height to return.  If returnedHeight >
339                // maxHeight, then the i'th position did not fit completely.
340                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
341                        && (i > disallowPartialChildPosition) // We've past the min pos
342                        && (prevHeightWithoutPartialChild > 0) // We have a prev height
343                        && (returnedHeight != maxHeight) // i'th child did not fit completely
344                        ? prevHeightWithoutPartialChild
345                        : maxHeight;
346            }
347
348            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
349                prevHeightWithoutPartialChild = returnedHeight;
350            }
351        }
352
353        // At this point, we went through the range of children, and they each
354        // completely fit, so return the returnedHeight
355        return returnedHeight;
356    }
357
358    protected void setSelectorEnabled(boolean enabled) {
359        if (mSelector != null) {
360            mSelector.setEnabled(enabled);
361        }
362    }
363
364    private static class GateKeeperDrawable extends DrawableWrapper {
365        private boolean mEnabled;
366
367        public GateKeeperDrawable(Drawable drawable) {
368            super(drawable);
369            mEnabled = true;
370        }
371
372        void setEnabled(boolean enabled) {
373            mEnabled = enabled;
374        }
375
376        @Override
377        public boolean setState(int[] stateSet) {
378            if (mEnabled) {
379                return super.setState(stateSet);
380            }
381            return false;
382        }
383
384        @Override
385        public void draw(Canvas canvas) {
386            if (mEnabled) {
387                super.draw(canvas);
388            }
389        }
390
391        @Override
392        public void setHotspot(float x, float y) {
393            if (mEnabled) {
394                super.setHotspot(x, y);
395            }
396        }
397
398        @Override
399        public void setHotspotBounds(int left, int top, int right, int bottom) {
400            if (mEnabled) {
401                super.setHotspotBounds(left, top, right, bottom);
402            }
403        }
404
405        @Override
406        public boolean setVisible(boolean visible, boolean restart) {
407            if (mEnabled) {
408                return super.setVisible(visible, restart);
409            }
410            return false;
411        }
412    }
413}
414