ListRowPresenter.java revision 0fcad32410ad29031630bb9cc2da35d33a4aa906
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 ColorOverlayDimmer mColorDimmer; 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 mColorDimmer = ColorOverlayDimmer.createDefault(rootView.getContext()); 66 mPaddingTop = mGridView.getPaddingTop(); 67 mPaddingBottom = mGridView.getPaddingBottom(); 68 mPaddingLeft = mGridView.getPaddingLeft(); 69 mPaddingRight = mGridView.getPaddingRight(); 70 } 71 72 public final ListRowPresenter getListRowPresenter() { 73 return mListRowPresenter; 74 } 75 76 public final HorizontalGridView getGridView() { 77 return mGridView; 78 } 79 80 public final ItemBridgeAdapter getBridgeAdapter() { 81 return mItemBridgeAdapter; 82 } 83 } 84 85 private int mRowHeight; 86 private int mExpandedRowHeight; 87 private PresenterSelector mHoverCardPresenterSelector; 88 private int mZoomFactor; 89 private boolean mShadowEnabled = true; 90 private int mBrowseRowsFadingEdgeLength = -1; 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(), needsDefaultListSelectEffect()); 168 return wrapper; 169 } 170 @Override 171 public void wrap(View wrapper, View wrapped) { 172 ((ShadowOverlayContainer) wrapper).wrap(wrapped); 173 } 174 }; 175 176 @Override 177 protected void initializeRowViewHolder(RowPresenter.ViewHolder holder) { 178 super.initializeRowViewHolder(holder); 179 final ViewHolder rowViewHolder = (ViewHolder) holder; 180 if (needsDefaultListSelectEffect() || needsDefaultShadow()) { 181 rowViewHolder.mItemBridgeAdapter.setWrapper(mCardWrapper); 182 } 183 if (needsDefaultListSelectEffect()) { 184 ShadowOverlayContainer.prepareParentForShadow(rowViewHolder.mGridView); 185 ((ViewGroup) rowViewHolder.view).setClipChildren(false); 186 if (rowViewHolder.mContainerViewHolder != null) { 187 ((ViewGroup) rowViewHolder.mContainerViewHolder.view).setClipChildren(false); 188 } 189 } 190 FocusHighlightHelper.setupBrowseItemFocusHighlight(rowViewHolder.mItemBridgeAdapter, mZoomFactor); 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 onCreate(final ItemBridgeAdapter.ViewHolder viewHolder) { 203 // Only when having an OnItemClickListner, we will attach the OnClickListener. 204 if (getOnItemClickedListener() != 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.getChildViewHolder(viewHolder.itemView); 210 if (getOnItemClickedListener() != null) { 211 getOnItemClickedListener().onItemClicked(ibh.mItem, 212 (ListRow) rowViewHolder.mRow); 213 } 214 } 215 }); 216 } 217 } 218 219 @Override 220 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder viewHolder) { 221 if (viewHolder.itemView instanceof ShadowOverlayContainer) { 222 int dimmedColor = rowViewHolder.mColorDimmer.getPaint().getColor(); 223 ((ShadowOverlayContainer) viewHolder.itemView).setOverlayColor(dimmedColor); 224 } 225 viewHolder.itemView.setActivated(rowViewHolder.mExpanded); 226 } 227 228 @Override 229 public void onAddPresenter(Presenter presenter, int type) { 230 rowViewHolder.getGridView().getRecycledViewPool().setMaxRecycledViews(type, 24); 231 } 232 }); 233 } 234 235 final boolean needsDefaultListSelectEffect() { 236 return isUsingDefaultListSelectEffect() && getSelectEffectEnabled(); 237 } 238 239 /** 240 * Set {@link PresenterSelector} used for showing a select object in a hover card. 241 */ 242 public final void setHoverCardPresenterSelector(PresenterSelector selector) { 243 mHoverCardPresenterSelector = selector; 244 } 245 246 /** 247 * Get {@link PresenterSelector} used for showing a select object in a hover card. 248 */ 249 public final PresenterSelector getHoverCardPresenterSelector() { 250 return mHoverCardPresenterSelector; 251 } 252 253 /* 254 * Perform operations when a child of horizontal grid view is selected. 255 */ 256 private void selectChildView(ViewHolder rowViewHolder, View view) { 257 ItemBridgeAdapter.ViewHolder ibh = null; 258 if (view != null) { 259 ibh = (ItemBridgeAdapter.ViewHolder) 260 rowViewHolder.mGridView.getChildViewHolder(view); 261 } 262 if (view == null) { 263 if (mHoverCardPresenterSelector != null) { 264 rowViewHolder.mHoverCardViewSwitcher.unselect(); 265 } 266 if (getOnItemSelectedListener() != null) { 267 getOnItemSelectedListener().onItemSelected(null, rowViewHolder.mRow); 268 } 269 } else if (rowViewHolder.mExpanded && rowViewHolder.mSelected) { 270 if (mHoverCardPresenterSelector != null) { 271 rowViewHolder.mHoverCardViewSwitcher.select(rowViewHolder.mGridView, view, 272 ibh.mItem); 273 } 274 if (getOnItemSelectedListener() != null) { 275 getOnItemSelectedListener().onItemSelected(ibh.mItem, rowViewHolder.mRow); 276 } 277 } 278 } 279 280 private static void initStatics(Context context) { 281 if (sSelectedRowTopPadding == 0) { 282 sSelectedRowTopPadding = context.getResources().getDimensionPixelSize( 283 R.dimen.lb_browse_selected_row_top_padding); 284 sExpandedSelectedRowTopPadding = context.getResources().getDimensionPixelSize( 285 R.dimen.lb_browse_expanded_selected_row_top_padding); 286 sExpandedRowNoHovercardBottomPadding = context.getResources().getDimensionPixelSize( 287 R.dimen.lb_browse_expanded_row_no_hovercard_bottom_padding); 288 } 289 } 290 291 private int getSpaceUnderBaseline(ListRowPresenter.ViewHolder vh) { 292 RowHeaderPresenter.ViewHolder headerViewHolder = vh.getHeaderViewHolder(); 293 if (headerViewHolder != null) { 294 if (getHeaderPresenter() != null) { 295 return getHeaderPresenter().getSpaceUnderBaseline(headerViewHolder); 296 } 297 return headerViewHolder.view.getPaddingBottom(); 298 } 299 return 0; 300 } 301 302 private void setVerticalPadding(ListRowPresenter.ViewHolder vh) { 303 int paddingTop, paddingBottom; 304 if (vh.isExpanded()) { 305 int headerSpaceUnderBaseline = getSpaceUnderBaseline(vh); 306 if (DEBUG) Log.v(TAG, "headerSpaceUnderBaseline " + headerSpaceUnderBaseline); 307 paddingTop = (vh.isSelected() ? sExpandedSelectedRowTopPadding : vh.mPaddingTop) - 308 headerSpaceUnderBaseline; 309 paddingBottom = mHoverCardPresenterSelector == null ? 310 sExpandedRowNoHovercardBottomPadding : vh.mPaddingBottom; 311 } else if (vh.isSelected()) { 312 paddingTop = sSelectedRowTopPadding; 313 paddingBottom = sSelectedRowTopPadding - vh.mPaddingTop; 314 } else { 315 paddingTop = vh.mPaddingTop; 316 paddingBottom = 0; 317 } 318 vh.getGridView().setPadding(vh.mPaddingLeft, paddingTop, vh.mPaddingRight, 319 paddingBottom); 320 } 321 322 @Override 323 protected RowPresenter.ViewHolder createRowViewHolder(ViewGroup parent) { 324 initStatics(parent.getContext()); 325 ListRowView rowView = new ListRowView(parent.getContext()); 326 setupFadingEffect(rowView); 327 if (mRowHeight != 0) { 328 rowView.getGridView().setRowHeight(mRowHeight); 329 } 330 return new ViewHolder(rowView, rowView.getGridView(), this); 331 } 332 333 @Override 334 protected void onRowViewSelected(RowPresenter.ViewHolder holder, boolean selected) { 335 super.onRowViewSelected(holder, selected); 336 ViewHolder vh = (ViewHolder) holder; 337 setVerticalPadding(vh); 338 updateFooterViewSwitcher(vh); 339 } 340 341 /* 342 * Show or hide hover card when row selection or expanded state is changed. 343 */ 344 private void updateFooterViewSwitcher(ViewHolder vh) { 345 if (vh.mExpanded && vh.mSelected) { 346 if (mHoverCardPresenterSelector != null) { 347 vh.mHoverCardViewSwitcher.init((ViewGroup) vh.view, 348 mHoverCardPresenterSelector); 349 } 350 ItemBridgeAdapter.ViewHolder ibh = (ItemBridgeAdapter.ViewHolder) 351 vh.mGridView.findViewHolderForPosition( 352 vh.mGridView.getSelectedPosition()); 353 selectChildView(vh, ibh == null ? null : ibh.itemView); 354 } else { 355 if (mHoverCardPresenterSelector != null) { 356 vh.mHoverCardViewSwitcher.unselect(); 357 } 358 } 359 } 360 361 private void setupFadingEffect(ListRowView rowView) { 362 // content is completely faded at 1/2 padding of left, fading length is 1/2 of padding. 363 HorizontalGridView gridView = rowView.getGridView(); 364 if (mBrowseRowsFadingEdgeLength < 0) { 365 TypedArray ta = gridView.getContext() 366 .obtainStyledAttributes(R.styleable.LeanbackTheme); 367 mBrowseRowsFadingEdgeLength = (int) ta.getDimension( 368 R.styleable.LeanbackTheme_browseRowsFadingEdgeLength, 0); 369 ta.recycle(); 370 } 371 gridView.setFadingLeftEdgeLength(mBrowseRowsFadingEdgeLength); 372 } 373 374 @Override 375 protected void onRowViewExpanded(RowPresenter.ViewHolder holder, boolean expanded) { 376 super.onRowViewExpanded(holder, expanded); 377 ViewHolder vh = (ViewHolder) holder; 378 if (getRowHeight() != getExpandedRowHeight()) { 379 int newHeight = expanded ? getExpandedRowHeight() : getRowHeight(); 380 vh.getGridView().setRowHeight(newHeight); 381 } 382 setVerticalPadding(vh); 383 vh.getGridView().setFadingLeftEdge(!expanded); 384 updateFooterViewSwitcher(vh); 385 } 386 387 @Override 388 protected void onBindRowViewHolder(RowPresenter.ViewHolder holder, Object item) { 389 super.onBindRowViewHolder(holder, item); 390 ViewHolder vh = (ViewHolder) holder; 391 ListRow rowItem = (ListRow) item; 392 vh.mItemBridgeAdapter.clear(); 393 vh.mItemBridgeAdapter.setAdapter(rowItem.getAdapter()); 394 vh.mGridView.setAdapter(vh.mItemBridgeAdapter); 395 } 396 397 @Override 398 protected void onUnbindRowViewHolder(RowPresenter.ViewHolder holder) { 399 ((ViewHolder) holder).mGridView.setAdapter(null); 400 super.onUnbindRowViewHolder(holder); 401 } 402 403 /** 404 * ListRowPresenter overrides the default select effect of {@link RowPresenter} 405 * and return false. 406 */ 407 @Override 408 public final boolean isUsingDefaultSelectEffect() { 409 return false; 410 } 411 412 /** 413 * Returns true so that default select effect is applied to each individual 414 * child of {@link HorizontalGridView}. Subclass may return false to disable 415 * the default implementation. 416 * @see #onSelectLevelChanged(RowPresenter.ViewHolder) 417 */ 418 public boolean isUsingDefaultListSelectEffect() { 419 return true; 420 } 421 422 /** 423 * Returns true if SDK >= 18, where default shadow 424 * is applied to each individual child of {@link HorizontalGridView}. 425 * Subclass may return false to disable. 426 */ 427 public boolean isUsingDefaultShadow() { 428 return ShadowOverlayContainer.supportsShadow(); 429 } 430 431 /** 432 * Returns true if SDK >= L, where Z shadow is enabled so that Z order is enabled 433 * on each child of horizontal list. If subclass returns false in isUsingDefaultShadow() 434 * and does not use Z-shadow on SDK >= L, it should override isUsingZOrder() return false. 435 */ 436 public boolean isUsingZOrder() { 437 return ShadowHelper.getInstance().usesZShadow(); 438 } 439 440 /** 441 * Enable or disable child shadow. 442 * This is not only for enable/disable default shadow implementation but also subclass must 443 * respect this flag. 444 */ 445 public final void setShadowEnabled(boolean enabled) { 446 mShadowEnabled = enabled; 447 } 448 449 /** 450 * Returns true if child shadow is enabled. 451 * This is not only for enable/disable default shadow implementation but also subclass must 452 * respect this flag. 453 */ 454 public final boolean getShadowEnabled() { 455 return mShadowEnabled; 456 } 457 458 final boolean needsDefaultShadow() { 459 return isUsingDefaultShadow() && getShadowEnabled(); 460 } 461 462 @Override 463 public boolean canDrawOutOfBounds() { 464 return needsDefaultShadow(); 465 } 466 467 /** 468 * Applies select level to header and draw a default color dim over each child 469 * of {@link HorizontalGridView}. 470 * <p> 471 * Subclass may override this method. A subclass 472 * needs to call super.onSelectLevelChanged() for applying header select level 473 * and optionally applying a default select level to each child view of 474 * {@link HorizontalGridView} if {@link #isUsingDefaultListSelectEffect()} 475 * is true. Subclass may override {@link #isUsingDefaultListSelectEffect()} to return 476 * false and deal with the individual item select level by itself. 477 * </p> 478 */ 479 @Override 480 protected void onSelectLevelChanged(RowPresenter.ViewHolder holder) { 481 super.onSelectLevelChanged(holder); 482 if (needsDefaultListSelectEffect()) { 483 ViewHolder vh = (ViewHolder) holder; 484 vh.mColorDimmer.setActiveLevel(holder.mSelectLevel); 485 int dimmedColor = vh.mColorDimmer.getPaint().getColor(); 486 for (int i = 0, count = vh.mGridView.getChildCount(); i < count; i++) { 487 ShadowOverlayContainer wrapper = (ShadowOverlayContainer) vh.mGridView.getChildAt(i); 488 wrapper.setOverlayColor(dimmedColor); 489 } 490 if (vh.mGridView.getFadingLeftEdge()) { 491 vh.mGridView.invalidate(); 492 } 493 } 494 } 495 496} 497