BaseGridView.java revision b23ee09bf4aee03bc403abf39016c8ca5ecf301c
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.widget; 15 16import android.content.Context; 17import android.content.res.TypedArray; 18import android.graphics.Rect; 19import android.support.v17.leanback.R; 20import android.support.v7.widget.RecyclerView; 21import android.util.AttributeSet; 22import android.view.Gravity; 23import android.view.KeyEvent; 24import android.view.MotionEvent; 25import android.view.View; 26 27/** 28 * Base class for vertically and horizontally scrolling lists. The items come 29 * from the {@link RecyclerView.Adapter} associated with this view. 30 * @hide 31 */ 32abstract class BaseGridView extends RecyclerView { 33 34 /** 35 * Always keep focused item at a aligned position. Developer can use 36 * WINDOW_ALIGN_XXX and ITEM_ALIGN_XXX to define how focused item is aligned. 37 * In this mode, the last focused position will be remembered and restored when focus 38 * is back to the view. 39 */ 40 public final static int FOCUS_SCROLL_ALIGNED = 0; 41 42 /** 43 * Scroll to make the focused item inside client area. 44 */ 45 public final static int FOCUS_SCROLL_ITEM = 1; 46 47 /** 48 * Scroll a page of items when focusing to item outside the client area. 49 * The page size matches the client area size of RecyclerView. 50 */ 51 public final static int FOCUS_SCROLL_PAGE = 2; 52 53 /** 54 * The first item is aligned with the low edge of the viewport. When 55 * navigating away from the first item, the focus maintains a middle 56 * location. 57 * <p> 58 * The middle location is calculated by "windowAlignOffset" and 59 * "windowAlignOffsetPercent"; if neither of these two is defined, the 60 * default value is 1/2 of the size. 61 */ 62 public final static int WINDOW_ALIGN_LOW_EDGE = 1; 63 64 /** 65 * The last item is aligned with the high edge of the viewport when 66 * navigating to the end of list. When navigating away from the end, the 67 * focus maintains a middle location. 68 * <p> 69 * The middle location is calculated by "windowAlignOffset" and 70 * "windowAlignOffsetPercent"; if neither of these two is defined, the 71 * default value is 1/2 of the size. 72 */ 73 public final static int WINDOW_ALIGN_HIGH_EDGE = 1 << 1; 74 75 /** 76 * The first item and last item are aligned with the two edges of the 77 * viewport. When navigating in the middle of list, the focus maintains a 78 * middle location. 79 * <p> 80 * The middle location is calculated by "windowAlignOffset" and 81 * "windowAlignOffsetPercent"; if neither of these two is defined, the 82 * default value is 1/2 of the size. 83 */ 84 public final static int WINDOW_ALIGN_BOTH_EDGE = 85 WINDOW_ALIGN_LOW_EDGE | WINDOW_ALIGN_HIGH_EDGE; 86 87 /** 88 * The focused item always stays in a middle location. 89 * <p> 90 * The middle location is calculated by "windowAlignOffset" and 91 * "windowAlignOffsetPercent"; if neither of these two is defined, the 92 * default value is 1/2 of the size. 93 */ 94 public final static int WINDOW_ALIGN_NO_EDGE = 0; 95 96 /** 97 * Value indicates that percent is not used. 98 */ 99 public final static float WINDOW_ALIGN_OFFSET_PERCENT_DISABLED = -1; 100 101 /** 102 * Value indicates that percent is not used. 103 */ 104 public final static float ITEM_ALIGN_OFFSET_PERCENT_DISABLED = -1; 105 106 /** 107 * Listener for intercepting touch dispatch events. 108 */ 109 public interface OnTouchInterceptListener { 110 /** 111 * Returns true if the touch dispatch event should be consumed. 112 */ 113 public boolean onInterceptTouchEvent(MotionEvent event); 114 } 115 116 /** 117 * Listener for intercepting generic motion dispatch events. 118 */ 119 public interface OnMotionInterceptListener { 120 /** 121 * Returns true if the touch dispatch event should be consumed. 122 */ 123 public boolean onInterceptMotionEvent(MotionEvent event); 124 } 125 126 /** 127 * Listener for intercepting key dispatch events. 128 */ 129 public interface OnKeyInterceptListener { 130 /** 131 * Returns true if the key dispatch event should be consumed. 132 */ 133 public boolean onInterceptKeyEvent(KeyEvent event); 134 } 135 136 protected final GridLayoutManager mLayoutManager; 137 138 /** 139 * Animate layout changes from a child resizing or adding/removing a child. 140 */ 141 private boolean mAnimateChildLayout = true; 142 143 private RecyclerView.ItemAnimator mSavedItemAnimator; 144 145 private OnTouchInterceptListener mOnTouchInterceptListener; 146 private OnMotionInterceptListener mOnMotionInterceptListener; 147 private OnKeyInterceptListener mOnKeyInterceptListener; 148 149 public BaseGridView(Context context, AttributeSet attrs, int defStyle) { 150 super(context, attrs, defStyle); 151 mLayoutManager = new GridLayoutManager(this); 152 setLayoutManager(mLayoutManager); 153 setDescendantFocusability(FOCUS_AFTER_DESCENDANTS); 154 setHasFixedSize(true); 155 setChildrenDrawingOrderEnabled(true); 156 setWillNotDraw(true); 157 setOverScrollMode(View.OVER_SCROLL_NEVER); 158 } 159 160 protected void initBaseGridViewAttributes(Context context, AttributeSet attrs) { 161 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.lbBaseGridView); 162 boolean throughFront = a.getBoolean(R.styleable.lbBaseGridView_focusOutFront, false); 163 boolean throughEnd = a.getBoolean(R.styleable.lbBaseGridView_focusOutEnd, false); 164 mLayoutManager.setFocusOutAllowed(throughFront, throughEnd); 165 mLayoutManager.setVerticalMargin( 166 a.getDimensionPixelSize(R.styleable.lbBaseGridView_verticalMargin, 0)); 167 mLayoutManager.setHorizontalMargin( 168 a.getDimensionPixelSize(R.styleable.lbBaseGridView_horizontalMargin, 0)); 169 if (a.hasValue(R.styleable.lbBaseGridView_android_gravity)) { 170 setGravity(a.getInt(R.styleable.lbBaseGridView_android_gravity, Gravity.NO_GRAVITY)); 171 } 172 a.recycle(); 173 } 174 175 /** 176 * Set the strategy used to scroll in response to item focus changing: 177 * <ul> 178 * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li> 179 * <li>{@link #FOCUS_SCROLL_ITEM}</li> 180 * <li>{@link #FOCUS_SCROLL_PAGE}</li> 181 * </ul> 182 */ 183 public void setFocusScrollStrategy(int scrollStrategy) { 184 if (scrollStrategy != FOCUS_SCROLL_ALIGNED && scrollStrategy != FOCUS_SCROLL_ITEM 185 && scrollStrategy != FOCUS_SCROLL_PAGE) { 186 throw new IllegalArgumentException("Invalid scrollStrategy"); 187 } 188 mLayoutManager.setFocusScrollStrategy(scrollStrategy); 189 requestLayout(); 190 } 191 192 /** 193 * Returns the strategy used to scroll in response to item focus changing. 194 * <ul> 195 * <li>{@link #FOCUS_SCROLL_ALIGNED} (default) </li> 196 * <li>{@link #FOCUS_SCROLL_ITEM}</li> 197 * <li>{@link #FOCUS_SCROLL_PAGE}</li> 198 * </ul> 199 */ 200 public int getFocusScrollStrategy() { 201 return mLayoutManager.getFocusScrollStrategy(); 202 } 203 204 /** 205 * Set how the focused item gets aligned in the view. 206 * 207 * @param windowAlignment {@link #WINDOW_ALIGN_BOTH_EDGE}, 208 * {@link #WINDOW_ALIGN_LOW_EDGE}, {@link #WINDOW_ALIGN_HIGH_EDGE} or 209 * {@link #WINDOW_ALIGN_NO_EDGE}. 210 */ 211 public void setWindowAlignment(int windowAlignment) { 212 mLayoutManager.setWindowAlignment(windowAlignment); 213 requestLayout(); 214 } 215 216 /** 217 * Get how the focused item gets aligned in the view. 218 * 219 * @return {@link #WINDOW_ALIGN_BOTH_EDGE}, {@link #WINDOW_ALIGN_LOW_EDGE}, 220 * {@link #WINDOW_ALIGN_HIGH_EDGE} or {@link #WINDOW_ALIGN_NO_EDGE}. 221 */ 222 public int getWindowAlignment() { 223 return mLayoutManager.getWindowAlignment(); 224 } 225 226 /** 227 * Set the absolute offset in pixels for window alignment. 228 * 229 * @param offset The number of pixels to offset. Can be negative for 230 * alignment from the high edge, or positive for alignment from the 231 * low edge. 232 */ 233 public void setWindowAlignmentOffset(int offset) { 234 mLayoutManager.setWindowAlignmentOffset(offset); 235 requestLayout(); 236 } 237 238 /** 239 * Get the absolute offset in pixels for window alignment. 240 * 241 * @return The number of pixels to offset. Will be negative for alignment 242 * from the high edge, or positive for alignment from the low edge. 243 * Default value is 0. 244 */ 245 public int getWindowAlignmentOffset() { 246 return mLayoutManager.getWindowAlignmentOffset(); 247 } 248 249 /** 250 * Set offset percent for window alignment in addition to {@link 251 * #getWindowAlignmentOffset()}. 252 * 253 * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the 254 * width from low edge. Use 255 * {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} to disable. 256 */ 257 public void setWindowAlignmentOffsetPercent(float offsetPercent) { 258 mLayoutManager.setWindowAlignmentOffsetPercent(offsetPercent); 259 requestLayout(); 260 } 261 262 /** 263 * Get offset percent for window alignment in addition to 264 * {@link #getWindowAlignmentOffset()}. 265 * 266 * @return Percentage to offset. E.g., 40 means 40% of the width from the 267 * low edge, or {@link #WINDOW_ALIGN_OFFSET_PERCENT_DISABLED} if 268 * disabled. Default value is 50. 269 */ 270 public float getWindowAlignmentOffsetPercent() { 271 return mLayoutManager.getWindowAlignmentOffsetPercent(); 272 } 273 274 /** 275 * Set the absolute offset in pixels for item alignment. 276 * 277 * @param offset The number of pixels to offset. Can be negative for 278 * alignment from the high edge, or positive for alignment from the 279 * low edge. 280 */ 281 public void setItemAlignmentOffset(int offset) { 282 mLayoutManager.setItemAlignmentOffset(offset); 283 requestLayout(); 284 } 285 286 /** 287 * Get the absolute offset in pixels for item alignment. 288 * 289 * @return The number of pixels to offset. Will be negative for alignment 290 * from the high edge, or positive for alignment from the low edge. 291 * Default value is 0. 292 */ 293 public int getItemAlignmentOffset() { 294 return mLayoutManager.getItemAlignmentOffset(); 295 } 296 297 /** 298 * Set to true if include padding in calculating item align offset. 299 * 300 * @param withPadding When it is true: we include left/top padding for positive 301 * item offset, include right/bottom padding for negative item offset. 302 */ 303 public void setItemAlignmentOffsetWithPadding(boolean withPadding) { 304 mLayoutManager.setItemAlignmentOffsetWithPadding(withPadding); 305 requestLayout(); 306 } 307 308 /** 309 * Returns true if include padding in calculating item align offset. 310 */ 311 public boolean isItemAlignmentOffsetWithPadding() { 312 return mLayoutManager.isItemAlignmentOffsetWithPadding(); 313 } 314 315 /** 316 * Set offset percent for item alignment in addition to {@link 317 * #getItemAlignmentOffset()}. 318 * 319 * @param offsetPercent Percentage to offset. E.g., 40 means 40% of the 320 * width from the low edge. Use 321 * {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} to disable. 322 */ 323 public void setItemAlignmentOffsetPercent(float offsetPercent) { 324 mLayoutManager.setItemAlignmentOffsetPercent(offsetPercent); 325 requestLayout(); 326 } 327 328 /** 329 * Get offset percent for item alignment in addition to {@link 330 * #getItemAlignmentOffset()}. 331 * 332 * @return Percentage to offset. E.g., 40 means 40% of the width from the 333 * low edge, or {@link #ITEM_ALIGN_OFFSET_PERCENT_DISABLED} if 334 * disabled. Default value is 50. 335 */ 336 public float getItemAlignmentOffsetPercent() { 337 return mLayoutManager.getItemAlignmentOffsetPercent(); 338 } 339 340 /** 341 * Set the id of the view to align with. Use zero (default) for the item 342 * view itself. 343 */ 344 public void setItemAlignmentViewId(int viewId) { 345 mLayoutManager.setItemAlignmentViewId(viewId); 346 } 347 348 /** 349 * Get the id of the view to align with, or zero for the item view itself. 350 */ 351 public int getItemAlignmentViewId() { 352 return mLayoutManager.getItemAlignmentViewId(); 353 } 354 355 /** 356 * Set the margin in pixels between two child items. 357 */ 358 public void setItemMargin(int margin) { 359 mLayoutManager.setItemMargin(margin); 360 requestLayout(); 361 } 362 363 /** 364 * Set the margin in pixels between two child items vertically. 365 */ 366 public void setVerticalMargin(int margin) { 367 mLayoutManager.setVerticalMargin(margin); 368 requestLayout(); 369 } 370 371 /** 372 * Get the margin in pixels between two child items vertically. 373 */ 374 public int getVerticalMargin() { 375 return mLayoutManager.getVerticalMargin(); 376 } 377 378 /** 379 * Set the margin in pixels between two child items horizontally. 380 */ 381 public void setHorizontalMargin(int margin) { 382 mLayoutManager.setHorizontalMargin(margin); 383 requestLayout(); 384 } 385 386 /** 387 * Get the margin in pixels between two child items horizontally. 388 */ 389 public int getHorizontalMargin() { 390 return mLayoutManager.getHorizontalMargin(); 391 } 392 393 /** 394 * Register a callback to be invoked when an item in BaseGridView has 395 * been selected. 396 */ 397 public void setOnChildSelectedListener(OnChildSelectedListener listener) { 398 mLayoutManager.setOnChildSelectedListener(listener); 399 } 400 401 /** 402 * Change the selected item immediately without animation. 403 */ 404 public void setSelectedPosition(int position) { 405 mLayoutManager.setSelection(this, position); 406 } 407 408 /** 409 * Change the selected item and run an animation to scroll to the target 410 * position. 411 */ 412 public void setSelectedPositionSmooth(int position) { 413 mLayoutManager.setSelectionSmooth(this, position); 414 } 415 416 /** 417 * Get the selected item position. 418 */ 419 public int getSelectedPosition() { 420 return mLayoutManager.getSelection(); 421 } 422 423 /** 424 * Set if an animation should run when a child changes size or when adding 425 * or removing a child. 426 * <p><i>Unstable API, might change later.</i> 427 */ 428 public void setAnimateChildLayout(boolean animateChildLayout) { 429 if (mAnimateChildLayout != animateChildLayout) { 430 mAnimateChildLayout = animateChildLayout; 431 if (!mAnimateChildLayout) { 432 mSavedItemAnimator = getItemAnimator(); 433 super.setItemAnimator(null); 434 } else { 435 super.setItemAnimator(mSavedItemAnimator); 436 } 437 } 438 } 439 440 /** 441 * Return true if an animation will run when a child changes size or when 442 * adding or removing a child. 443 * <p><i>Unstable API, might change later.</i> 444 */ 445 public boolean isChildLayoutAnimated() { 446 return mAnimateChildLayout; 447 } 448 449 /** 450 * Describes how the child views are positioned. Defaults to 451 * GRAVITY_TOP|GRAVITY_LEFT. 452 * 453 * @param gravity See {@link android.view.Gravity} 454 */ 455 public void setGravity(int gravity) { 456 mLayoutManager.setGravity(gravity); 457 requestLayout(); 458 } 459 460 @Override 461 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 462 return mLayoutManager.gridOnRequestFocusInDescendants(this, direction, 463 previouslyFocusedRect); 464 } 465 466 /** 467 * Get the x/y offsets to final position from current position if the view 468 * is selected. 469 * 470 * @param view The view to get offsets. 471 * @param offsets offsets[0] holds offset of X, offsets[1] holds offset of 472 * Y. 473 */ 474 public void getViewSelectedOffsets(View view, int[] offsets) { 475 mLayoutManager.getViewSelectedOffsets(view, offsets); 476 } 477 478 @Override 479 public int getChildDrawingOrder(int childCount, int i) { 480 return mLayoutManager.getChildDrawingOrder(this, childCount, i); 481 } 482 483 final boolean isChildrenDrawingOrderEnabledInternal() { 484 return isChildrenDrawingOrderEnabled(); 485 } 486 487 /** 488 * Disable or enable focus search. 489 */ 490 public final void setFocusSearchDisabled(boolean disabled) { 491 mLayoutManager.setFocusSearchDisabled(disabled); 492 } 493 494 /** 495 * Return true if focus search is disabled. 496 */ 497 public final boolean isFocusSearchDisabled() { 498 return mLayoutManager.isFocusSearchDisabled(); 499 } 500 501 /** 502 * Enable or disable layout. All children will be removed when layout is 503 * disabled. 504 */ 505 public void setLayoutEnabled(boolean layoutEnabled) { 506 mLayoutManager.setLayoutEnabled(layoutEnabled); 507 } 508 509 /** 510 * Enable or disable pruning child. Disable is useful during transition. 511 */ 512 public void setPruneChild(boolean pruneChild) { 513 mLayoutManager.setPruneChild(pruneChild); 514 } 515 516 /** 517 * Returns true if the view at the given position has a same row sibling 518 * in front of it. 519 * 520 * @param position Position in adapter. 521 */ 522 public boolean hasPreviousViewInSameRow(int position) { 523 return mLayoutManager.hasPreviousViewInSameRow(position); 524 } 525 526 /** 527 * Enable or disable the default "focus draw at last" order rule. 528 */ 529 public void setFocusDrawingOrderEnabled(boolean enabled) { 530 super.setChildrenDrawingOrderEnabled(enabled); 531 } 532 533 /** 534 * Returns true if default "focus draw at last" order rule is enabled. 535 */ 536 public boolean isFocusDrawingOrderEnabled() { 537 return super.isChildrenDrawingOrderEnabled(); 538 } 539 540 /** 541 * Sets the touch intercept listener. 542 */ 543 public void setOnTouchInterceptListener(OnTouchInterceptListener listener) { 544 mOnTouchInterceptListener = listener; 545 } 546 547 /** 548 * Sets the generic motion intercept listener. 549 */ 550 public void setOnMotionInterceptListener(OnMotionInterceptListener listener) { 551 mOnMotionInterceptListener = listener; 552 } 553 554 /** 555 * Sets the key intercept listener. 556 */ 557 public void setOnKeyInterceptListener(OnKeyInterceptListener listener) { 558 mOnKeyInterceptListener = listener; 559 } 560 561 @Override 562 public boolean dispatchKeyEvent(KeyEvent event) { 563 if (mOnKeyInterceptListener != null) { 564 if (mOnKeyInterceptListener.onInterceptKeyEvent(event)) { 565 return true; 566 } 567 } 568 return super.dispatchKeyEvent(event); 569 } 570 571 @Override 572 public boolean dispatchTouchEvent(MotionEvent event) { 573 if (mOnTouchInterceptListener != null) { 574 if (mOnTouchInterceptListener.onInterceptTouchEvent(event)) { 575 return true; 576 } 577 } 578 return super.dispatchTouchEvent(event); 579 } 580 581 @Override 582 public boolean dispatchGenericFocusedEvent(MotionEvent event) { 583 if (mOnMotionInterceptListener != null) { 584 if (mOnMotionInterceptListener.onInterceptMotionEvent(event)) { 585 return true; 586 } 587 } 588 return super.dispatchGenericFocusedEvent(event); 589 } 590} 591