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