BrowseSupportFragment.java revision 80a1d2dfbe5c1f26370cc1753c3ae321f126f5d2
1/* This file is auto-generated from BrowseFragment.java. DO NOT MODIFY. */ 2 3/* 4 * Copyright (C) 2014 The Android Open Source Project 5 * 6 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 7 * in compliance with the License. You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software distributed under the License 12 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 13 * or implied. See the License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16package android.support.v17.leanback.app; 17 18import android.support.v4.app.Fragment; 19import android.support.v4.app.FragmentManager; 20import android.support.v4.app.FragmentManager.BackStackEntry; 21import android.content.res.TypedArray; 22import android.graphics.Color; 23import android.graphics.Rect; 24import android.os.Bundle; 25import android.support.annotation.ColorInt; 26import android.support.v17.leanback.R; 27import android.support.v17.leanback.transition.TransitionHelper; 28import android.support.v17.leanback.transition.TransitionListener; 29import android.support.v17.leanback.widget.BrowseFrameLayout; 30import android.support.v17.leanback.widget.ListRow; 31import android.support.v17.leanback.widget.ObjectAdapter; 32import android.support.v17.leanback.widget.OnItemViewClickedListener; 33import android.support.v17.leanback.widget.OnItemViewSelectedListener; 34import android.support.v17.leanback.widget.PageRow; 35import android.support.v17.leanback.widget.Presenter; 36import android.support.v17.leanback.widget.PresenterSelector; 37import android.support.v17.leanback.widget.Row; 38import android.support.v17.leanback.widget.RowHeaderPresenter; 39import android.support.v17.leanback.widget.RowPresenter; 40import android.support.v17.leanback.widget.ScaleFrameLayout; 41import android.support.v17.leanback.widget.TitleView; 42import android.support.v17.leanback.widget.VerticalGridView; 43import android.support.v4.view.ViewCompat; 44import android.util.Log; 45import android.view.LayoutInflater; 46import android.view.View; 47import android.view.ViewGroup; 48import android.view.ViewGroup.MarginLayoutParams; 49import android.view.ViewTreeObserver; 50 51import static android.support.v7.widget.RecyclerView.NO_POSITION; 52 53/** 54 * A fragment for creating Leanback browse screens. It is composed of a 55 * RowsSupportFragment and a HeadersSupportFragment. 56 * <p> 57 * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set 58 * of rows in a vertical list. The elements in this adapter must be subclasses 59 * of {@link Row}. 60 * <p> 61 * The HeadersSupportFragment can be set to be either shown or hidden by default, or 62 * may be disabled entirely. See {@link #setHeadersState} for details. 63 * <p> 64 * By default the BrowseSupportFragment includes support for returning to the headers 65 * when the user presses Back. For Activities that customize {@link 66 * android.support.v4.app.FragmentActivity#onBackPressed()}, you must disable this default Back key support by 67 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and 68 * use {@link BrowseSupportFragment.BrowseTransitionListener} and 69 * {@link #startHeadersTransition(boolean)}. 70 * <p> 71 * The recommended theme to use with a BrowseSupportFragment is 72 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}. 73 * </p> 74 */ 75public class BrowseSupportFragment extends BaseSupportFragment { 76 77 // BUNDLE attribute for saving header show/hide status when backstack is used: 78 static final String HEADER_STACK_INDEX = "headerStackIndex"; 79 // BUNDLE attribute for saving header show/hide status when backstack is not used: 80 static final String HEADER_SHOW = "headerShow"; 81 82 final class BackStackListener implements FragmentManager.OnBackStackChangedListener { 83 int mLastEntryCount; 84 int mIndexOfHeadersBackStack; 85 86 BackStackListener() { 87 mLastEntryCount = getFragmentManager().getBackStackEntryCount(); 88 mIndexOfHeadersBackStack = -1; 89 } 90 91 void load(Bundle savedInstanceState) { 92 if (savedInstanceState != null) { 93 mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1); 94 mShowingHeaders = mIndexOfHeadersBackStack == -1; 95 } else { 96 if (!mShowingHeaders) { 97 getFragmentManager().beginTransaction() 98 .addToBackStack(mWithHeadersBackStackName).commit(); 99 } 100 } 101 } 102 103 void save(Bundle outState) { 104 outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack); 105 } 106 107 108 @Override 109 public void onBackStackChanged() { 110 if (getFragmentManager() == null) { 111 Log.w(TAG, "getFragmentManager() is null, stack:", new Exception()); 112 return; 113 } 114 int count = getFragmentManager().getBackStackEntryCount(); 115 // if backstack is growing and last pushed entry is "headers" backstack, 116 // remember the index of the entry. 117 if (count > mLastEntryCount) { 118 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1); 119 if (mWithHeadersBackStackName.equals(entry.getName())) { 120 mIndexOfHeadersBackStack = count - 1; 121 } 122 } else if (count < mLastEntryCount) { 123 // if popped "headers" backstack, initiate the show header transition if needed 124 if (mIndexOfHeadersBackStack >= count) { 125 mIndexOfHeadersBackStack = -1; 126 if (!mShowingHeaders) { 127 startHeadersTransitionInternal(true); 128 } 129 } 130 } 131 mLastEntryCount = count; 132 } 133 } 134 135 /** 136 * Listener for transitions between browse headers and rows. 137 */ 138 public static class BrowseTransitionListener { 139 /** 140 * Callback when headers transition starts. 141 * 142 * @param withHeaders True if the transition will result in headers 143 * being shown, false otherwise. 144 */ 145 public void onHeadersTransitionStart(boolean withHeaders) { 146 } 147 /** 148 * Callback when headers transition stops. 149 * 150 * @param withHeaders True if the transition will result in headers 151 * being shown, false otherwise. 152 */ 153 public void onHeadersTransitionStop(boolean withHeaders) { 154 } 155 } 156 157 private class SetSelectionRunnable implements Runnable { 158 static final int TYPE_INVALID = -1; 159 static final int TYPE_INTERNAL_SYNC = 0; 160 static final int TYPE_USER_REQUEST = 1; 161 162 private int mPosition; 163 private int mType; 164 private boolean mSmooth; 165 166 SetSelectionRunnable() { 167 reset(); 168 } 169 170 void post(int position, int type, boolean smooth) { 171 // Posting the set selection, rather than calling it immediately, prevents an issue 172 // with adapter changes. Example: a row is added before the current selected row; 173 // first the fast lane view updates its selection, then the rows fragment has that 174 // new selection propagated immediately; THEN the rows view processes the same adapter 175 // change and moves the selection again. 176 if (type >= mType) { 177 mPosition = position; 178 mType = type; 179 mSmooth = smooth; 180 mBrowseFrame.removeCallbacks(this); 181 mBrowseFrame.post(this); 182 } 183 } 184 185 @Override 186 public void run() { 187 setSelection(mPosition, mSmooth); 188 reset(); 189 } 190 191 private void reset() { 192 mPosition = -1; 193 mType = TYPE_INVALID; 194 mSmooth = false; 195 } 196 } 197 198 /** 199 * Interface that defines the interaction between {@link BrowseSupportFragment} and it's main 200 * content fragment. The key method is {@link AbstractMainFragmentAdapter#getFragment()}, 201 * it will be used to get the fragment to be shown in the content section. Clients can 202 * provide any implementation of fragment and customize it's interaction with 203 * {@link BrowseSupportFragment} by overriding the necessary methods. 204 * 205 * <p> 206 * Clients are expected to provide 207 * an instance of {@link MainSupportFragmentAdapterFactory} which will be responsible for providing 208 * implementations of {@link AbstractMainFragmentAdapter} for given content types. Currently 209 * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype 210 * of {@link Row}. We provide an out of the box adapter implementation for any rows other than 211 * {@link PageRow} - {@link RowsSupportFragmentAdapter}. 212 * 213 * <p> 214 * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment 215 * design. Users will have to provide an implementation of {@link AbstractMainFragmentAdapter} 216 * and provide that through {@link MainSupportFragmentAdapterFactory}. 217 * {@link AbstractMainFragmentAdapter} implementation can supply any fragment and override 218 * just those interactions that makes sense. 219 */ 220 public static abstract class AbstractMainFragmentAdapter { 221 private boolean mScalingEnabled; 222 public abstract Fragment getFragment(); 223 224 /** 225 * Returns whether its scrolling. 226 */ 227 public boolean isScrolling() { 228 return false; 229 } 230 231 /** 232 * Set the visibility of titles/hovercard of browse rows. 233 */ 234 public void setExpand(boolean expand) { 235 } 236 237 /** 238 * For rows that willing to participate entrance transition, this function 239 * hide views if afterTransition is true, show views if afterTransition is false. 240 */ 241 public void setEntranceTransitionState(boolean state) { 242 } 243 244 /** 245 * Sets the window alignment and also the pivots for scale operation. 246 */ 247 public void setAlignment(int windowAlignOffsetFromTop) { 248 } 249 250 /** 251 * Callback indicating transition prepare start. 252 */ 253 public boolean onTransitionPrepare() { 254 return false; 255 } 256 257 /** 258 * Callback indicating transition start. 259 */ 260 public void onTransitionStart() { 261 } 262 263 /** 264 * Callback indicating transition end. 265 */ 266 public void onTransitionEnd() { 267 } 268 269 /** 270 * Returns whether row scaling is enabled. 271 */ 272 public boolean isScalingEnabled() { 273 return mScalingEnabled; 274 } 275 276 /** 277 * Sets the row scaling property. 278 */ 279 public void setScalingEnabled(boolean scalingEnabled) { 280 this.mScalingEnabled = scalingEnabled; 281 } 282 283 /** 284 * Returns the object responsible for communicating with any fragment containing 285 * list of rows e.g. {@link RowsSupportFragment}. 286 * When this method returns null, some of the callbacks would be skipped. 287 * For {@link PageRow}, this should return null as it doesn't have a list of items. 288 */ 289 public MainFragmentRowsAdapter getRowsAdapter() { 290 return null; 291 } 292 } 293 294 /** 295 * This is used to pass information to {@link RowsSupportFragment}. {@link RowsSupportFragmentAdapter} 296 * would return an instance to connect the callbacks from {@link BrowseSupportFragment} to 297 * {@link RowsSupportFragment}. 298 */ 299 public static class MainFragmentRowsAdapter { 300 /** 301 * Set the visibility titles/hover of browse rows. 302 */ 303 public void setAdapter(ObjectAdapter adapter) { 304 } 305 306 /** 307 * Sets an item clicked listener on the fragment. 308 */ 309 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 310 } 311 312 /** 313 * Sets an item selection listener. 314 */ 315 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 316 } 317 318 /** 319 * Selects a Row and perform an optional task on the Row. 320 */ 321 public void setSelectedPosition(int rowPosition, 322 boolean smooth, 323 final Presenter.ViewHolderTask rowHolderTask) { 324 } 325 326 /** 327 * Selects a Row. 328 */ 329 public void setSelectedPosition(int rowPosition, boolean smooth) { 330 } 331 332 /** 333 * Returns the selected position. 334 */ 335 public int getSelectedPosition() { 336 return 0; 337 } 338 } 339 340 private boolean createMainFragment(ObjectAdapter adapter, int position) { 341 Object item = null; 342 if (adapter == null || adapter.size() == 0) { 343 } else { 344 if (position < 0 || position > adapter.size()) { 345 throw new IllegalArgumentException( 346 String.format("Invalid position %d requested", position)); 347 } 348 item = adapter.get(position); 349 } 350 AbstractMainFragmentAdapter oldAdapter = mMainFragmentAdapter; 351 boolean isPageRow = item instanceof PageRow; 352 353 if (isPageRow) { 354 mMainFragmentAdapter = mMainSupportFragmentAdapterFactory.getPageFragmentAdapter( 355 adapter, position); 356 } else { 357 mMainFragmentAdapter = mMainSupportFragmentAdapterFactory.getRowsSupportFragmentAdapter(); 358 } 359 mainFragmentRowsAdapter = isPageRow ? null : mMainFragmentAdapter.getRowsAdapter(); 360 mMainFragment = mMainFragmentAdapter.getFragment(); 361 return oldAdapter != mMainFragmentAdapter; 362 } 363 364 /** 365 * Factory class for {@link BrowseSupportFragment.AbstractMainFragmentAdapter}. Developers can 366 * override {@link MainSupportFragmentAdapterFactory#getPageFragmentAdapter(ObjectAdapter, int)} 367 * method to provide their instance of {@link AbstractMainFragmentAdapter}. 368 * {@link BrowseSupportFragment} will use this factory to create fragments using 369 * {@link BrowseSupportFragment.AbstractMainFragmentAdapter#getFragment()} to display on 370 * the main content section. 371 */ 372 public static class MainSupportFragmentAdapterFactory { 373 private RowsSupportFragmentAdapter rowsFragmentAdapter; 374 375 private final RowsSupportFragmentAdapter getRowsSupportFragmentAdapter() { 376 if (rowsFragmentAdapter == null) { 377 rowsFragmentAdapter = new RowsSupportFragmentAdapter(); 378 } 379 return rowsFragmentAdapter; 380 } 381 382 /** 383 * Override this method to return an instance of {@link AbstractMainFragmentAdapter}. 384 */ 385 public AbstractMainFragmentAdapter getPageFragmentAdapter( 386 ObjectAdapter adapter, int position) { 387 return null; 388 } 389 } 390 391 private static final String TAG = "BrowseSupportFragment"; 392 393 private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_"; 394 395 private static boolean DEBUG = false; 396 397 /** The headers fragment is enabled and shown by default. */ 398 public static final int HEADERS_ENABLED = 1; 399 400 /** The headers fragment is enabled and hidden by default. */ 401 public static final int HEADERS_HIDDEN = 2; 402 403 /** The headers fragment is disabled and will never be shown. */ 404 public static final int HEADERS_DISABLED = 3; 405 406 private MainSupportFragmentAdapterFactory mMainSupportFragmentAdapterFactory 407 = new MainSupportFragmentAdapterFactory(); 408 private AbstractMainFragmentAdapter mMainFragmentAdapter; 409 private Fragment mMainFragment; 410 private HeadersSupportFragment mHeadersSupportFragment; 411 private MainFragmentRowsAdapter mainFragmentRowsAdapter; 412 413 private ObjectAdapter mAdapter; 414 415 private int mHeadersState = HEADERS_ENABLED; 416 private int mBrandColor = Color.TRANSPARENT; 417 private boolean mBrandColorSet; 418 419 private BrowseFrameLayout mBrowseFrame; 420 private ScaleFrameLayout mScaleFrameLayout; 421 private boolean mHeadersBackStackEnabled = true; 422 private String mWithHeadersBackStackName; 423 private boolean mShowingHeaders = true; 424 private boolean mCanShowHeaders = true; 425 private int mContainerListMarginStart; 426 private int mContainerListAlignTop; 427 private boolean mMainFragmentScaleEnabled = true; 428 private OnItemViewSelectedListener mExternalOnItemViewSelectedListener; 429 private OnItemViewClickedListener mOnItemViewClickedListener; 430 private int mSelectedPosition = 0; 431 private float mScaleFactor; 432 433 private PresenterSelector mHeaderPresenterSelector; 434 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 435 436 // transition related: 437 private Object mSceneWithHeaders; 438 private Object mSceneWithoutHeaders; 439 private Object mSceneAfterEntranceTransition; 440 private Object mHeadersTransition; 441 private BackStackListener mBackStackChangedListener; 442 private BrowseTransitionListener mBrowseTransitionListener; 443 444 private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + ".title"; 445 private static final String ARG_BADGE_URI = BrowseSupportFragment.class.getCanonicalName() + ".badge"; 446 private static final String ARG_HEADERS_STATE = 447 BrowseSupportFragment.class.getCanonicalName() + ".headersState"; 448 449 /** 450 * Creates arguments for a browse fragment. 451 * 452 * @param args The Bundle to place arguments into, or null if the method 453 * should return a new Bundle. 454 * @param title The title of the BrowseSupportFragment. 455 * @param headersState The initial state of the headers of the 456 * BrowseSupportFragment. Must be one of {@link #HEADERS_ENABLED}, {@link 457 * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}. 458 * @return A Bundle with the given arguments for creating a BrowseSupportFragment. 459 */ 460 public static Bundle createArgs(Bundle args, String title, int headersState) { 461 if (args == null) { 462 args = new Bundle(); 463 } 464 args.putString(ARG_TITLE, title); 465 args.putInt(ARG_HEADERS_STATE, headersState); 466 return args; 467 } 468 469 /** 470 * Sets the brand color for the browse fragment. The brand color is used as 471 * the primary color for UI elements in the browse fragment. For example, 472 * the background color of the headers fragment uses the brand color. 473 * 474 * @param color The color to use as the brand color of the fragment. 475 */ 476 public void setBrandColor(@ColorInt int color) { 477 mBrandColor = color; 478 mBrandColorSet = true; 479 480 if (mHeadersSupportFragment != null) { 481 mHeadersSupportFragment.setBackgroundColor(mBrandColor); 482 } 483 } 484 485 /** 486 * Returns the brand color for the browse fragment. 487 * The default is transparent. 488 */ 489 @ColorInt 490 public int getBrandColor() { 491 return mBrandColor; 492 } 493 494 /** 495 * Sets the adapter containing the rows for the fragment. 496 * 497 * <p>The items referenced by the adapter must be be derived from 498 * {@link Row}. These rows will be used by the rows fragment and the headers 499 * fragment (if not disabled) to render the browse rows. 500 * 501 * @param adapter An ObjectAdapter for the browse rows. All items must 502 * derive from {@link Row}. 503 */ 504 public void setAdapter(ObjectAdapter adapter) { 505 mAdapter = adapter; 506 if (mMainFragment != null) { 507 mainFragmentRowsAdapter.setAdapter(adapter); 508 mHeadersSupportFragment.setAdapter(adapter); 509 } 510 } 511 512 public void setMainSupportFragmentAdapterFactory(MainSupportFragmentAdapterFactory factory) { 513 this.mMainSupportFragmentAdapterFactory = factory; 514 } 515 /** 516 * Returns the adapter containing the rows for the fragment. 517 */ 518 public ObjectAdapter getAdapter() { 519 return mAdapter; 520 } 521 522 /** 523 * Sets an item selection listener. 524 */ 525 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 526 mExternalOnItemViewSelectedListener = listener; 527 } 528 529 /** 530 * Returns an item selection listener. 531 */ 532 public OnItemViewSelectedListener getOnItemViewSelectedListener() { 533 return mExternalOnItemViewSelectedListener; 534 } 535 536 /** 537 * Get RowsSupportFragment if it's bound to BrowseSupportFragment or null if either BrowseSupportFragment has 538 * not been created yet or a different fragment is bound to it. 539 * 540 * @return RowsSupportFragment if it's bound to BrowseSupportFragment or null otherwise. 541 */ 542 public RowsSupportFragment getRowsSupportFragment() { 543 if (mMainFragment instanceof RowsSupportFragment) { 544 return (RowsSupportFragment) mMainFragment; 545 } 546 547 return null; 548 } 549 550 /** 551 * Get currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet. 552 * @return Currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet. 553 */ 554 public HeadersSupportFragment getHeadersSupportFragment() { 555 return mHeadersSupportFragment; 556 } 557 558 /** 559 * Sets an item clicked listener on the fragment. 560 * OnItemViewClickedListener will override {@link View.OnClickListener} that 561 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 562 * So in general, developer should choose one of the listeners but not both. 563 */ 564 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 565 mOnItemViewClickedListener = listener; 566 if (mMainFragmentAdapter != null) { 567 mainFragmentRowsAdapter.setOnItemViewClickedListener(listener); 568 } 569 } 570 571 /** 572 * Returns the item Clicked listener. 573 */ 574 public OnItemViewClickedListener getOnItemViewClickedListener() { 575 return mOnItemViewClickedListener; 576 } 577 578 /** 579 * Starts a headers transition. 580 * 581 * <p>This method will begin a transition to either show or hide the 582 * headers, depending on the value of withHeaders. If headers are disabled 583 * for this browse fragment, this method will throw an exception. 584 * 585 * @param withHeaders True if the headers should transition to being shown, 586 * false if the transition should result in headers being hidden. 587 */ 588 public void startHeadersTransition(boolean withHeaders) { 589 if (!mCanShowHeaders) { 590 throw new IllegalStateException("Cannot start headers transition"); 591 } 592 if (isInHeadersTransition() || mShowingHeaders == withHeaders) { 593 return; 594 } 595 startHeadersTransitionInternal(withHeaders); 596 } 597 598 /** 599 * Returns true if the headers transition is currently running. 600 */ 601 public boolean isInHeadersTransition() { 602 return mHeadersTransition != null; 603 } 604 605 /** 606 * Returns true if headers are shown. 607 */ 608 public boolean isShowingHeaders() { 609 return mShowingHeaders; 610 } 611 612 /** 613 * Sets a listener for browse fragment transitions. 614 * 615 * @param listener The listener to call when a browse headers transition 616 * begins or ends. 617 */ 618 public void setBrowseTransitionListener(BrowseTransitionListener listener) { 619 mBrowseTransitionListener = listener; 620 } 621 622 /** 623 * @deprecated use {@link BrowseSupportFragment#enableMainFragmentScaling(boolean)} instead. 624 * 625 * @param enable true to enable row scaling 626 */ 627 public void enableRowScaling(boolean enable) { 628 enableMainFragmentScaling(enable); 629 } 630 631 /** 632 * Enables scaling of main fragment when headers are present. For the page/row fragment, 633 * scaling is enabled only when both this method and 634 * {@link AbstractMainFragmentAdapter#isScalingEnabled()} are enabled. 635 * 636 * @param enable true to enable row scaling 637 */ 638 public void enableMainFragmentScaling(boolean enable) { 639 mMainFragmentScaleEnabled = enable; 640 } 641 642 private void startHeadersTransitionInternal(final boolean withHeaders) { 643 if (getFragmentManager().isDestroyed()) { 644 return; 645 } 646 mShowingHeaders = withHeaders; 647 mMainFragmentAdapter.onTransitionPrepare(); 648 mMainFragmentAdapter.onTransitionStart(); 649 onExpandTransitionStart(!withHeaders, new Runnable() { 650 @Override 651 public void run() { 652 mHeadersSupportFragment.onTransitionPrepare(); 653 mHeadersSupportFragment.onTransitionStart(); 654 createHeadersTransition(); 655 if (mBrowseTransitionListener != null) { 656 mBrowseTransitionListener.onHeadersTransitionStart(withHeaders); 657 } 658 TransitionHelper.runTransition( 659 withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition); 660 if (mHeadersBackStackEnabled) { 661 if (!withHeaders) { 662 getFragmentManager().beginTransaction() 663 .addToBackStack(mWithHeadersBackStackName).commit(); 664 } else { 665 int index = mBackStackChangedListener.mIndexOfHeadersBackStack; 666 if (index >= 0) { 667 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index); 668 getFragmentManager().popBackStackImmediate(entry.getId(), 669 FragmentManager.POP_BACK_STACK_INCLUSIVE); 670 } 671 } 672 } 673 } 674 }); 675 } 676 677 boolean isVerticalScrolling() { 678 // don't run transition 679 return mHeadersSupportFragment.isScrolling() || mMainFragmentAdapter.isScrolling(); 680 } 681 682 683 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 684 new BrowseFrameLayout.OnFocusSearchListener() { 685 @Override 686 public View onFocusSearch(View focused, int direction) { 687 // if headers is running transition, focus stays 688 if (mCanShowHeaders && isInHeadersTransition()) { 689 return focused; 690 } 691 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 692 693 if (getTitleView() != null && focused != getTitleView() && 694 direction == View.FOCUS_UP) { 695 return getTitleView(); 696 } 697 if (getTitleView() != null && getTitleView().hasFocus() && 698 direction == View.FOCUS_DOWN) { 699 return mCanShowHeaders && mShowingHeaders ? 700 mHeadersSupportFragment.getVerticalGridView() : mMainFragment.getView(); 701 } 702 703 boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; 704 int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 705 int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; 706 if (mCanShowHeaders && direction == towardStart) { 707 if (isVerticalScrolling() || mShowingHeaders) { 708 return focused; 709 } 710 return mHeadersSupportFragment.getVerticalGridView(); 711 } else if (direction == towardEnd) { 712 if (isVerticalScrolling()) { 713 return focused; 714 } 715 return mMainFragment.getView(); 716 } else { 717 return null; 718 } 719 } 720 }; 721 722 private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = 723 new BrowseFrameLayout.OnChildFocusListener() { 724 725 @Override 726 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 727 if (getChildFragmentManager().isDestroyed()) { 728 return true; 729 } 730 // Make sure not changing focus when requestFocus() is called. 731 if (mCanShowHeaders && mShowingHeaders) { 732 if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null && 733 mHeadersSupportFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 734 return true; 735 } 736 } 737 if (mMainFragment != null && mMainFragment.getView() != null && 738 mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 739 return true; 740 } 741 if (getTitleView() != null && 742 getTitleView().requestFocus(direction, previouslyFocusedRect)) { 743 return true; 744 } 745 return false; 746 } 747 748 @Override 749 public void onRequestChildFocus(View child, View focused) { 750 if (getChildFragmentManager().isDestroyed()) { 751 return; 752 } 753 if (!mCanShowHeaders || isInHeadersTransition()) return; 754 int childId = child.getId(); 755 if (childId == R.id.browse_container_dock && mShowingHeaders) { 756 startHeadersTransitionInternal(false); 757 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 758 startHeadersTransitionInternal(true); 759 } 760 } 761 }; 762 763 @Override 764 public void onSaveInstanceState(Bundle outState) { 765 super.onSaveInstanceState(outState); 766 if (mBackStackChangedListener != null) { 767 mBackStackChangedListener.save(outState); 768 } else { 769 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 770 } 771 } 772 773 @Override 774 public void onCreate(Bundle savedInstanceState) { 775 super.onCreate(savedInstanceState); 776 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 777 mContainerListMarginStart = (int) ta.getDimension( 778 R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() 779 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); 780 mContainerListAlignTop = (int) ta.getDimension( 781 R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() 782 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); 783 ta.recycle(); 784 785 readArguments(getArguments()); 786 787 if (mCanShowHeaders) { 788 if (mHeadersBackStackEnabled) { 789 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 790 mBackStackChangedListener = new BackStackListener(); 791 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 792 mBackStackChangedListener.load(savedInstanceState); 793 } else { 794 if (savedInstanceState != null) { 795 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 796 } 797 } 798 } 799 800 mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1); 801 } 802 803 @Override 804 public void onDestroy() { 805 if (mBackStackChangedListener != null) { 806 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 807 } 808 super.onDestroy(); 809 } 810 811 @Override 812 public View onCreateView(LayoutInflater inflater, ViewGroup container, 813 Bundle savedInstanceState) { 814 if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) { 815 mHeadersSupportFragment = new HeadersSupportFragment(); 816 createMainFragment(mAdapter, mSelectedPosition); 817 getChildFragmentManager().beginTransaction() 818 .replace(R.id.browse_headers_dock, mHeadersSupportFragment) 819 .replace(R.id.scale_frame, mMainFragment) 820 .commit(); 821 } else { 822 mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager() 823 .findFragmentById(R.id.scale_frame); 824 mMainFragment = getChildFragmentManager() 825 .findFragmentById(R.id.browse_container_dock); 826 } 827 828 mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders); 829 if (mHeaderPresenterSelector != null) { 830 mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector); 831 } 832 mHeadersSupportFragment.setAdapter(mAdapter); 833 mHeadersSupportFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener); 834 mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener); 835 836 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 837 838 setTitleView((TitleView) root.findViewById(R.id.browse_title_group)); 839 840 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 841 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 842 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 843 844 mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame); 845 mScaleFrameLayout.setPivotX(0); 846 mScaleFrameLayout.setPivotY(mContainerListAlignTop); 847 848 setupMainFragment(); 849 850 if (mBrandColorSet) { 851 mHeadersSupportFragment.setBackgroundColor(mBrandColor); 852 } 853 854 mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 855 @Override 856 public void run() { 857 showHeaders(true); 858 } 859 }); 860 mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 861 @Override 862 public void run() { 863 showHeaders(false); 864 } 865 }); 866 mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 867 @Override 868 public void run() { 869 setEntranceTransitionEndState(); 870 } 871 }); 872 return root; 873 } 874 875 private void setupMainFragment() { 876 if (mainFragmentRowsAdapter != null) { 877 mainFragmentRowsAdapter.setAdapter(mAdapter); 878 mainFragmentRowsAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener); 879 mainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); 880 } 881 } 882 883 private void createHeadersTransition() { 884 mHeadersTransition = TransitionHelper.loadTransition(getActivity(), 885 mShowingHeaders ? 886 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 887 888 TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() { 889 @Override 890 public void onTransitionStart(Object transition) { 891 } 892 @Override 893 public void onTransitionEnd(Object transition) { 894 mHeadersTransition = null; 895 mMainFragmentAdapter.onTransitionEnd(); 896 mHeadersSupportFragment.onTransitionEnd(); 897 if (mShowingHeaders) { 898 VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView(); 899 if (headerGridView != null && !headerGridView.hasFocus()) { 900 headerGridView.requestFocus(); 901 } 902 } else { 903 View rowsGridView = mMainFragment.getView(); 904 if (rowsGridView != null && !rowsGridView.hasFocus()) { 905 rowsGridView.requestFocus(); 906 } 907 } 908 if (mBrowseTransitionListener != null) { 909 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 910 } 911 } 912 }); 913 } 914 915 /** 916 * Sets the {@link PresenterSelector} used to render the row headers. 917 * 918 * @param headerPresenterSelector The PresenterSelector that will determine 919 * the Presenter for each row header. 920 */ 921 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 922 mHeaderPresenterSelector = headerPresenterSelector; 923 if (mHeadersSupportFragment != null) { 924 mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector); 925 } 926 } 927 928 private void setHeadersOnScreen(boolean onScreen) { 929 MarginLayoutParams lp; 930 View containerList; 931 containerList = mHeadersSupportFragment.getView(); 932 lp = (MarginLayoutParams) containerList.getLayoutParams(); 933 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 934 containerList.setLayoutParams(lp); 935 } 936 937 private void showHeaders(boolean show) { 938 if (DEBUG) Log.v(TAG, "showHeaders " + show); 939 mHeadersSupportFragment.setHeadersEnabled(show); 940 setHeadersOnScreen(show); 941 expandMainFragment(!show); 942 } 943 944 private void expandMainFragment(boolean expand) { 945 MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams(); 946 params.leftMargin = !expand ? mContainerListMarginStart : 0; 947 mScaleFrameLayout.setLayoutParams(params); 948 mMainFragmentAdapter.setExpand(expand); 949 950 setMainFragmentAlignment(); 951 final float scaleFactor = !expand 952 && mMainFragmentScaleEnabled && mMainFragmentAdapter.isScalingEnabled() 953 ? mScaleFactor : 1; 954 mScaleFrameLayout.setLayoutScaleY(scaleFactor); 955 mScaleFrameLayout.setChildScale(scaleFactor); 956 } 957 958 private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener = 959 new HeadersSupportFragment.OnHeaderClickedListener() { 960 @Override 961 public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 962 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 963 return; 964 } 965 startHeadersTransitionInternal(false); 966 mMainFragment.getView().requestFocus(); 967 } 968 }; 969 970 private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() { 971 @Override 972 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 973 RowPresenter.ViewHolder rowViewHolder, Row row) { 974 if (mMainFragment == null) { 975 return; 976 } 977 978 int position = ((RowsSupportFragment) mMainFragment) 979 .getVerticalGridView().getSelectedPosition(); 980 if (DEBUG) Log.v(TAG, "row selected position " + position); 981 onRowSelected(position); 982 if (mExternalOnItemViewSelectedListener != null) { 983 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 984 rowViewHolder, row); 985 } 986 } 987 }; 988 989 private HeadersSupportFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener = 990 new HeadersSupportFragment.OnHeaderViewSelectedListener() { 991 @Override 992 public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 993 int position = mHeadersSupportFragment.getVerticalGridView().getSelectedPosition(); 994 if (DEBUG) Log.v(TAG, "header selected position " + position); 995 onRowSelected(position); 996 } 997 }; 998 999 private void onRowSelected(int position) { 1000 if (position != mSelectedPosition) { 1001 mSetSelectionRunnable.post( 1002 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 1003 1004 if (getAdapter() == null || getAdapter().size() == 0 || position == 0) { 1005 showTitle(true); 1006 } else { 1007 showTitle(false); 1008 } 1009 } 1010 } 1011 1012 private void setSelection(int position, boolean smooth) { 1013 if (position == NO_POSITION) { 1014 return; 1015 } 1016 1017 mHeadersSupportFragment.setSelectedPosition(position, smooth); 1018 1019 if (createMainFragment(mAdapter, position)) { 1020 swapBrowseContent(mMainFragment); 1021 expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); 1022 setupMainFragment(); 1023 } 1024 if (mainFragmentRowsAdapter != null) { 1025 mainFragmentRowsAdapter.setSelectedPosition(position, smooth); 1026 } 1027 mSelectedPosition = position; 1028 } 1029 1030 private void swapBrowseContent(Fragment fragment) { 1031 getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit(); 1032 } 1033 1034 /** 1035 * Sets the selected row position with smooth animation. 1036 */ 1037 public void setSelectedPosition(int position) { 1038 setSelectedPosition(position, true); 1039 } 1040 1041 /** 1042 * Gets position of currently selected row. 1043 * @return Position of currently selected row. 1044 */ 1045 public int getSelectedPosition() { 1046 return mSelectedPosition; 1047 } 1048 1049 /** 1050 * Sets the selected row position. 1051 */ 1052 public void setSelectedPosition(int position, boolean smooth) { 1053 mSetSelectionRunnable.post( 1054 position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); 1055 } 1056 1057 /** 1058 * Selects a Row and perform an optional task on the Row. For example 1059 * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code> 1060 * scrolls to 11th row and selects 6th item on that row. The method will be ignored if 1061 * RowsSupportFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, 1062 * ViewGroup, Bundle)}). 1063 * 1064 * @param rowPosition Which row to select. 1065 * @param smooth True to scroll to the row, false for no animation. 1066 * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers 1067 * fragment will be collapsed. 1068 */ 1069 public void setSelectedPosition(int rowPosition, boolean smooth, 1070 final Presenter.ViewHolderTask rowHolderTask) { 1071 if (mMainSupportFragmentAdapterFactory == null) { 1072 return; 1073 } 1074 if (rowHolderTask != null) { 1075 startHeadersTransition(false); 1076 } 1077 if (mainFragmentRowsAdapter != null) { 1078 mainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask); 1079 } 1080 } 1081 1082 @Override 1083 public void onStart() { 1084 super.onStart(); 1085 mHeadersSupportFragment.setAlignment(mContainerListAlignTop); 1086 setMainFragmentAlignment(); 1087 1088 if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment.getView() != null) { 1089 mHeadersSupportFragment.getView().requestFocus(); 1090 } else if ((!mCanShowHeaders || !mShowingHeaders) 1091 && mMainFragment.getView() != null) { 1092 mMainFragment.getView().requestFocus(); 1093 } 1094 1095 if (mCanShowHeaders) { 1096 showHeaders(mShowingHeaders); 1097 } 1098 1099 if (isEntranceTransitionEnabled()) { 1100 setEntranceTransitionStartState(); 1101 } 1102 } 1103 1104 private void onExpandTransitionStart(boolean expand, final Runnable callback) { 1105 if (expand) { 1106 callback.run(); 1107 return; 1108 } 1109 // Run a "pre" layout when we go non-expand, in order to get the initial 1110 // positions of added rows. 1111 new ExpandPreLayout(callback, mMainFragmentAdapter).execute(); 1112 } 1113 1114 private void setMainFragmentAlignment() { 1115 int alignOffset = mContainerListAlignTop; 1116 if (mMainFragmentScaleEnabled 1117 && mMainFragmentAdapter.isScalingEnabled() 1118 && mShowingHeaders) { 1119 alignOffset = (int) (alignOffset / mScaleFactor + 0.5f); 1120 } 1121 mMainFragmentAdapter.setAlignment(alignOffset); 1122 } 1123 1124 /** 1125 * Enables/disables headers transition on back key support. This is enabled by 1126 * default. The BrowseSupportFragment will add a back stack entry when headers are 1127 * showing. Running a headers transition when the back key is pressed only 1128 * works when the headers state is {@link #HEADERS_ENABLED} or 1129 * {@link #HEADERS_HIDDEN}. 1130 * <p> 1131 * NOTE: If an Activity has its own onBackPressed() handling, you must 1132 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 1133 * and {@link BrowseTransitionListener} in your own back stack handling. 1134 */ 1135 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 1136 mHeadersBackStackEnabled = headersBackStackEnabled; 1137 } 1138 1139 /** 1140 * Returns true if headers transition on back key support is enabled. 1141 */ 1142 public final boolean isHeadersTransitionOnBackEnabled() { 1143 return mHeadersBackStackEnabled; 1144 } 1145 1146 private void readArguments(Bundle args) { 1147 if (args == null) { 1148 return; 1149 } 1150 if (args.containsKey(ARG_TITLE)) { 1151 setTitle(args.getString(ARG_TITLE)); 1152 } 1153 if (args.containsKey(ARG_HEADERS_STATE)) { 1154 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 1155 } 1156 } 1157 1158 /** 1159 * Sets the state for the headers column in the browse fragment. Must be one 1160 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1161 * {@link #HEADERS_DISABLED}. 1162 * 1163 * @param headersState The state of the headers for the browse fragment. 1164 */ 1165 public void setHeadersState(int headersState) { 1166 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1167 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1168 } 1169 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1170 1171 if (headersState != mHeadersState) { 1172 mHeadersState = headersState; 1173 switch (headersState) { 1174 case HEADERS_ENABLED: 1175 mCanShowHeaders = true; 1176 mShowingHeaders = true; 1177 break; 1178 case HEADERS_HIDDEN: 1179 mCanShowHeaders = true; 1180 mShowingHeaders = false; 1181 break; 1182 case HEADERS_DISABLED: 1183 mCanShowHeaders = false; 1184 mShowingHeaders = false; 1185 break; 1186 default: 1187 Log.w(TAG, "Unknown headers state: " + headersState); 1188 break; 1189 } 1190 if (mHeadersSupportFragment != null) { 1191 mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders); 1192 } 1193 } 1194 } 1195 1196 /** 1197 * Returns the state of the headers column in the browse fragment. 1198 */ 1199 public int getHeadersState() { 1200 return mHeadersState; 1201 } 1202 1203 @Override 1204 protected Object createEntranceTransition() { 1205 return TransitionHelper.loadTransition(getActivity(), 1206 R.transition.lb_browse_entrance_transition); 1207 } 1208 1209 @Override 1210 protected void runEntranceTransition(Object entranceTransition) { 1211 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 1212 } 1213 1214 @Override 1215 protected void onEntranceTransitionPrepare() { 1216 mHeadersSupportFragment.onTransitionPrepare(); 1217 mMainFragmentAdapter.onTransitionPrepare(); 1218 } 1219 1220 @Override 1221 protected void onEntranceTransitionStart() { 1222 mHeadersSupportFragment.onTransitionStart(); 1223 mMainFragmentAdapter.onTransitionStart(); 1224 } 1225 1226 @Override 1227 protected void onEntranceTransitionEnd() { 1228 mMainFragmentAdapter.onTransitionEnd(); 1229 mHeadersSupportFragment.onTransitionEnd(); 1230 } 1231 1232 void setSearchOrbViewOnScreen(boolean onScreen) { 1233 View searchOrbView = getTitleView().getSearchAffordanceView(); 1234 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1235 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1236 searchOrbView.setLayoutParams(lp); 1237 } 1238 1239 void setEntranceTransitionStartState() { 1240 setHeadersOnScreen(false); 1241 setSearchOrbViewOnScreen(false); 1242 mMainFragmentAdapter.setEntranceTransitionState(false); 1243 } 1244 1245 void setEntranceTransitionEndState() { 1246 setHeadersOnScreen(mShowingHeaders); 1247 setSearchOrbViewOnScreen(true); 1248 mMainFragmentAdapter.setEntranceTransitionState(true); 1249 } 1250 1251 private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { 1252 1253 private final View mView; 1254 private final Runnable mCallback; 1255 private int mState; 1256 private AbstractMainFragmentAdapter mainFragmentAdapter; 1257 1258 final static int STATE_INIT = 0; 1259 final static int STATE_FIRST_DRAW = 1; 1260 final static int STATE_SECOND_DRAW = 2; 1261 1262 ExpandPreLayout(Runnable callback, AbstractMainFragmentAdapter adapter) { 1263 mView = adapter.getFragment().getView(); 1264 mCallback = callback; 1265 mainFragmentAdapter = adapter; 1266 } 1267 1268 void execute() { 1269 mView.getViewTreeObserver().addOnPreDrawListener(this); 1270 mainFragmentAdapter.setExpand(false); 1271 mState = STATE_INIT; 1272 } 1273 1274 @Override 1275 public boolean onPreDraw() { 1276 if (mainFragmentAdapter.getFragment().getView() == null) { 1277 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1278 return true; 1279 } 1280 if (mState == STATE_INIT) { 1281 mainFragmentAdapter.setExpand(true); 1282 mState = STATE_FIRST_DRAW; 1283 } else if (mState == STATE_FIRST_DRAW) { 1284 mCallback.run(); 1285 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1286 mState = STATE_SECOND_DRAW; 1287 } 1288 return false; 1289 } 1290 } 1291} 1292