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