ListRowPresenter.java revision 60bb6af2e336072921f5d3c3861e86b3cc6241b3
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.support.v17.leanback.R; 19import android.support.v17.leanback.graphics.ColorOverlayDimmer; 20import android.util.Log; 21import android.view.KeyEvent; 22import android.view.View; 23import android.view.ViewGroup; 24import android.view.ViewGroup.LayoutParams; 25 26import java.util.HashMap; 27 28/** 29 * ListRowPresenter renders {@link ListRow} using a 30 * {@link HorizontalGridView} hosted in a {@link ListRowView}. 31 * 32 * <h3>Hover card</h3> 33 * Optionally, {@link #setHoverCardPresenterSelector(PresenterSelector)} can be used to 34 * display a view for the currently focused list item below the rendered 35 * list. This view is known as a hover card. 36 * 37 * <h3>Selection animation</h3> 38 * ListRowPresenter disables {@link RowPresenter}'s default dimming effect and draw 39 * a dim overlay on top of each individual child items. Subclass may override and disable 40 * {@link #isUsingDefaultListSelectEffect()} and write its own dim effect in 41 * {@link #onSelectLevelChanged(RowPresenter.ViewHolder)}. 42 * 43 * <h3>Shadow</h3> 44 * ListRowPresenter applies a default shadow to child of each view. Call 45 * {@link #setShadowEnabled(boolean)} to disable shadow. Subclass may override and return 46 * false in {@link #isUsingDefaultShadow()} and replace with its own shadow implementation. 47 */ 48public class ListRowPresenter extends RowPresenter { 49 50 private static final String TAG = "ListRowPresenter"; 51 private static final boolean DEBUG = false; 52 53 private static final int DEFAULT_RECYCLED_POOL_SIZE = 24; 54 55 public static class ViewHolder extends RowPresenter.ViewHolder { 56 final ListRowPresenter mListRowPresenter; 57 final HorizontalGridView mGridView; 58 ItemBridgeAdapter mItemBridgeAdapter; 59 final HorizontalHoverCardSwitcher mHoverCardViewSwitcher = new HorizontalHoverCardSwitcher(); 60 final int mPaddingTop; 61 final int mPaddingBottom; 62 final int mPaddingLeft; 63 final int mPaddingRight; 64 65 public ViewHolder(View rootView, HorizontalGridView gridView, ListRowPresenter p) { 66 super(rootView); 67 mGridView = gridView; 68 mListRowPresenter = p; 69 mPaddingTop = mGridView.getPaddingTop(); 70 mPaddingBottom = mGridView.getPaddingBottom(); 71 mPaddingLeft = mGridView.getPaddingLeft(); 72 mPaddingRight = mGridView.getPaddingRight(); 73 } 74 75 public final ListRowPresenter getListRowPresenter() { 76 return mListRowPresenter; 77 } 78 79 public final HorizontalGridView getGridView() { 80 return mGridView; 81 } 82 83 public final ItemBridgeAdapter getBridgeAdapter() { 84 return mItemBridgeAdapter; 85 } 86 } 87 88 class ListRowPresenterItemBridgeAdapter extends ItemBridgeAdapter { 89 ListRowPresenter.ViewHolder mRowViewHolder; 90 91 ListRowPresenterItemBridgeAdapter(ListRowPresenter.ViewHolder rowViewHolder) { 92 mRowViewHolder = rowViewHolder; 93 } 94 95 @Override 96 public void onBind(final ItemBridgeAdapter.ViewHolder viewHolder) { 97 // Only when having an OnItemClickListner, we will attach the OnClickListener. 98 if (getOnItemClickedListener() != null || getOnItemViewClickedListener() != null) { 99 viewHolder.mHolder.view.setOnClickListener(new View.OnClickListener() { 100 @Override 101 public void onClick(View v) { 102 ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder) 103 mRowViewHolder.mGridView.getChildViewHolder(viewHolder.itemView); 104 if (getOnItemClickedListener() != null) { 105 getOnItemClickedListener().onItemClicked(ibh.mItem, 106 (ListRow) mRowViewHolder.mRow); 107 } 108 if (getOnItemViewClickedListener() != null) { 109 getOnItemViewClickedListener().onItemClicked(viewHolder.mHolder, 110 ibh.mItem, mRowViewHolder, (ListRow) mRowViewHolder.mRow); 111 } 112 } 113 }); 114 } 115 } 116 117 @Override 118 public void onUnbind(ItemBridgeAdapter.ViewHolder viewHolder) { 119 if (getOnItemClickedListener() != null || getOnItemViewClickedListener() != null) { 120 viewHolder.mHolder.view.setOnClickListener(null); 121 } 122 } 123 124 @Override 125 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) { 126 if (viewHolder.itemView instanceof ShadowOverlayContainer) { 127 int dimmedColor = mRowViewHolder.mColorDimmer.getPaint().getColor(); 128 ((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor); 129 } 130 mRowViewHolder.syncActivatedStatus(viewHolder.itemView); 131 } 132 133 @Override 134 public void onAddPresenter(Presenter presenter, int type) { 135 mRowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews( 136 type, getRecycledPoolSize(presenter)); 137 } 138 } 139 140 private int mRowHeight; 141 private int mExpandedRowHeight; 142 private PresenterSelector mHoverCardPresenterSelector; 143 private int mZoomFactor; 144 private boolean mShadowEnabled = true; 145 private int mBrowseRowsFadingEdgeLength = -1; 146 private boolean mRoundedCornersEnabled = true; 147 private HashMap<Presenter, Integer> mRecycledPoolSize = new HashMap<Presenter, Integer>(); 148 149 private static int sSelectedRowTopPadding; 150 private static int sExpandedSelectedRowTopPadding; 151 private static int sExpandedRowNoHovercardBottomPadding; 152 153 /** 154 * Constructs a ListRowPresenter with defaults. 155 * Uses {@link FocusHighlight#ZOOM_FACTOR_MEDIUM} for focus zooming. 156 */ 157 public ListRowPresenter() { 158 this(FocusHighlight.ZOOM_FACTOR_MEDIUM); 159 } 160 161 /** 162 * Constructs a ListRowPresenter with the given parameters. 163 * 164 * @param zoomFactor Controls the zoom factor used when an item view is focused. One of 165 * {@link FocusHighlight#ZOOM_FACTOR_NONE}, 166 * {@link FocusHighlight#ZOOM_FACTOR_SMALL}, 167 * {@link FocusHighlight#ZOOM_FACTOR_XSMALL}, 168 * {@link FocusHighlight#ZOOM_FACTOR_MEDIUM}, 169 * {@link FocusHighlight#ZOOM_FACTOR_LARGE} 170 */ 171 public ListRowPresenter(int zoomFactor) { 172 if (!FocusHighlightHelper.isValidZoomIndex(zoomFactor)) { 173 throw new IllegalArgumentException("Unhandled zoom factor"); 174 } 175 mZoomFactor = zoomFactor; 176 } 177 178 /** 179 * Sets the row height for rows created by this Presenter. Rows 180 * created before calling this method will not be updated. 181 * 182 * @param rowHeight Row height in pixels, or WRAP_CONTENT, or 0 183 * to use the default height. 184 */ 185 public void setRowHeight(int rowHeight) { 186 mRowHeight = rowHeight; 187 } 188 189 /** 190 * Returns the row height for list rows created by this Presenter. 191 */ 192 public int getRowHeight() { 193 return mRowHeight; 194 } 195 196 /** 197 * Sets the expanded row height for rows created by this Presenter. 198 * If not set, expanded rows have the same height as unexpanded 199 * rows. 200 * 201 * @param rowHeight The row height in to use when the row is expanded, 202 * in pixels, or WRAP_CONTENT, or 0 to use the default. 203 */ 204 public void setExpandedRowHeight(int rowHeight) { 205 mExpandedRowHeight = rowHeight; 206 } 207 208 /** 209 * Returns the expanded row height for rows created by this Presenter. 210 */ 211 public int getExpandedRowHeight() { 212 return mExpandedRowHeight != 0 ? mExpandedRowHeight : mRowHeight; 213 } 214 215 /** 216 * Returns the zoom factor used for focus highlighting. 217 */ 218 public final int getZoomFactor() { 219 return mZoomFactor; 220 } 221 222 private ItemBridgeAdapter.Wrapper mCardWrapper = new ItemBridgeAdapter.Wrapper() { 223 @Override 224 public View createWrapper(View root) { 225 ShadowOverlayContainer wrapper = new ShadowOverlayContainer(root.getContext()); 226 wrapper.setLayoutParams( 227 new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); 228 wrapper.initialize(needsDefaultShadow(), 229 needsDefaultListSelectEffect(), 230 areChildRoundedCornersEnabled()); 231 return wrapper; 232 } 233 @Override 234 public void wrap(View wrapper, View wrapped) { 235 ((ShadowOverlayContainer) wrapper).wrap(wrapped); 236 } 237 }; 238 239 @Override 240 protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) { 241 super.initializeRowViewHolder(holder); 242 final ViewHolder rowViewHolder = (ViewHolder) holder; 243 rowViewHolder.mItemBridgeAdapter = new ListRowPresenterItemBridgeAdapter(rowViewHolder); 244 if (needsDefaultListSelectEffect() || needsDefaultShadow() 245 || areChildRoundedCornersEnabled()) { 246 rowViewHolder.mItemBridgeAdapter.setWrapper(mCardWrapper); 247 } 248 if (needsDefaultListSelectEffect()) { 249 ShadowOverlayContainer.prepareParentForShadow(rowViewHolder.mGridView); 250 } 251 FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, 252 mZoomFactor, false); 253 rowViewHolder.mGridView.setFocusDrawingOrderEnabled(!isUsingZOrder()); 254 rowViewHolder.mGridView.setOnChildSelectedListener( 255 new OnChildSelectedListener() { 256 @Override 257 public void onChildSelected(ViewGroup parent, View view, int position, long id) { 258 selectChildView(rowViewHolder, view); 259 } 260 }); 261 rowViewHolder.mGridView.setOnUnhandledKeyListener( 262 new BaseGridView.OnUnhandledKeyListener() { 263 @Override 264 public boolean onUnhandledKey(KeyEvent event) { 265 if (rowViewHolder.getOnKeyListener() != null && 266 rowViewHolder.getOnKeyListener().onKey( 267 rowViewHolder.view, event.getKeyCode(), event)) { 268 return true; 269 } 270 return false; 271 } 272 }); 273 } 274 275 final boolean needsDefaultListSelectEffect() { 276 return isUsingDefaultListSelectEffect() && getSelectEffectEnabled(); 277 } 278 279 /** 280 * Sets the recycled pool size for the given presenter. 281 */ 282 public void setRecycledPoolSize(Presenter presenter, int size) { 283 mRecycledPoolSize.put(presenter, size); 284 } 285 286 /** 287 * Returns the recycled pool size for the given presenter. 288 */ 289 public int getRecycledPoolSize(Presenter presenter) { 290 return mRecycledPoolSize.containsKey(presenter) ? mRecycledPoolSize.get(presenter) : 291 DEFAULT_RECYCLED_POOL_SIZE; 292 } 293 294 /** 295 * Set {@link PresenterSelector} used for showing a select object in a hover card. 296 */ 297 public final void setHoverCardPresenterSelector(PresenterSelector selector) { 298 mHoverCardPresenterSelector = selector; 299 } 300 301 /** 302 * Get {@link PresenterSelector} used for showing a select object in a hover card. 303 */ 304 public final PresenterSelector getHoverCardPresenterSelector() { 305 return mHoverCardPresenterSelector; 306 } 307 308 /* 309 * Perform operations when a child of horizontal grid view is selected. 310 */ 311 private void selectChildView(ViewHolder rowViewHolder, View view) { 312 if (view != null) { 313 if (rowViewHolder.mExpanded && rowViewHolder.mSelected) { 314 ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder) 315 rowViewHolder.mGridView.getChildViewHolder(view); 316 317 if (mHoverCardPresenterSelector != null) { 318 rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view, 319 ibh.mItem); 320 } 321 if (getOnItemViewSelectedListener() != null) { 322 getOnItemViewSelectedListener().onItemSelected(ibh.mHolder, ibh.mItem, 323 rowViewHolder, rowViewHolder.mRow); 324 } 325 if (getOnItemSelectedListener() != null) { 326 getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow); 327 } 328 } 329 } else { 330 if (mHoverCardPresenterSelector != null) { 331 rowViewHolder.mHoverCardViewSwitcher.unselect(); 332 } 333 if (getOnItemViewSelectedListener() != null) { 334 getOnItemViewSelectedListener().onItemSelected(null, null, 335 rowViewHolder, rowViewHolder.mRow); 336 } 337 if (getOnItemSelectedListener() != null) { 338 getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow); 339 } 340 } 341 } 342 343 private static void initStatics(Context context) { 344 if (sSelectedRowTopPadding == 0) { 345 sSelectedRowTopPadding = context.getResources().getDimensionPixelSize( 346 R.dimen.lb_browse_selected_row_top_padding); 347 sExpandedSelectedRowTopPadding = context.getResources().getDimensionPixelSize( 348 R.dimen.lb_browse_expanded_selected_row_top_padding); 349 sExpandedRowNoHovercardBottomPadding = context.getResources().getDimensionPixelSize( 350 R.dimen.lb_browse_expanded_row_no_hovercard_bottom_padding); 351 } 352 } 353 354 private int getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh) { 355 RowHeaderPresenter.ViewHolder headerViewHolder = vh.getHeaderViewHolder(); 356 if (headerViewHolder != null) { 357 if (getHeaderPresenter() != null) { 358 return getHeaderPresenter().getSpaceUnderBaseline(headerViewHolder); 359 } 360 return headerViewHolder.view.getPaddingBottom(); 361 } 362 return 0; 363 } 364 365 private void setVerticalPadding(ListRowPresenter.ViewHolder vh) { 366 int paddingTop, paddingBottom; 367 // Note: sufficient bottom padding needed for card shadows. 368 if (vh.isExpanded()) { 369 int headerSpaceUnderBaseline = getSpaceUnderBaseline(vh); 370 if (DEBUG) Log.v(TAG, "headerSpaceUnderBaseline " + headerSpaceUnderBaseline); 371 paddingTop = (vh.isSelected() ? sExpandedSelectedRowTopPadding : vh.mPaddingTop) - 372 headerSpaceUnderBaseline; 373 paddingBottom = mHoverCardPresenterSelector == null ? 374 sExpandedRowNoHovercardBottomPadding : vh.mPaddingBottom; 375 } else if (vh.isSelected()) { 376 paddingTop = sSelectedRowTopPadding - vh.mPaddingBottom; 377 paddingBottom = sSelectedRowTopPadding; 378 } else { 379 paddingTop = 0; 380 paddingBottom = vh.mPaddingBottom; 381 } 382 vh.getGridView().setPadding(vh.mPaddingLeft, paddingTop, vh.mPaddingRight, 383 paddingBottom); 384 } 385 386 @Override 387 protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { 388 initStatics(parent.getContext()); 389 ListRowView rowView = new ListRowView(parent.getContext()); 390 setupFadingEffect(rowView); 391 if (mRowHeight != 0) { 392 rowView.getGridView().setRowHeight(mRowHeight); 393 } 394 return new ViewHolder(rowView, rowView.getGridView(), this); 395 } 396 397 @Override 398 protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) { 399 super.onRowViewSelected(holder, selected); 400 ViewHolder vh = (ViewHolder) holder; 401 setVerticalPadding(vh); 402 updateFooterViewSwitcher(vh); 403 } 404 405 /* 406 * Show or hide hover card when row selection or expanded state is changed. 407 */ 408 private void updateFooterViewSwitcher(ViewHolder vh) { 409 if (vh.mExpanded && vh.mSelected) { 410 if (mHoverCardPresenterSelector != null) { 411 vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view, 412 mHoverCardPresenterSelector); 413 } 414 ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder) 415 vh.mGridView.findViewHolderForPosition( 416 vh.mGridView.getSelectedPosition()); 417 selectChildView(vh, ibh == null ? null : ibh.itemView); 418 } else { 419 if (mHoverCardPresenterSelector != null) { 420 vh.mHoverCardViewSwitcher.unselect(); 421 } 422 } 423 } 424 425 private void setupFadingEffect(ListRowView rowView) { 426 // content is completely faded at 1/2 padding of left, fading length is 1/2 of padding. 427 HorizontalGridView gridView = rowView.getGridView(); 428 if (mBrowseRowsFadingEdgeLength < 0) { 429 TypedArray ta = gridView.getContext() 430 .obtainStyledAttributes(R.styleable.LeanbackTheme); 431 mBrowseRowsFadingEdgeLength = (int) ta.getDimension( 432 R.styleable.LeanbackTheme_browseRowsFadingEdgeLength, 0); 433 ta.recycle(); 434 } 435 gridView.setFadingLeftEdgeLength(mBrowseRowsFadingEdgeLength); 436 } 437 438 @Override 439 protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) { 440 super.onRowViewExpanded(holder, expanded); 441 ViewHolder vh = (ViewHolder) holder; 442 if (getRowHeight() != getExpandedRowHeight()) { 443 int newHeight = expanded ? getExpandedRowHeight() : getRowHeight(); 444 vh.getGridView().setRowHeight(newHeight); 445 } 446 setVerticalPadding(vh); 447 updateFooterViewSwitcher(vh); 448 } 449 450 @Override 451 protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) { 452 super.onBindRowViewHolder(holder, item); 453 ViewHolder vh = (ViewHolder) holder; 454 ListRow rowItem = (ListRow) item; 455 vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter()); 456 vh.mGridView.setAdapter(vh.mItemBridgeAdapter); 457 } 458 459 @Override 460 protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) { 461 ViewHolder vh = (ViewHolder) holder; 462 vh.mGridView.setAdapter(null); 463 vh.mItemBridgeAdapter.clear(); 464 super.onUnbindRowViewHolder(holder); 465 } 466 467 /** 468 * ListRowPresenter overrides the default select effect of {@link RowPresenter} 469 * and return false. 470 */ 471 @Override 472 public final boolean isUsingDefaultSelectEffect() { 473 return false; 474 } 475 476 /** 477 * Returns true so that default select effect is applied to each individual 478 * child of {@link HorizontalGridView}. Subclass may return false to disable 479 * the default implementation. 480 * @see #onSelectLevelChanged(RowPresenter.ViewHolder) 481 */ 482 public boolean isUsingDefaultListSelectEffect() { 483 return true; 484 } 485 486 /** 487 * Returns true if SDK >= 18, where default shadow 488 * is applied to each individual child of {@link HorizontalGridView}. 489 * Subclass may return false to disable. 490 */ 491 public boolean isUsingDefaultShadow() { 492 return ShadowOverlayContainer.supportsShadow(); 493 } 494 495 /** 496 * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled 497 * on each child of horizontal list. If subclass returns false in isUsingDefaultShadow() 498 * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false. 499 */ 500 public boolean isUsingZOrder() { 501 return ShadowHelper.getInstance().usesZShadow(); 502 } 503 504 /** 505 * Enable or disable child shadow. 506 * This is not only for enable/disable default shadow implementation but also subclass must 507 * respect this flag. 508 */ 509 public final void setShadowEnabled(boolean enabled) { 510 mShadowEnabled = enabled; 511 } 512 513 /** 514 * Returns true if child shadow is enabled. 515 * This is not only for enable/disable default shadow implementation but also subclass must 516 * respect this flag. 517 */ 518 public final boolean getShadowEnabled() { 519 return mShadowEnabled; 520 } 521 522 /** 523 * Enables or disabled rounded corners on children of this row. 524 * Supported on Android SDK >= L. 525 */ 526 public final void enableChildRoundedCorners(boolean enable) { 527 mRoundedCornersEnabled = enable; 528 } 529 530 /** 531 * Returns true if rounded corners are enabled for children of this row. 532 */ 533 public final boolean areChildRoundedCornersEnabled() { 534 return mRoundedCornersEnabled; 535 } 536 537 final boolean needsDefaultShadow() { 538 return isUsingDefaultShadow() && getShadowEnabled(); 539 } 540 541 @Override 542 public boolean canDrawOutOfBounds() { 543 return needsDefaultShadow(); 544 } 545 546 /** 547 * Applies select level to header and draw a default color dim over each child 548 * of {@link HorizontalGridView}. 549 * <p> 550 * Subclass may override this method. A subclass 551 * needs to call super.onSelectLevelChanged() for applying header select level 552 * and optionally applying a default select level to each child view of 553 * {@link HorizontalGridView} if {@link #isUsingDefaultListSelectEffect()} 554 * is true. Subclass may override {@link #isUsingDefaultListSelectEffect()} to return 555 * false and deal with the individual item select level by itself. 556 * </p> 557 */ 558 @Override 559 protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) { 560 super.onSelectLevelChanged(holder); 561 if (needsDefaultListSelectEffect()) { 562 ViewHolder vh = (ViewHolder) holder; 563 int dimmedColor = vh.mColorDimmer.getPaint().getColor(); 564 for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) { 565 ShadowOverlayContainer wrapper = (ShadowOverlayContainer) vh.mGridView.getChildAt(i); 566 wrapper.setOverlayColor(dimmedColor); 567 } 568 if (vh.mGridView.getFadingLeftEdge()) { 569 vh.mGridView.invalidate(); 570 } 571 } 572 } 573 574 @Override 575 public void freeze(RowPresenter.ViewHolder holder, boolean freeze) { 576 ViewHolder vh = (ViewHolder) holder; 577 vh.mGridView.setScrollEnabled(!freeze); 578 } 579 580 @Override 581 public void setEntranceTransitionState(RowPresenter.ViewHolder holder, 582 boolean afterEntrance) { 583 super.setEntranceTransitionState(holder, afterEntrance); 584 ((ViewHolder) holder).mGridView.setChildrenVisibility( 585 afterEntrance? View.VISIBLE : View.INVISIBLE); 586 } 587} 588