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