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