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