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