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