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