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