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