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