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 */ 14 package android.support.v17.leanback.widget; 15 16 import android.support.v17.leanback.app.HeadersFragment; 17 import android.support.v17.leanback.graphics.ColorOverlayDimmer; 18 import android.view.View; 19 import android.view.ViewGroup; 20 21 /** 22 * An abstract {@link Presenter} that renders a {@link Row}. 23 * 24 * <h3>Customize UI widgets</h3> 25 * When a subclass of RowPresenter adds UI widgets, it should subclass 26 * {@link RowPresenter.ViewHolder} and override {@link #createRowViewHolder(ViewGroup)} 27 * and {@link #initializeRowViewHolder(ViewHolder)}. The subclass must use layout id 28 * "row_content" for the widget that will be aligned to the title of any {@link HeadersFragment} 29 * that may exist in the parent fragment. RowPresenter contains an optional and 30 * replaceable {@link RowHeaderPresenter} that renders the header. You can disable 31 * the default rendering or replace the Presenter with a new header presenter 32 * by calling {@link #setHeaderPresenter(RowHeaderPresenter)}. 33 * 34 * <h3>UI events from fragments</h3> 35 * RowPresenter receives calls from its parent (typically a Fragment) when: 36 * <ul> 37 * <li> 38 * A Row is selected via {@link #setRowViewSelected(Presenter.ViewHolder, boolean)}. The event 39 * is triggered immediately when there is a row selection change before the selection 40 * animation is started. 41 * Subclasses of RowPresenter may override {@link #onRowViewSelected(ViewHolder, boolean)}. 42 * </li> 43 * <li> 44 * A Row is expanded to full width via {@link #setRowViewExpanded(Presenter.ViewHolder, boolean)}. 45 * The event is triggered immediately before the expand animation is started. 46 * Subclasses of RowPresenter may override {@link #onRowViewExpanded(ViewHolder, boolean)}. 47 * </li> 48 * </ul> 49 * 50 * <h3>User events</h3> 51 * RowPresenter provides {@link OnItemSelectedListener} and {@link OnItemClickedListener}. 52 * If a subclass wants to add its own {@link View.OnFocusChangeListener} or 53 * {@link View.OnClickListener}, it must do that in {@link #createRowViewHolder(ViewGroup)} 54 * to be properly chained by the library. Adding View listeners after 55 * {@link #createRowViewHolder(ViewGroup)} is undefined and may result in 56 * incorrect behavior by the library's listeners. 57 * 58 * <h3>Selection animation</h3> 59 * <p> 60 * When a user scrolls through rows, a fragment will initiate animation and call 61 * {@link #setSelectLevel(Presenter.ViewHolder, float)} with float value between 62 * 0 and 1. By default, the RowPresenter draws a dim overlay on top of the row 63 * view for views that are not selected. Subclasses may override this default effect 64 * by having {@link #isUsingDefaultSelectEffect()} return false and overriding 65 * {@link #onSelectLevelChanged(ViewHolder)} to apply a different selection effect. 66 * </p> 67 * <p> 68 * Call {@link #setSelectEffectEnabled(boolean)} to enable/disable the select effect, 69 * This will not only enable/disable the default dim effect but also subclasses must 70 * respect this flag as well. 71 * </p> 72 */ 73 public abstract class RowPresenter extends Presenter { 74 75 static class ContainerViewHolder extends Presenter.ViewHolder { 76 /** 77 * wrapped row view holder 78 */ 79 final ViewHolder mRowViewHolder; 80 ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder)81 public ContainerViewHolder(RowContainerView containerView, ViewHolder rowViewHolder) { 82 super(containerView); 83 containerView.addRowView(rowViewHolder.view); 84 if (rowViewHolder.mHeaderViewHolder != null) { 85 containerView.addHeaderView(rowViewHolder.mHeaderViewHolder.view); 86 } 87 mRowViewHolder = rowViewHolder; 88 mRowViewHolder.mContainerViewHolder = this; 89 } 90 } 91 92 /** 93 * A view holder for a {@link Row}. 94 */ 95 public static class ViewHolder extends Presenter.ViewHolder { 96 ContainerViewHolder mContainerViewHolder; 97 RowHeaderPresenter.ViewHolder mHeaderViewHolder; 98 Row mRow; 99 boolean mSelected; 100 boolean mExpanded; 101 boolean mInitialzed; 102 float mSelectLevel = 0f; // initially unselected 103 protected final ColorOverlayDimmer mColorDimmer; 104 105 /** 106 * Constructor for ViewHolder. 107 * 108 * @param view The View bound to the Row. 109 */ ViewHolder(View view)110 public ViewHolder(View view) { 111 super(view); 112 mColorDimmer = ColorOverlayDimmer.createDefault(view.getContext()); 113 } 114 115 /** 116 * Returns the Row bound to the View in this ViewHolder. 117 */ getRow()118 public final Row getRow() { 119 return mRow; 120 } 121 122 /** 123 * Returns whether the Row is in its expanded state. 124 * 125 * @return true if the Row is expanded, false otherwise. 126 */ isExpanded()127 public final boolean isExpanded() { 128 return mExpanded; 129 } 130 131 /** 132 * Returns whether the Row is selected. 133 * 134 * @return true if the Row is selected, false otherwise. 135 */ isSelected()136 public final boolean isSelected() { 137 return mSelected; 138 } 139 140 /** 141 * Returns the current selection level of the Row. 142 */ getSelectLevel()143 public final float getSelectLevel() { 144 return mSelectLevel; 145 } 146 147 /** 148 * Returns the view holder for the Row header for this Row. 149 */ getHeaderViewHolder()150 public final RowHeaderPresenter.ViewHolder getHeaderViewHolder() { 151 return mHeaderViewHolder; 152 } 153 } 154 155 private RowHeaderPresenter mHeaderPresenter = new RowHeaderPresenter(); 156 private OnItemSelectedListener mOnItemSelectedListener; 157 private OnItemClickedListener mOnItemClickedListener; 158 private OnItemViewSelectedListener mOnItemViewSelectedListener; 159 private OnItemViewClickedListener mOnItemViewClickedListener; 160 161 boolean mSelectEffectEnabled = true; 162 163 @Override onCreateViewHolder(ViewGroup parent)164 public final Presenter.ViewHolder onCreateViewHolder(ViewGroup parent) { 165 ViewHolder vh = createRowViewHolder(parent); 166 vh.mInitialzed = false; 167 Presenter.ViewHolder result; 168 if (needsRowContainerView()) { 169 RowContainerView containerView = new RowContainerView(parent.getContext()); 170 if (mHeaderPresenter != null) { 171 vh.mHeaderViewHolder = (RowHeaderPresenter.ViewHolder) 172 mHeaderPresenter.onCreateViewHolder((ViewGroup) vh.view); 173 } 174 result = new ContainerViewHolder(containerView, vh); 175 } else { 176 result = vh; 177 } 178 initializeRowViewHolder(vh); 179 if (!vh.mInitialzed) { 180 throw new RuntimeException("super.initializeRowViewHolder() must be called"); 181 } 182 return result; 183 } 184 185 /** 186 * Called to create a ViewHolder object for a Row. Subclasses will override 187 * this method to return a different concrete ViewHolder object. 188 * 189 * @param parent The parent View for the Row's view holder. 190 * @return A ViewHolder for the Row's View. 191 */ createRowViewHolder(ViewGroup parent)192 protected abstract ViewHolder createRowViewHolder(ViewGroup parent); 193 194 /** 195 * Called after a {@link RowPresenter.ViewHolder} is created for a Row. 196 * Subclasses may override this method and start by calling 197 * super.initializeRowViewHolder(ViewHolder). 198 * 199 * @param vh The ViewHolder to initialize for the Row. 200 */ initializeRowViewHolder(ViewHolder vh)201 protected void initializeRowViewHolder(ViewHolder vh) { 202 vh.mInitialzed = true; 203 // set clip children to false for slide transition 204 ((ViewGroup) vh.view).setClipChildren(false); 205 if (vh.mContainerViewHolder != null) { 206 ((ViewGroup) vh.mContainerViewHolder.view).setClipChildren(false); 207 } 208 } 209 210 /** 211 * Set the Presenter used for rendering the header. Can be null to disable 212 * header rendering. The method must be called before creating any Row Views. 213 */ setHeaderPresenter(RowHeaderPresenter headerPresenter)214 public final void setHeaderPresenter(RowHeaderPresenter headerPresenter) { 215 mHeaderPresenter = headerPresenter; 216 } 217 218 /** 219 * Get the Presenter used for rendering the header, or null if none has been 220 * set. 221 */ getHeaderPresenter()222 public final RowHeaderPresenter getHeaderPresenter() { 223 return mHeaderPresenter; 224 } 225 226 /** 227 * Get the {@link RowPresenter.ViewHolder} from the given Presenter 228 * ViewHolder. 229 */ getRowViewHolder(Presenter.ViewHolder holder)230 public final ViewHolder getRowViewHolder(Presenter.ViewHolder holder) { 231 if (holder instanceof ContainerViewHolder) { 232 return ((ContainerViewHolder) holder).mRowViewHolder; 233 } else { 234 return (ViewHolder) holder; 235 } 236 } 237 238 /** 239 * Set the expanded state of a Row view. 240 * 241 * @param holder The Row ViewHolder to set expanded state on. 242 * @param expanded True if the Row is expanded, false otherwise. 243 */ setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded)244 public final void setRowViewExpanded(Presenter.ViewHolder holder, boolean expanded) { 245 ViewHolder rowViewHolder = getRowViewHolder(holder); 246 rowViewHolder.mExpanded = expanded; 247 onRowViewExpanded(rowViewHolder, expanded); 248 } 249 250 /** 251 * Set the selected state of a Row view. 252 * 253 * @param holder The Row ViewHolder to set expanded state on. 254 * @param selected True if the Row is expanded, false otherwise. 255 */ setRowViewSelected(Presenter.ViewHolder holder, boolean selected)256 public final void setRowViewSelected(Presenter.ViewHolder holder, boolean selected) { 257 ViewHolder rowViewHolder = getRowViewHolder(holder); 258 rowViewHolder.mSelected = selected; 259 onRowViewSelected(rowViewHolder, selected); 260 } 261 262 /** 263 * Subclass may override this to respond to expanded state changes of a Row. 264 * The default implementation will hide/show the header view. Subclasses may 265 * make visual changes to the Row View but must not create animation on the 266 * Row view. 267 */ onRowViewExpanded(ViewHolder vh, boolean expanded)268 protected void onRowViewExpanded(ViewHolder vh, boolean expanded) { 269 updateHeaderViewVisibility(vh); 270 vh.view.setActivated(expanded); 271 } 272 273 /** 274 * Subclass may override this to respond to selected state changes of a Row. 275 * Subclass may make visual changes to Row view but must not create 276 * animation on the Row view. 277 */ onRowViewSelected(ViewHolder vh, boolean selected)278 protected void onRowViewSelected(ViewHolder vh, boolean selected) { 279 if (selected) { 280 if (mOnItemViewSelectedListener != null) { 281 mOnItemViewSelectedListener.onItemSelected(null, null, vh, vh.getRow()); 282 } 283 if (mOnItemSelectedListener != null) { 284 mOnItemSelectedListener.onItemSelected(null, vh.getRow()); 285 } 286 } 287 updateHeaderViewVisibility(vh); 288 } 289 updateHeaderViewVisibility(ViewHolder vh)290 private void updateHeaderViewVisibility(ViewHolder vh) { 291 if (mHeaderPresenter != null && vh.mHeaderViewHolder != null) { 292 RowContainerView containerView = ((RowContainerView) vh.mContainerViewHolder.view); 293 containerView.showHeader(vh.isExpanded()); 294 } 295 } 296 297 /** 298 * Set the current select level to a value between 0 (unselected) and 1 (selected). 299 * Subclasses may override {@link #onSelectLevelChanged(ViewHolder)} to 300 * respond to changes in the selected level. 301 */ setSelectLevel(Presenter.ViewHolder vh, float level)302 public final void setSelectLevel(Presenter.ViewHolder vh, float level) { 303 ViewHolder rowViewHolder = getRowViewHolder(vh); 304 rowViewHolder.mSelectLevel = level; 305 onSelectLevelChanged(rowViewHolder); 306 } 307 308 /** 309 * Get the current select level. The value will be between 0 (unselected) 310 * and 1 (selected). 311 */ getSelectLevel(Presenter.ViewHolder vh)312 public final float getSelectLevel(Presenter.ViewHolder vh) { 313 return getRowViewHolder(vh).mSelectLevel; 314 } 315 316 /** 317 * Callback when select level is changed. The default implementation applies 318 * the select level to {@link RowHeaderPresenter#setSelectLevel(RowHeaderPresenter.ViewHolder, float)} 319 * when {@link #getSelectEffectEnabled()} is true. Subclasses may override 320 * this function and implement a different select effect. In this case, you 321 * should also override {@link #isUsingDefaultSelectEffect()} to disable 322 * the default dimming effect applied by the library. 323 */ onSelectLevelChanged(ViewHolder vh)324 protected void onSelectLevelChanged(ViewHolder vh) { 325 if (getSelectEffectEnabled()) { 326 vh.mColorDimmer.setActiveLevel(vh.mSelectLevel); 327 if (vh.mHeaderViewHolder != null) { 328 mHeaderPresenter.setSelectLevel(vh.mHeaderViewHolder, vh.mSelectLevel); 329 } 330 if (isUsingDefaultSelectEffect()) { 331 ((RowContainerView) vh.mContainerViewHolder.view).setForegroundColor( 332 vh.mColorDimmer.getPaint().getColor()); 333 } 334 } 335 } 336 337 /** 338 * Enables or disables the row selection effect. 339 * This will not only affect the default dim effect, but subclasses must 340 * respect this flag as well. 341 */ setSelectEffectEnabled(boolean applyDimOnSelect)342 public final void setSelectEffectEnabled(boolean applyDimOnSelect) { 343 mSelectEffectEnabled = applyDimOnSelect; 344 } 345 346 /** 347 * Returns true if the row selection effect is enabled. 348 * This value not only determines whether the default dim implementation is 349 * used, but subclasses must also respect this flag. 350 */ getSelectEffectEnabled()351 public final boolean getSelectEffectEnabled() { 352 return mSelectEffectEnabled; 353 } 354 355 /** 356 * Return whether this RowPresenter is using the default dimming effect 357 * provided by the library. Subclasses may(most likely) return false and 358 * override {@link #onSelectLevelChanged(ViewHolder)}. 359 */ isUsingDefaultSelectEffect()360 public boolean isUsingDefaultSelectEffect() { 361 return true; 362 } 363 needsDefaultSelectEffect()364 final boolean needsDefaultSelectEffect() { 365 return isUsingDefaultSelectEffect() && getSelectEffectEnabled(); 366 } 367 needsRowContainerView()368 final boolean needsRowContainerView() { 369 return mHeaderPresenter != null || needsDefaultSelectEffect(); 370 } 371 372 /** 373 * Return true if the Row view can draw outside its bounds. 374 */ canDrawOutOfBounds()375 public boolean canDrawOutOfBounds() { 376 return false; 377 } 378 379 @Override onBindViewHolder(Presenter.ViewHolder viewHolder, Object item)380 public final void onBindViewHolder(Presenter.ViewHolder viewHolder, Object item) { 381 onBindRowViewHolder(getRowViewHolder(viewHolder), item); 382 } 383 onBindRowViewHolder(ViewHolder vh, Object item)384 protected void onBindRowViewHolder(ViewHolder vh, Object item) { 385 vh.mRow = (Row) item; 386 if (vh.mHeaderViewHolder != null) { 387 mHeaderPresenter.onBindViewHolder(vh.mHeaderViewHolder, item); 388 } 389 } 390 391 @Override onUnbindViewHolder(Presenter.ViewHolder viewHolder)392 public final void onUnbindViewHolder(Presenter.ViewHolder viewHolder) { 393 onUnbindRowViewHolder(getRowViewHolder(viewHolder)); 394 } 395 onUnbindRowViewHolder(ViewHolder vh)396 protected void onUnbindRowViewHolder(ViewHolder vh) { 397 if (vh.mHeaderViewHolder != null) { 398 mHeaderPresenter.onUnbindViewHolder(vh.mHeaderViewHolder); 399 } 400 vh.mRow = null; 401 } 402 403 @Override onViewAttachedToWindow(Presenter.ViewHolder holder)404 public final void onViewAttachedToWindow(Presenter.ViewHolder holder) { 405 onRowViewAttachedToWindow(getRowViewHolder(holder)); 406 } 407 onRowViewAttachedToWindow(ViewHolder vh)408 protected void onRowViewAttachedToWindow(ViewHolder vh) { 409 if (vh.mHeaderViewHolder != null) { 410 mHeaderPresenter.onViewAttachedToWindow(vh.mHeaderViewHolder); 411 } 412 } 413 414 @Override onViewDetachedFromWindow(Presenter.ViewHolder holder)415 public final void onViewDetachedFromWindow(Presenter.ViewHolder holder) { 416 onRowViewDetachedFromWindow(getRowViewHolder(holder)); 417 } 418 onRowViewDetachedFromWindow(ViewHolder vh)419 protected void onRowViewDetachedFromWindow(ViewHolder vh) { 420 if (vh.mHeaderViewHolder != null) { 421 mHeaderPresenter.onViewDetachedFromWindow(vh.mHeaderViewHolder); 422 } 423 cancelAnimationsRecursive(vh.view); 424 } 425 426 /** 427 * Set the listener for item or row selection. A RowPresenter fires a row 428 * selection event with a null item. Subclasses (e.g. {@link ListRowPresenter}) 429 * can fire a selection event with the selected item. 430 */ setOnItemSelectedListener(OnItemSelectedListener listener)431 public final void setOnItemSelectedListener(OnItemSelectedListener listener) { 432 mOnItemSelectedListener = listener; 433 } 434 435 /** 436 * Get the listener for item or row selection. 437 */ getOnItemSelectedListener()438 public final OnItemSelectedListener getOnItemSelectedListener() { 439 return mOnItemSelectedListener; 440 } 441 442 /** 443 * Set the listener for item click events. A RowPresenter does not use this 444 * listener, but a subclass may fire an item click event if it has the concept 445 * of an item. The {@link OnItemClickedListener} will override any 446 * {@link View.OnClickListener} that an item's Presenter sets during 447 * {@link Presenter#onCreateViewHolder(ViewGroup)}. So in general, you 448 * should choose to use an OnItemClickedListener or a {@link 449 * View.OnClickListener}, but not both. 450 */ setOnItemClickedListener(OnItemClickedListener listener)451 public final void setOnItemClickedListener(OnItemClickedListener listener) { 452 mOnItemClickedListener = listener; 453 } 454 455 /** 456 * Get the listener for item click events. 457 */ getOnItemClickedListener()458 public final OnItemClickedListener getOnItemClickedListener() { 459 return mOnItemClickedListener; 460 } 461 462 /** 463 * Set listener for item or row selection. RowPresenter fires row selection 464 * event with null item, subclass of RowPresenter e.g. {@link ListRowPresenter} can 465 * fire a selection event with selected item. 466 */ setOnItemViewSelectedListener(OnItemViewSelectedListener listener)467 public final void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 468 mOnItemViewSelectedListener = listener; 469 } 470 471 /** 472 * Get listener for item or row selection. 473 */ getOnItemViewSelectedListener()474 public final OnItemViewSelectedListener getOnItemViewSelectedListener() { 475 return mOnItemViewSelectedListener; 476 } 477 478 /** 479 * Set listener for item click event. RowPresenter does nothing but subclass of 480 * RowPresenter may fire item click event if it does have a concept of item. 481 * OnItemViewClickedListener will override {@link View.OnClickListener} that 482 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 483 * So in general, developer should choose one of the listeners but not both. 484 */ setOnItemViewClickedListener(OnItemViewClickedListener listener)485 public final void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 486 mOnItemViewClickedListener = listener; 487 } 488 489 /** 490 * Set listener for item click event. 491 */ getOnItemViewClickedListener()492 public final OnItemViewClickedListener getOnItemViewClickedListener() { 493 return mOnItemViewClickedListener; 494 } 495 496 /** 497 * Freeze/Unfreeze the row, typically used when transition starts/ends. 498 * This method is called by fragment, app should not call it directly. 499 */ freeze(ViewHolder holder, boolean freeze)500 public void freeze(ViewHolder holder, boolean freeze) { 501 } 502 503 /** 504 * Change visibility of views, entrance transition will be run against the views that 505 * change visibilities. Subclass may override and begin with calling 506 * super.setEntranceTransitionState(). This method is called by fragment, 507 * app should not call it directly. 508 */ setEntranceTransitionState(ViewHolder holder, boolean afterTransition)509 public void setEntranceTransitionState(ViewHolder holder, boolean afterTransition) { 510 if (holder.mHeaderViewHolder != null) { 511 holder.mHeaderViewHolder.view.setVisibility(afterTransition ? 512 View.VISIBLE : View.INVISIBLE); 513 } 514 } 515 } 516