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