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