RowsFragment.java revision df2923d64b7fac60614eefcb769415f3003a0c47
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.app; 15 16import java.util.ArrayList; 17 18import android.animation.TimeAnimator; 19import android.animation.TimeAnimator.TimeListener; 20import android.os.Bundle; 21import android.support.v17.leanback.R; 22import android.support.v17.leanback.widget.ItemBridgeAdapter; 23import android.support.v17.leanback.widget.OnItemViewClickedListener; 24import android.support.v17.leanback.widget.OnItemViewSelectedListener; 25import android.support.v17.leanback.widget.RowPresenter.ViewHolder; 26import android.support.v17.leanback.widget.ScaleFrameLayout; 27import android.support.v17.leanback.widget.VerticalGridView; 28import android.support.v17.leanback.widget.ViewHolderTask; 29import android.support.v17.leanback.widget.HorizontalGridView; 30import android.support.v17.leanback.widget.RowPresenter; 31import android.support.v17.leanback.widget.ListRowPresenter; 32import android.support.v17.leanback.widget.Presenter; 33import android.support.v7.widget.RecyclerView; 34import android.util.Log; 35import android.view.LayoutInflater; 36import android.view.View; 37import android.view.ViewGroup; 38import android.view.ViewTreeObserver; 39import android.view.animation.DecelerateInterpolator; 40import android.view.animation.Interpolator; 41 42/** 43 * An ordered set of rows of leanback widgets. 44 * <p> 45 * A RowsFragment renders the elements of its 46 * {@link android.support.v17.leanback.widget.ObjectAdapter} as a set 47 * of rows in a vertical list. The elements in this adapter must be subclasses 48 * of {@link android.support.v17.leanback.widget.Row}. 49 * </p> 50 */ 51public class RowsFragment extends BaseRowFragment { 52 53 /** 54 * Internal helper class that manages row select animation and apply a default 55 * dim to each row. 56 */ 57 final class RowViewHolderExtra implements TimeListener { 58 final RowPresenter mRowPresenter; 59 final Presenter.ViewHolder mRowViewHolder; 60 61 final TimeAnimator mSelectAnimator = new TimeAnimator(); 62 63 int mSelectAnimatorDurationInUse; 64 Interpolator mSelectAnimatorInterpolatorInUse; 65 float mSelectLevelAnimStart; 66 float mSelectLevelAnimDelta; 67 68 RowViewHolderExtra(ItemBridgeAdapter.ViewHolder ibvh) { 69 mRowPresenter = (RowPresenter) ibvh.getPresenter(); 70 mRowViewHolder = ibvh.getViewHolder(); 71 mSelectAnimator.setTimeListener(this); 72 } 73 74 @Override 75 public void onTimeUpdate(TimeAnimator animation, long totalTime, long deltaTime) { 76 if (mSelectAnimator.isRunning()) { 77 updateSelect(totalTime, deltaTime); 78 } 79 } 80 81 void updateSelect(long totalTime, long deltaTime) { 82 float fraction; 83 if (totalTime >= mSelectAnimatorDurationInUse) { 84 fraction = 1; 85 mSelectAnimator.end(); 86 } else { 87 fraction = (float) (totalTime / (double) mSelectAnimatorDurationInUse); 88 } 89 if (mSelectAnimatorInterpolatorInUse != null) { 90 fraction = mSelectAnimatorInterpolatorInUse.getInterpolation(fraction); 91 } 92 float level = mSelectLevelAnimStart + fraction * mSelectLevelAnimDelta; 93 mRowPresenter.setSelectLevel(mRowViewHolder, level); 94 } 95 96 void animateSelect(boolean select, boolean immediate) { 97 mSelectAnimator.end(); 98 final float end = select ? 1 : 0; 99 if (immediate) { 100 mRowPresenter.setSelectLevel(mRowViewHolder, end); 101 } else if (mRowPresenter.getSelectLevel(mRowViewHolder) != end) { 102 mSelectAnimatorDurationInUse = mSelectAnimatorDuration; 103 mSelectAnimatorInterpolatorInUse = mSelectAnimatorInterpolator; 104 mSelectLevelAnimStart = mRowPresenter.getSelectLevel(mRowViewHolder); 105 mSelectLevelAnimDelta = end - mSelectLevelAnimStart; 106 mSelectAnimator.start(); 107 } 108 } 109 110 } 111 112 private static final String TAG = "RowsFragment"; 113 private static final boolean DEBUG = false; 114 115 private ItemBridgeAdapter.ViewHolder mSelectedViewHolder; 116 private int mSubPosition; 117 private boolean mExpand = true; 118 private boolean mViewsCreated; 119 private float mRowScaleFactor; 120 private int mAlignedTop; 121 private boolean mRowScaleEnabled; 122 private ScaleFrameLayout mScaleFrameLayout; 123 private boolean mAfterEntranceTransition = true; 124 125 private OnItemViewSelectedListener mOnItemViewSelectedListener; 126 private OnItemViewClickedListener mOnItemViewClickedListener; 127 128 // Select animation and interpolator are not intended to be 129 // exposed at this moment. They might be synced with vertical scroll 130 // animation later. 131 int mSelectAnimatorDuration; 132 Interpolator mSelectAnimatorInterpolator = new DecelerateInterpolator(2); 133 134 private RecyclerView.RecycledViewPool mRecycledViewPool; 135 private ArrayList<Presenter> mPresenterMapper; 136 137 private ItemBridgeAdapter.AdapterListener mExternalAdapterListener; 138 139 @Override 140 protected VerticalGridView findGridViewFromRoot(View view) { 141 return (VerticalGridView) view.findViewById(R.id.container_list); 142 } 143 144 /** 145 * Sets an item clicked listener on the fragment. 146 * OnItemViewClickedListener will override {@link View.OnClickListener} that 147 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 148 * So in general, developer should choose one of the listeners but not both. 149 */ 150 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 151 mOnItemViewClickedListener = listener; 152 if (mViewsCreated) { 153 throw new IllegalStateException( 154 "Item clicked listener must be set before views are created"); 155 } 156 } 157 158 /** 159 * Returns the item clicked listener. 160 */ 161 public OnItemViewClickedListener getOnItemViewClickedListener() { 162 return mOnItemViewClickedListener; 163 } 164 165 /** 166 * Set the visibility of titles/hovercard of browse rows. 167 */ 168 public void setExpand(boolean expand) { 169 mExpand = expand; 170 VerticalGridView listView = getVerticalGridView(); 171 if (listView != null) { 172 updateRowScaling(); 173 final int count = listView.getChildCount(); 174 if (DEBUG) Log.v(TAG, "setExpand " + expand + " count " + count); 175 for (int i = 0; i < count; i++) { 176 View view = listView.getChildAt(i); 177 ItemBridgeAdapter.ViewHolder vh = (ItemBridgeAdapter.ViewHolder) listView.getChildViewHolder(view); 178 setRowViewExpanded(vh, mExpand); 179 } 180 } 181 } 182 183 /** 184 * Sets an item selection listener. 185 */ 186 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 187 mOnItemViewSelectedListener = listener; 188 VerticalGridView listView = getVerticalGridView(); 189 if (listView != null) { 190 final int count = listView.getChildCount(); 191 for (int i = 0; i < count; i++) { 192 View view = listView.getChildAt(i); 193 ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) 194 listView.getChildViewHolder(view); 195 getRowViewHolder(ibvh).setOnItemViewSelectedListener(mOnItemViewSelectedListener); 196 } 197 } 198 } 199 200 /** 201 * Returns an item selection listener. 202 */ 203 public OnItemViewSelectedListener getOnItemViewSelectedListener() { 204 return mOnItemViewSelectedListener; 205 } 206 207 /** 208 * Enables scaling of rows. 209 * 210 * @param enable true to enable row scaling 211 */ 212 public void enableRowScaling(boolean enable) { 213 mRowScaleEnabled = enable; 214 } 215 216 @Override 217 void onRowSelected(RecyclerView parent, RecyclerView.ViewHolder viewHolder, 218 int position, int subposition) { 219 if (mSelectedViewHolder != viewHolder || mSubPosition != subposition) { 220 if (DEBUG) Log.v(TAG, "new row selected position " + position + " subposition " 221 + subposition + " view " + viewHolder.itemView); 222 mSubPosition = subposition; 223 if (mSelectedViewHolder != null) { 224 setRowViewSelected(mSelectedViewHolder, false, false); 225 } 226 mSelectedViewHolder = (ItemBridgeAdapter.ViewHolder) viewHolder; 227 if (mSelectedViewHolder != null) { 228 setRowViewSelected(mSelectedViewHolder, true, false); 229 } 230 } 231 } 232 233 /** 234 * Get row ViewHolder at adapter position. Returns null if the row object is not in adapter or 235 * the row object has not been bound to a row view. 236 * @param position Position of row in adapter. 237 * @return Row ViewHolder at a given adapter position. 238 */ 239 public RowPresenter.ViewHolder getRowViewHolder(int position) { 240 VerticalGridView verticalView = getVerticalGridView(); 241 if (verticalView == null) { 242 return null; 243 } 244 return getRowViewHolder((ItemBridgeAdapter.ViewHolder) 245 verticalView.findViewHolderForAdapterPosition(position)); 246 } 247 248 @Override 249 int getLayoutResourceId() { 250 return R.layout.lb_rows_fragment; 251 } 252 253 @Override 254 public void onCreate(Bundle savedInstanceState) { 255 super.onCreate(savedInstanceState); 256 mSelectAnimatorDuration = getResources().getInteger( 257 R.integer.lb_browse_rows_anim_duration); 258 mRowScaleFactor = getResources().getFraction( 259 R.fraction.lb_browse_rows_scale, 1, 1); 260 } 261 262 @Override 263 public View onCreateView(LayoutInflater inflater, ViewGroup container, 264 Bundle savedInstanceState) { 265 View view = super.onCreateView(inflater, container, savedInstanceState); 266 mScaleFrameLayout = (ScaleFrameLayout) view.findViewById(R.id.scale_frame); 267 return view; 268 } 269 270 @Override 271 public void onViewCreated(View view, Bundle savedInstanceState) { 272 if (DEBUG) Log.v(TAG, "onViewCreated"); 273 super.onViewCreated(view, savedInstanceState); 274 // Align the top edge of child with id row_content. 275 // Need set this for directly using RowsFragment. 276 getVerticalGridView().setItemAlignmentViewId(R.id.row_content); 277 getVerticalGridView().setSaveChildrenPolicy(VerticalGridView.SAVE_LIMITED_CHILD); 278 279 mRecycledViewPool = null; 280 mPresenterMapper = null; 281 } 282 283 @Override 284 public void onDestroyView() { 285 mViewsCreated = false; 286 super.onDestroyView(); 287 } 288 289 @Override 290 void setItemAlignment() { 291 super.setItemAlignment(); 292 if (getVerticalGridView() != null) { 293 getVerticalGridView().setItemAlignmentOffsetWithPadding(true); 294 } 295 } 296 297 void setExternalAdapterListener(ItemBridgeAdapter.AdapterListener listener) { 298 mExternalAdapterListener = listener; 299 } 300 301 /** 302 * Returns the view that will change scale. 303 */ 304 View getScaleView() { 305 return getVerticalGridView(); 306 } 307 308 /** 309 * Sets the pivots to scale rows fragment. 310 */ 311 void setScalePivots(float pivotX, float pivotY) { 312 // set pivot on ScaleFrameLayout, it will be propagated to its child VerticalGridView 313 // where we actually change scale. 314 mScaleFrameLayout.setPivotX(pivotX); 315 mScaleFrameLayout.setPivotY(pivotY); 316 } 317 318 private static void setRowViewExpanded(ItemBridgeAdapter.ViewHolder vh, boolean expanded) { 319 ((RowPresenter) vh.getPresenter()).setRowViewExpanded(vh.getViewHolder(), expanded); 320 } 321 322 private static void setRowViewSelected(ItemBridgeAdapter.ViewHolder vh, boolean selected, 323 boolean immediate) { 324 RowViewHolderExtra extra = (RowViewHolderExtra) vh.getExtraObject(); 325 extra.animateSelect(selected, immediate); 326 ((RowPresenter) vh.getPresenter()).setRowViewSelected(vh.getViewHolder(), selected); 327 } 328 329 private final ItemBridgeAdapter.AdapterListener mBridgeAdapterListener = 330 new ItemBridgeAdapter.AdapterListener() { 331 @Override 332 public void onAddPresenter(Presenter presenter, int type) { 333 if (mExternalAdapterListener != null) { 334 mExternalAdapterListener.onAddPresenter(presenter, type); 335 } 336 } 337 @Override 338 public void onCreate(ItemBridgeAdapter.ViewHolder vh) { 339 VerticalGridView listView = getVerticalGridView(); 340 if (listView != null) { 341 // set clip children false for slide animation 342 listView.setClipChildren(false); 343 } 344 setupSharedViewPool(vh); 345 mViewsCreated = true; 346 vh.setExtraObject(new RowViewHolderExtra(vh)); 347 // selected state is initialized to false, then driven by grid view onChildSelected 348 // events. When there is rebind, grid view fires onChildSelected event properly. 349 // So we don't need do anything special later in onBind or onAttachedToWindow. 350 setRowViewSelected(vh, false, true); 351 if (mExternalAdapterListener != null) { 352 mExternalAdapterListener.onCreate(vh); 353 } 354 } 355 @Override 356 public void onAttachedToWindow(ItemBridgeAdapter.ViewHolder vh) { 357 if (DEBUG) Log.v(TAG, "onAttachToWindow"); 358 // All views share the same mExpand value. When we attach a view to grid view, 359 // we should make sure it pick up the latest mExpand value we set early on other 360 // attached views. For no-structure-change update, the view is rebound to new data, 361 // but again it should use the unchanged mExpand value, so we don't need do any 362 // thing in onBind. 363 setRowViewExpanded(vh, mExpand); 364 RowPresenter rowPresenter = (RowPresenter) vh.getPresenter(); 365 RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(vh.getViewHolder()); 366 rowVh.setOnItemViewSelectedListener(mOnItemViewSelectedListener); 367 rowVh.setOnItemViewClickedListener(mOnItemViewClickedListener); 368 rowPresenter.setEntranceTransitionState(rowVh, mAfterEntranceTransition); 369 if (mExternalAdapterListener != null) { 370 mExternalAdapterListener.onAttachedToWindow(vh); 371 } 372 } 373 @Override 374 public void onDetachedFromWindow(ItemBridgeAdapter.ViewHolder vh) { 375 if (mSelectedViewHolder == vh) { 376 setRowViewSelected(mSelectedViewHolder, false, true); 377 mSelectedViewHolder = null; 378 } 379 if (mExternalAdapterListener != null) { 380 mExternalAdapterListener.onDetachedFromWindow(vh); 381 } 382 } 383 @Override 384 public void onBind(ItemBridgeAdapter.ViewHolder vh) { 385 if (mExternalAdapterListener != null) { 386 mExternalAdapterListener.onBind(vh); 387 } 388 } 389 @Override 390 public void onUnbind(ItemBridgeAdapter.ViewHolder vh) { 391 setRowViewSelected(vh, false, true); 392 if (mExternalAdapterListener != null) { 393 mExternalAdapterListener.onUnbind(vh); 394 } 395 } 396 }; 397 398 private void setupSharedViewPool(ItemBridgeAdapter.ViewHolder bridgeVh) { 399 RowPresenter rowPresenter = (RowPresenter) bridgeVh.getPresenter(); 400 RowPresenter.ViewHolder rowVh = rowPresenter.getRowViewHolder(bridgeVh.getViewHolder()); 401 402 if (rowVh instanceof ListRowPresenter.ViewHolder) { 403 HorizontalGridView view = ((ListRowPresenter.ViewHolder) rowVh).getGridView(); 404 // Recycled view pool is shared between all list rows 405 if (mRecycledViewPool == null) { 406 mRecycledViewPool = view.getRecycledViewPool(); 407 } else { 408 view.setRecycledViewPool(mRecycledViewPool); 409 } 410 411 ItemBridgeAdapter bridgeAdapter = 412 ((ListRowPresenter.ViewHolder) rowVh).getBridgeAdapter(); 413 if (mPresenterMapper == null) { 414 mPresenterMapper = bridgeAdapter.getPresenterMapper(); 415 } else { 416 bridgeAdapter.setPresenterMapper(mPresenterMapper); 417 } 418 } 419 } 420 421 @Override 422 void updateAdapter() { 423 super.updateAdapter(); 424 mSelectedViewHolder = null; 425 mViewsCreated = false; 426 427 ItemBridgeAdapter adapter = getBridgeAdapter(); 428 if (adapter != null) { 429 adapter.setAdapterListener(mBridgeAdapterListener); 430 } 431 } 432 433 @Override 434 boolean onTransitionPrepare() { 435 boolean prepared = super.onTransitionPrepare(); 436 if (prepared) { 437 freezeRows(true); 438 } 439 return prepared; 440 } 441 442 class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { 443 444 final View mVerticalView; 445 final Runnable mCallback; 446 int mState; 447 448 final static int STATE_INIT = 0; 449 final static int STATE_FIRST_DRAW = 1; 450 final static int STATE_SECOND_DRAW = 2; 451 452 ExpandPreLayout(Runnable callback) { 453 mVerticalView = getVerticalGridView(); 454 mCallback = callback; 455 } 456 457 void execute() { 458 mVerticalView.getViewTreeObserver().addOnPreDrawListener(this); 459 setExpand(false); 460 mState = STATE_INIT; 461 } 462 463 @Override 464 public boolean onPreDraw() { 465 if (getView() == null || getActivity() == null) { 466 mVerticalView.getViewTreeObserver().removeOnPreDrawListener(this); 467 return true; 468 } 469 if (mState == STATE_INIT) { 470 setExpand(true); 471 mState = STATE_FIRST_DRAW; 472 } else if (mState == STATE_FIRST_DRAW) { 473 mCallback.run(); 474 mVerticalView.getViewTreeObserver().removeOnPreDrawListener(this); 475 mState = STATE_SECOND_DRAW; 476 } 477 return false; 478 } 479 } 480 481 void onExpandTransitionStart(boolean expand, final Runnable callback) { 482 onTransitionPrepare(); 483 onTransitionStart(); 484 if (expand) { 485 callback.run(); 486 return; 487 } 488 // Run a "pre" layout when we go non-expand, in order to get the initial 489 // positions of added rows. 490 new ExpandPreLayout(callback).execute(); 491 } 492 493 private boolean needsScale() { 494 return mRowScaleEnabled && !mExpand; 495 } 496 497 private void updateRowScaling() { 498 final float scaleFactor = needsScale() ? mRowScaleFactor : 1f; 499 mScaleFrameLayout.setLayoutScaleY(scaleFactor); 500 getScaleView().setScaleY(scaleFactor); 501 getScaleView().setScaleX(scaleFactor); 502 updateWindowAlignOffset(); 503 } 504 505 private void updateWindowAlignOffset() { 506 int alignOffset = mAlignedTop; 507 if (needsScale()) { 508 alignOffset = (int) (alignOffset / mRowScaleFactor + 0.5f); 509 } 510 getVerticalGridView().setWindowAlignmentOffset(alignOffset); 511 } 512 513 @Override 514 void setWindowAlignmentFromTop(int alignedTop) { 515 mAlignedTop = alignedTop; 516 final VerticalGridView gridView = getVerticalGridView(); 517 if (gridView != null) { 518 updateWindowAlignOffset(); 519 // align to a fixed position from top 520 gridView.setWindowAlignmentOffsetPercent( 521 VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 522 gridView.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 523 } 524 } 525 526 @Override 527 void onTransitionEnd() { 528 super.onTransitionEnd(); 529 freezeRows(false); 530 } 531 532 private void freezeRows(boolean freeze) { 533 VerticalGridView verticalView = getVerticalGridView(); 534 if (verticalView != null) { 535 final int count = verticalView.getChildCount(); 536 for (int i = 0; i < count; i++) { 537 ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) 538 verticalView.getChildViewHolder(verticalView.getChildAt(i)); 539 RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter(); 540 RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder()); 541 rowPresenter.freeze(vh, freeze); 542 } 543 } 544 } 545 546 /** 547 * For rows that willing to participate entrance transition, this function 548 * hide views if afterTransition is true, show views if afterTransition is false. 549 */ 550 void setEntranceTransitionState(boolean afterTransition) { 551 mAfterEntranceTransition = afterTransition; 552 VerticalGridView verticalView = getVerticalGridView(); 553 if (verticalView != null) { 554 final int count = verticalView.getChildCount(); 555 for (int i = 0; i < count; i++) { 556 ItemBridgeAdapter.ViewHolder ibvh = (ItemBridgeAdapter.ViewHolder) 557 verticalView.getChildViewHolder(verticalView.getChildAt(i)); 558 RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter(); 559 RowPresenter.ViewHolder vh = rowPresenter.getRowViewHolder(ibvh.getViewHolder()); 560 rowPresenter.setEntranceTransitionState(vh, mAfterEntranceTransition); 561 } 562 } 563 } 564 565 /** 566 * Selects a Row and perform an optional task on the Row. For example 567 * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code> 568 * Scroll to 11th row and selects 6th item on that row. The method will be ignored if 569 * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, 570 * ViewGroup, Bundle)}). 571 * 572 * @param rowPosition Which row to select. 573 * @param smooth True to scroll to the row, false for no animation. 574 * @param rowHolderTask Task to perform on the Row. 575 */ 576 public void setSelectedPosition(int rowPosition, boolean smooth, 577 final Presenter.ViewHolderTask rowHolderTask) { 578 VerticalGridView verticalView = getVerticalGridView(); 579 if (verticalView == null) { 580 return; 581 } 582 ViewHolderTask task = null; 583 if (rowHolderTask != null) { 584 task = new ViewHolderTask() { 585 @Override 586 public void run(RecyclerView.ViewHolder rvh) { 587 rowHolderTask.run(getRowViewHolder((ItemBridgeAdapter.ViewHolder) rvh)); 588 } 589 }; 590 } 591 if (smooth) { 592 verticalView.setSelectedPositionSmooth(rowPosition, task); 593 } else { 594 verticalView.setSelectedPosition(rowPosition, task); 595 } 596 } 597 598 static RowPresenter.ViewHolder getRowViewHolder(ItemBridgeAdapter.ViewHolder ibvh) { 599 if (ibvh == null) { 600 return null; 601 } 602 RowPresenter rowPresenter = (RowPresenter) ibvh.getPresenter(); 603 return rowPresenter.getRowViewHolder(ibvh.getViewHolder()); 604 } 605} 606