BrowseSupportFragment.java revision 489c9a861b7cde47efcd5cf6351bc9696786ae41
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.support.v4.app.FragmentTransaction; 22import android.content.res.TypedArray; 23import android.graphics.Color; 24import android.graphics.Rect; 25import android.os.Bundle; 26import android.support.annotation.ColorInt; 27import android.support.v17.leanback.R; 28import android.support.v17.leanback.transition.TransitionHelper; 29import android.support.v17.leanback.transition.TransitionListener; 30import android.support.v17.leanback.widget.BrowseFrameLayout; 31import android.support.v17.leanback.widget.InvisibleRowPresenter; 32import android.support.v17.leanback.widget.ListRow; 33import android.support.v17.leanback.widget.ObjectAdapter; 34import android.support.v17.leanback.widget.OnItemViewClickedListener; 35import android.support.v17.leanback.widget.OnItemViewSelectedListener; 36import android.support.v17.leanback.widget.PageRow; 37import android.support.v17.leanback.widget.Presenter; 38import android.support.v17.leanback.widget.PresenterSelector; 39import android.support.v17.leanback.widget.Row; 40import android.support.v17.leanback.widget.RowHeaderPresenter; 41import android.support.v17.leanback.widget.RowPresenter; 42import android.support.v17.leanback.widget.ScaleFrameLayout; 43import android.support.v17.leanback.widget.TitleViewAdapter; 44import android.support.v17.leanback.widget.VerticalGridView; 45import android.support.v4.view.ViewCompat; 46import android.support.v7.widget.RecyclerView; 47import android.util.Log; 48import android.view.LayoutInflater; 49import android.view.View; 50import android.view.ViewGroup; 51import android.view.ViewGroup.MarginLayoutParams; 52import android.view.ViewTreeObserver; 53 54import java.util.HashMap; 55import java.util.Map; 56 57import static android.support.v7.widget.RecyclerView.NO_POSITION; 58 59/** 60 * A fragment for creating Leanback browse screens. It is composed of a 61 * RowsSupportFragment and a HeadersSupportFragment. 62 * <p> 63 * A BrowseSupportFragment renders the elements of its {@link ObjectAdapter} as a set 64 * of rows in a vertical list. The elements in this adapter must be subclasses 65 * of {@link Row}. 66 * <p> 67 * The HeadersSupportFragment can be set to be either shown or hidden by default, or 68 * may be disabled entirely. See {@link #setHeadersState} for details. 69 * <p> 70 * By default the BrowseSupportFragment includes support for returning to the headers 71 * when the user presses Back. For Activities that customize {@link 72 * android.support.v4.app.FragmentActivity#onBackPressed()}, you must disable this default Back key support by 73 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and 74 * use {@link BrowseSupportFragment.BrowseTransitionListener} and 75 * {@link #startHeadersTransition(boolean)}. 76 * <p> 77 * The recommended theme to use with a BrowseSupportFragment is 78 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}. 79 * </p> 80 */ 81public class BrowseSupportFragment extends BaseSupportFragment { 82 83 // BUNDLE attribute for saving header show/hide status when backstack is used: 84 static final String HEADER_STACK_INDEX = "headerStackIndex"; 85 // BUNDLE attribute for saving header show/hide status when backstack is not used: 86 static final String HEADER_SHOW = "headerShow"; 87 private static final String IS_PAGE_ROW = "isPageRow"; 88 private static final String CURRENT_SELECTED_POSITION = "currentSelectedPosition"; 89 90 final class BackStackListener implements FragmentManager.OnBackStackChangedListener { 91 int mLastEntryCount; 92 int mIndexOfHeadersBackStack; 93 94 BackStackListener() { 95 mLastEntryCount = getFragmentManager().getBackStackEntryCount(); 96 mIndexOfHeadersBackStack = -1; 97 } 98 99 void load(Bundle savedInstanceState) { 100 if (savedInstanceState != null) { 101 mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1); 102 mShowingHeaders = mIndexOfHeadersBackStack == -1; 103 } else { 104 if (!mShowingHeaders) { 105 getFragmentManager().beginTransaction() 106 .addToBackStack(mWithHeadersBackStackName).commit(); 107 } 108 } 109 } 110 111 void save(Bundle outState) { 112 outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack); 113 } 114 115 116 @Override 117 public void onBackStackChanged() { 118 if (getFragmentManager() == null) { 119 Log.w(TAG, "getFragmentManager() is null, stack:", new Exception()); 120 return; 121 } 122 int count = getFragmentManager().getBackStackEntryCount(); 123 // if backstack is growing and last pushed entry is "headers" backstack, 124 // remember the index of the entry. 125 if (count > mLastEntryCount) { 126 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1); 127 if (mWithHeadersBackStackName.equals(entry.getName())) { 128 mIndexOfHeadersBackStack = count - 1; 129 } 130 } else if (count < mLastEntryCount) { 131 // if popped "headers" backstack, initiate the show header transition if needed 132 if (mIndexOfHeadersBackStack >= count) { 133 if (!isHeadersDataReady()) { 134 // if main fragment was restored first before BrowseSupportFragment's adapter gets 135 // restored: don't start header transition, but add the entry back. 136 getFragmentManager().beginTransaction() 137 .addToBackStack(mWithHeadersBackStackName).commit(); 138 return; 139 } 140 mIndexOfHeadersBackStack = -1; 141 if (!mShowingHeaders) { 142 startHeadersTransitionInternal(true); 143 } 144 } 145 } 146 mLastEntryCount = count; 147 } 148 } 149 150 /** 151 * Listener for transitions between browse headers and rows. 152 */ 153 public static class BrowseTransitionListener { 154 /** 155 * Callback when headers transition starts. 156 * 157 * @param withHeaders True if the transition will result in headers 158 * being shown, false otherwise. 159 */ 160 public void onHeadersTransitionStart(boolean withHeaders) { 161 } 162 /** 163 * Callback when headers transition stops. 164 * 165 * @param withHeaders True if the transition will result in headers 166 * being shown, false otherwise. 167 */ 168 public void onHeadersTransitionStop(boolean withHeaders) { 169 } 170 } 171 172 private class SetSelectionRunnable implements Runnable { 173 static final int TYPE_INVALID = -1; 174 static final int TYPE_INTERNAL_SYNC = 0; 175 static final int TYPE_USER_REQUEST = 1; 176 177 private int mPosition; 178 private int mType; 179 private boolean mSmooth; 180 181 SetSelectionRunnable() { 182 reset(); 183 } 184 185 void post(int position, int type, boolean smooth) { 186 // Posting the set selection, rather than calling it immediately, prevents an issue 187 // with adapter changes. Example: a row is added before the current selected row; 188 // first the fast lane view updates its selection, then the rows fragment has that 189 // new selection propagated immediately; THEN the rows view processes the same adapter 190 // change and moves the selection again. 191 if (type >= mType) { 192 mPosition = position; 193 mType = type; 194 mSmooth = smooth; 195 mBrowseFrame.removeCallbacks(this); 196 mBrowseFrame.post(this); 197 } 198 } 199 200 @Override 201 public void run() { 202 setSelection(mPosition, mSmooth); 203 reset(); 204 } 205 206 private void reset() { 207 mPosition = -1; 208 mType = TYPE_INVALID; 209 mSmooth = false; 210 } 211 } 212 213 /** 214 * Possible set of actions that {@link BrowseSupportFragment} exposes to clients. Custom 215 * fragments can interact with {@link BrowseSupportFragment} using this interface. 216 */ 217 public interface FragmentHost { 218 /** 219 * Fragments are required to invoke this callback once their view is created 220 * inside {@link Fragment#onViewCreated} method. {@link BrowseSupportFragment} starts the entrance 221 * animation only after receiving this callback. Failure to invoke this method 222 * will lead to fragment not showing up. 223 * 224 * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment. 225 */ 226 void notifyViewCreated(MainFragmentAdapter fragmentAdapter); 227 228 /** 229 * Fragments mapped to {@link PageRow} are required to invoke this callback once their data 230 * is created for transition, the entrance animation only after receiving this callback. 231 * Failure to invoke this method will lead to fragment not showing up. 232 * 233 * @param fragmentAdapter {@link MainFragmentAdapter} used by the current fragment. 234 */ 235 void notifyDataReady(MainFragmentAdapter fragmentAdapter); 236 237 /** 238 * Show or hide title view in {@link BrowseSupportFragment} for fragments mapped to 239 * {@link PageRow}. Otherwise the request is ignored, in that case BrowseSupportFragment is fully 240 * in control of showing/hiding title view. 241 * <p> 242 * When HeadersSupportFragment is visible, BrowseSupportFragment will hide search affordance view if 243 * there are other focusable rows above currently focused row. 244 * 245 * @param show Boolean indicating whether or not to show the title view. 246 */ 247 void showTitleView(boolean show); 248 } 249 250 /** 251 * Default implementation of {@link FragmentHost} that is used only by 252 * {@link BrowseSupportFragment}. 253 */ 254 private final class FragmentHostImpl implements FragmentHost { 255 boolean mShowTitleView = true; 256 boolean mDataReady = false; 257 258 @Override 259 public void notifyViewCreated(MainFragmentAdapter fragmentAdapter) { 260 performPendingStates(); 261 } 262 263 @Override 264 public void notifyDataReady(MainFragmentAdapter fragmentAdapter) { 265 mDataReady = true; 266 267 // If fragment host is not the currently active fragment (in BrowseSupportFragment), then 268 // ignore the request. 269 if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) { 270 return; 271 } 272 273 // We only honor showTitle request for PageRows. 274 if (!mIsPageRow) { 275 return; 276 } 277 278 performPendingStates(); 279 } 280 281 @Override 282 public void showTitleView(boolean show) { 283 mShowTitleView = show; 284 285 // If fragment host is not the currently active fragment (in BrowseSupportFragment), then 286 // ignore the request. 287 if (mMainFragmentAdapter == null || mMainFragmentAdapter.getFragmentHost() != this) { 288 return; 289 } 290 291 // We only honor showTitle request for PageRows. 292 if (!mIsPageRow) { 293 return; 294 } 295 296 updateTitleViewVisibility(); 297 } 298 } 299 300 /** 301 * Interface that defines the interaction between {@link BrowseSupportFragment} and it's main 302 * content fragment. The key method is {@link MainFragmentAdapter#getFragment()}, 303 * it will be used to get the fragment to be shown in the content section. Clients can 304 * provide any implementation of fragment and customize it's interaction with 305 * {@link BrowseSupportFragment} by overriding the necessary methods. 306 * 307 * <p> 308 * Clients are expected to provide 309 * an instance of {@link MainFragmentAdapterRegistry} which will be responsible for providing 310 * implementations of {@link MainFragmentAdapter} for given content types. Currently 311 * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype 312 * of {@link Row}. We provide an out of the box adapter implementation for any rows other than 313 * {@link PageRow} - {@link android.support.v17.leanback.app.RowsSupportFragment.MainFragmentAdapter}. 314 * 315 * <p> 316 * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment 317 * design. Users will have to provide an implementation of {@link MainFragmentAdapter} 318 * and provide that through {@link MainFragmentAdapterRegistry}. 319 * {@link MainFragmentAdapter} implementation can supply any fragment and override 320 * just those interactions that makes sense. 321 */ 322 public static class MainFragmentAdapter<T extends Fragment> { 323 private boolean mScalingEnabled; 324 private final T mFragment; 325 private FragmentHostImpl mFragmentHost; 326 327 public MainFragmentAdapter(T fragment) { 328 this.mFragment = fragment; 329 } 330 331 public final T getFragment() { 332 return mFragment; 333 } 334 335 /** 336 * Returns whether its scrolling. 337 */ 338 public boolean isScrolling() { 339 return false; 340 } 341 342 /** 343 * Set the visibility of titles/hovercard of browse rows. 344 */ 345 public void setExpand(boolean expand) { 346 } 347 348 /** 349 * For rows that willing to participate entrance transition, this function 350 * hide views if afterTransition is true, show views if afterTransition is false. 351 */ 352 public void setEntranceTransitionState(boolean state) { 353 } 354 355 /** 356 * Sets the window alignment and also the pivots for scale operation. 357 */ 358 public void setAlignment(int windowAlignOffsetFromTop) { 359 } 360 361 /** 362 * Callback indicating transition prepare start. 363 */ 364 public boolean onTransitionPrepare() { 365 return false; 366 } 367 368 /** 369 * Callback indicating transition start. 370 */ 371 public void onTransitionStart() { 372 } 373 374 /** 375 * Callback indicating transition end. 376 */ 377 public void onTransitionEnd() { 378 } 379 380 /** 381 * Returns whether row scaling is enabled. 382 */ 383 public boolean isScalingEnabled() { 384 return mScalingEnabled; 385 } 386 387 /** 388 * Sets the row scaling property. 389 */ 390 public void setScalingEnabled(boolean scalingEnabled) { 391 this.mScalingEnabled = scalingEnabled; 392 } 393 394 /** 395 * Returns the current host interface so that main fragment can interact with 396 * {@link BrowseSupportFragment}. 397 */ 398 public final FragmentHost getFragmentHost() { 399 return mFragmentHost; 400 } 401 402 void setFragmentHost(FragmentHostImpl fragmentHost) { 403 this.mFragmentHost = fragmentHost; 404 } 405 } 406 407 /** 408 * Interface to be implemented by all fragments for providing an instance of 409 * {@link MainFragmentAdapter}. Both {@link RowsSupportFragment} and custom fragment provided 410 * against {@link PageRow} will need to implement this interface. 411 */ 412 public interface MainFragmentAdapterProvider { 413 /** 414 * Returns an instance of {@link MainFragmentAdapter} that {@link BrowseSupportFragment} 415 * would use to communicate with the target fragment. 416 */ 417 MainFragmentAdapter getMainFragmentAdapter(); 418 } 419 420 /** 421 * Interface to be implemented by {@link RowsSupportFragment} and it's subclasses for providing 422 * an instance of {@link MainFragmentRowsAdapter}. 423 */ 424 public interface MainFragmentRowsAdapterProvider { 425 /** 426 * Returns an instance of {@link MainFragmentRowsAdapter} that {@link BrowseSupportFragment} 427 * would use to communicate with the target fragment. 428 */ 429 MainFragmentRowsAdapter getMainFragmentRowsAdapter(); 430 } 431 432 /** 433 * This is used to pass information to {@link RowsSupportFragment} or its subclasses. 434 * {@link BrowseSupportFragment} uses this interface to pass row based interaction events to 435 * the target fragment. 436 */ 437 public static class MainFragmentRowsAdapter<T extends Fragment> { 438 private final T mFragment; 439 440 public MainFragmentRowsAdapter(T fragment) { 441 if (fragment == null) { 442 throw new IllegalArgumentException("Fragment can't be null"); 443 } 444 this.mFragment = fragment; 445 } 446 447 public final T getFragment() { 448 return mFragment; 449 } 450 /** 451 * Set the visibility titles/hover of browse rows. 452 */ 453 public void setAdapter(ObjectAdapter adapter) { 454 } 455 456 /** 457 * Sets an item clicked listener on the fragment. 458 */ 459 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 460 } 461 462 /** 463 * Sets an item selection listener. 464 */ 465 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 466 } 467 468 /** 469 * Selects a Row and perform an optional task on the Row. 470 */ 471 public void setSelectedPosition(int rowPosition, 472 boolean smooth, 473 final Presenter.ViewHolderTask rowHolderTask) { 474 } 475 476 /** 477 * Selects a Row. 478 */ 479 public void setSelectedPosition(int rowPosition, boolean smooth) { 480 } 481 482 /** 483 * Returns the selected position. 484 */ 485 public int getSelectedPosition() { 486 return 0; 487 } 488 } 489 490 private boolean createMainFragment(ObjectAdapter adapter, int position) { 491 Object item = null; 492 if (adapter == null || adapter.size() == 0) { 493 return false; 494 } else { 495 if (position < 0) { 496 position = 0; 497 } else if (position >= adapter.size()) { 498 throw new IllegalArgumentException( 499 String.format("Invalid position %d requested", position)); 500 } 501 item = adapter.get(position); 502 } 503 504 mSelectedPosition = position; 505 boolean oldIsPageRow = mIsPageRow; 506 mIsPageRow = item instanceof PageRow; 507 boolean swap; 508 509 if (mMainFragment == null) { 510 swap = true; 511 } else { 512 if (oldIsPageRow) { 513 swap = true; 514 } else { 515 swap = mIsPageRow; 516 } 517 } 518 519 if (swap) { 520 mMainFragment = mMainFragmentAdapterRegistry.createFragment(item); 521 if (!(mMainFragment instanceof MainFragmentAdapterProvider)) { 522 throw new IllegalArgumentException( 523 "Fragment must implement MainFragmentAdapterProvider"); 524 } 525 526 mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) 527 .getMainFragmentAdapter(); 528 mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); 529 if (!mIsPageRow) { 530 if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { 531 mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider)mMainFragment) 532 .getMainFragmentRowsAdapter(); 533 } else { 534 mMainFragmentRowsAdapter = null; 535 } 536 mIsPageRow = mMainFragmentRowsAdapter == null; 537 } else { 538 mMainFragmentRowsAdapter = null; 539 } 540 } 541 542 return swap; 543 } 544 545 /** 546 * Factory class responsible for creating fragment given the current item. {@link ListRow} 547 * should returns {@link RowsSupportFragment} or it's subclass whereas {@link PageRow} 548 * can return any fragment class. 549 */ 550 public abstract static class FragmentFactory<T extends Fragment> { 551 public abstract T createFragment(Object row); 552 } 553 554 /** 555 * FragmentFactory implementation for {@link ListRow}. 556 */ 557 public static class ListRowFragmentFactory extends FragmentFactory<RowsSupportFragment> { 558 @Override 559 public RowsSupportFragment createFragment(Object row) { 560 return new RowsSupportFragment(); 561 } 562 } 563 564 /** 565 * Registry class maintaining the mapping of {@link Row} subclasses to {@link FragmentFactory}. 566 * BrowseRowFragment automatically registers {@link ListRowFragmentFactory} for 567 * handling {@link ListRow}. Developers can override that and also if they want to 568 * use custom fragment, they can register a custom {@link FragmentFactory} 569 * against {@link PageRow}. 570 */ 571 public final static class MainFragmentAdapterRegistry { 572 private final Map<Class, FragmentFactory> mItemToFragmentFactoryMapping = new HashMap(); 573 private final static FragmentFactory sDefaultFragmentFactory = new ListRowFragmentFactory(); 574 575 public MainFragmentAdapterRegistry() { 576 registerFragment(ListRow.class, sDefaultFragmentFactory); 577 } 578 579 public void registerFragment(Class rowClass, FragmentFactory factory) { 580 mItemToFragmentFactoryMapping.put(rowClass, factory); 581 } 582 583 public Fragment createFragment(Object item) { 584 if (item == null) { 585 throw new IllegalArgumentException("Item can't be null"); 586 } 587 588 FragmentFactory fragmentFactory = mItemToFragmentFactoryMapping.get(item.getClass()); 589 if (fragmentFactory == null && !(item instanceof PageRow)) { 590 fragmentFactory = sDefaultFragmentFactory; 591 } 592 593 return fragmentFactory.createFragment(item); 594 } 595 } 596 597 private static final String TAG = "BrowseSupportFragment"; 598 599 private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_"; 600 601 private static boolean DEBUG = false; 602 603 /** The headers fragment is enabled and shown by default. */ 604 public static final int HEADERS_ENABLED = 1; 605 606 /** The headers fragment is enabled and hidden by default. */ 607 public static final int HEADERS_HIDDEN = 2; 608 609 /** The headers fragment is disabled and will never be shown. */ 610 public static final int HEADERS_DISABLED = 3; 611 612 private MainFragmentAdapterRegistry mMainFragmentAdapterRegistry 613 = new MainFragmentAdapterRegistry(); 614 private MainFragmentAdapter mMainFragmentAdapter; 615 private Fragment mMainFragment; 616 private HeadersSupportFragment mHeadersSupportFragment; 617 private MainFragmentRowsAdapter mMainFragmentRowsAdapter; 618 619 private ObjectAdapter mAdapter; 620 private PresenterSelector mAdapterPresenter; 621 private PresenterSelector mWrappingPresenterSelector; 622 623 private int mHeadersState = HEADERS_ENABLED; 624 private int mBrandColor = Color.TRANSPARENT; 625 private boolean mBrandColorSet; 626 627 private BrowseFrameLayout mBrowseFrame; 628 private ScaleFrameLayout mScaleFrameLayout; 629 private boolean mHeadersBackStackEnabled = true; 630 private String mWithHeadersBackStackName; 631 private boolean mShowingHeaders = true; 632 private boolean mCanShowHeaders = true; 633 private int mContainerListMarginStart; 634 private int mContainerListAlignTop; 635 private boolean mMainFragmentScaleEnabled = true; 636 private OnItemViewSelectedListener mExternalOnItemViewSelectedListener; 637 private OnItemViewClickedListener mOnItemViewClickedListener; 638 private int mSelectedPosition = -1; 639 private float mScaleFactor; 640 private boolean mIsPageRow; 641 642 private PresenterSelector mHeaderPresenterSelector; 643 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 644 645 // transition related: 646 private Object mSceneWithHeaders; 647 private Object mSceneWithoutHeaders; 648 private Object mSceneAfterEntranceTransition; 649 private Object mHeadersTransition; 650 private BackStackListener mBackStackChangedListener; 651 private BrowseTransitionListener mBrowseTransitionListener; 652 653 private static final String ARG_TITLE = BrowseSupportFragment.class.getCanonicalName() + ".title"; 654 private static final String ARG_HEADERS_STATE = 655 BrowseSupportFragment.class.getCanonicalName() + ".headersState"; 656 657 /** 658 * Creates arguments for a browse fragment. 659 * 660 * @param args The Bundle to place arguments into, or null if the method 661 * should return a new Bundle. 662 * @param title The title of the BrowseSupportFragment. 663 * @param headersState The initial state of the headers of the 664 * BrowseSupportFragment. Must be one of {@link #HEADERS_ENABLED}, {@link 665 * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}. 666 * @return A Bundle with the given arguments for creating a BrowseSupportFragment. 667 */ 668 public static Bundle createArgs(Bundle args, String title, int headersState) { 669 if (args == null) { 670 args = new Bundle(); 671 } 672 args.putString(ARG_TITLE, title); 673 args.putInt(ARG_HEADERS_STATE, headersState); 674 return args; 675 } 676 677 /** 678 * Sets the brand color for the browse fragment. The brand color is used as 679 * the primary color for UI elements in the browse fragment. For example, 680 * the background color of the headers fragment uses the brand color. 681 * 682 * @param color The color to use as the brand color of the fragment. 683 */ 684 public void setBrandColor(@ColorInt int color) { 685 mBrandColor = color; 686 mBrandColorSet = true; 687 688 if (mHeadersSupportFragment != null) { 689 mHeadersSupportFragment.setBackgroundColor(mBrandColor); 690 } 691 } 692 693 /** 694 * Returns the brand color for the browse fragment. 695 * The default is transparent. 696 */ 697 @ColorInt 698 public int getBrandColor() { 699 return mBrandColor; 700 } 701 702 /** 703 * Wrapping app provided PresenterSelector to support InvisibleRowPresenter for SectionRow 704 * DividerRow and PageRow. 705 */ 706 private void createAndSetWrapperPresenter() { 707 final PresenterSelector adapterPresenter = mAdapter.getPresenterSelector(); 708 if (adapterPresenter == null) { 709 throw new IllegalArgumentException("Adapter.getPresenterSelector() is null"); 710 } 711 if (adapterPresenter == mAdapterPresenter) { 712 return; 713 } 714 mAdapterPresenter = adapterPresenter; 715 716 Presenter[] presenters = adapterPresenter.getPresenters(); 717 final Presenter invisibleRowPresenter = new InvisibleRowPresenter(); 718 final Presenter[] allPresenters = new Presenter[presenters.length + 1]; 719 System.arraycopy(allPresenters, 0, presenters, 0, presenters.length); 720 allPresenters[allPresenters.length - 1] = invisibleRowPresenter; 721 mAdapter.setPresenterSelector(new PresenterSelector() { 722 @Override 723 public Presenter getPresenter(Object item) { 724 Row row = (Row) item; 725 if (row.isRenderedAsRowView()) { 726 return adapterPresenter.getPresenter(item); 727 } else { 728 return invisibleRowPresenter; 729 } 730 } 731 732 @Override 733 public Presenter[] getPresenters() { 734 return allPresenters; 735 } 736 }); 737 } 738 739 /** 740 * Sets the adapter containing the rows for the fragment. 741 * 742 * <p>The items referenced by the adapter must be be derived from 743 * {@link Row}. These rows will be used by the rows fragment and the headers 744 * fragment (if not disabled) to render the browse rows. 745 * 746 * @param adapter An ObjectAdapter for the browse rows. All items must 747 * derive from {@link Row}. 748 */ 749 public void setAdapter(ObjectAdapter adapter) { 750 mAdapter = adapter; 751 createAndSetWrapperPresenter(); 752 if (getView() == null) { 753 return; 754 } 755 replaceMainFragment(mSelectedPosition); 756 757 if (adapter != null) { 758 if (mMainFragmentRowsAdapter != null) { 759 mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(adapter)); 760 } 761 mHeadersSupportFragment.setAdapter(adapter); 762 } 763 } 764 765 public final MainFragmentAdapterRegistry getMainFragmentRegistry() { 766 return mMainFragmentAdapterRegistry; 767 } 768 769 /** 770 * Returns the adapter containing the rows for the fragment. 771 */ 772 public ObjectAdapter getAdapter() { 773 return mAdapter; 774 } 775 776 /** 777 * Sets an item selection listener. 778 */ 779 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 780 mExternalOnItemViewSelectedListener = listener; 781 } 782 783 /** 784 * Returns an item selection listener. 785 */ 786 public OnItemViewSelectedListener getOnItemViewSelectedListener() { 787 return mExternalOnItemViewSelectedListener; 788 } 789 790 /** 791 * Get RowsSupportFragment if it's bound to BrowseSupportFragment or null if either BrowseSupportFragment has 792 * not been created yet or a different fragment is bound to it. 793 * 794 * @return RowsSupportFragment if it's bound to BrowseSupportFragment or null otherwise. 795 */ 796 public RowsSupportFragment getRowsSupportFragment() { 797 if (mMainFragment instanceof RowsSupportFragment) { 798 return (RowsSupportFragment) mMainFragment; 799 } 800 801 return null; 802 } 803 804 /** 805 * @return Current main fragment or null if not created. 806 */ 807 public Fragment getMainFragment() { 808 return mMainFragment; 809 } 810 811 /** 812 * Get currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet. 813 * @return Currently bound HeadersSupportFragment or null if HeadersSupportFragment has not been created yet. 814 */ 815 public HeadersSupportFragment getHeadersSupportFragment() { 816 return mHeadersSupportFragment; 817 } 818 819 /** 820 * Sets an item clicked listener on the fragment. 821 * OnItemViewClickedListener will override {@link View.OnClickListener} that 822 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 823 * So in general, developer should choose one of the listeners but not both. 824 */ 825 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 826 mOnItemViewClickedListener = listener; 827 if (mMainFragmentRowsAdapter != null) { 828 mMainFragmentRowsAdapter.setOnItemViewClickedListener(listener); 829 } 830 } 831 832 /** 833 * Returns the item Clicked listener. 834 */ 835 public OnItemViewClickedListener getOnItemViewClickedListener() { 836 return mOnItemViewClickedListener; 837 } 838 839 /** 840 * Starts a headers transition. 841 * 842 * <p>This method will begin a transition to either show or hide the 843 * headers, depending on the value of withHeaders. If headers are disabled 844 * for this browse fragment, this method will throw an exception. 845 * 846 * @param withHeaders True if the headers should transition to being shown, 847 * false if the transition should result in headers being hidden. 848 */ 849 public void startHeadersTransition(boolean withHeaders) { 850 if (!mCanShowHeaders) { 851 throw new IllegalStateException("Cannot start headers transition"); 852 } 853 if (isInHeadersTransition() || mShowingHeaders == withHeaders) { 854 return; 855 } 856 startHeadersTransitionInternal(withHeaders); 857 } 858 859 /** 860 * Returns true if the headers transition is currently running. 861 */ 862 public boolean isInHeadersTransition() { 863 return mHeadersTransition != null; 864 } 865 866 /** 867 * Returns true if headers are shown. 868 */ 869 public boolean isShowingHeaders() { 870 return mShowingHeaders; 871 } 872 873 /** 874 * Sets a listener for browse fragment transitions. 875 * 876 * @param listener The listener to call when a browse headers transition 877 * begins or ends. 878 */ 879 public void setBrowseTransitionListener(BrowseTransitionListener listener) { 880 mBrowseTransitionListener = listener; 881 } 882 883 /** 884 * @deprecated use {@link BrowseSupportFragment#enableMainFragmentScaling(boolean)} instead. 885 * 886 * @param enable true to enable row scaling 887 */ 888 @Deprecated 889 public void enableRowScaling(boolean enable) { 890 enableMainFragmentScaling(enable); 891 } 892 893 /** 894 * Enables scaling of main fragment when headers are present. For the page/row fragment, 895 * scaling is enabled only when both this method and 896 * {@link MainFragmentAdapter#isScalingEnabled()} are enabled. 897 * 898 * @param enable true to enable row scaling 899 */ 900 public void enableMainFragmentScaling(boolean enable) { 901 mMainFragmentScaleEnabled = enable; 902 } 903 904 private void startHeadersTransitionInternal(final boolean withHeaders) { 905 if (getFragmentManager().isDestroyed()) { 906 return; 907 } 908 if (!isHeadersDataReady()) { 909 return; 910 } 911 mShowingHeaders = withHeaders; 912 mMainFragmentAdapter.onTransitionPrepare(); 913 mMainFragmentAdapter.onTransitionStart(); 914 onExpandTransitionStart(!withHeaders, new Runnable() { 915 @Override 916 public void run() { 917 mHeadersSupportFragment.onTransitionPrepare(); 918 mHeadersSupportFragment.onTransitionStart(); 919 createHeadersTransition(); 920 if (mBrowseTransitionListener != null) { 921 mBrowseTransitionListener.onHeadersTransitionStart(withHeaders); 922 } 923 TransitionHelper.runTransition( 924 withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition); 925 if (mHeadersBackStackEnabled) { 926 if (!withHeaders) { 927 getFragmentManager().beginTransaction() 928 .addToBackStack(mWithHeadersBackStackName).commit(); 929 } else { 930 int index = mBackStackChangedListener.mIndexOfHeadersBackStack; 931 if (index >= 0) { 932 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index); 933 getFragmentManager().popBackStackImmediate(entry.getId(), 934 FragmentManager.POP_BACK_STACK_INCLUSIVE); 935 } 936 } 937 } 938 } 939 }); 940 } 941 942 boolean isVerticalScrolling() { 943 // don't run transition 944 return mHeadersSupportFragment.isScrolling() || mMainFragmentAdapter.isScrolling(); 945 } 946 947 948 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 949 new BrowseFrameLayout.OnFocusSearchListener() { 950 @Override 951 public View onFocusSearch(View focused, int direction) { 952 // if headers is running transition, focus stays 953 if (mCanShowHeaders && isInHeadersTransition()) { 954 return focused; 955 } 956 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 957 958 if (getTitleView() != null && focused != getTitleView() && 959 direction == View.FOCUS_UP) { 960 return getTitleView(); 961 } 962 if (getTitleView() != null && getTitleView().hasFocus() && 963 direction == View.FOCUS_DOWN) { 964 return mCanShowHeaders && mShowingHeaders ? 965 mHeadersSupportFragment.getVerticalGridView() : mMainFragment.getView(); 966 } 967 968 boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; 969 int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 970 int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; 971 if (mCanShowHeaders && direction == towardStart) { 972 if (isVerticalScrolling() || mShowingHeaders || !isHeadersDataReady()) { 973 return focused; 974 } 975 return mHeadersSupportFragment.getVerticalGridView(); 976 } else if (direction == towardEnd) { 977 if (isVerticalScrolling()) { 978 return focused; 979 } else if (mMainFragment != null && mMainFragment.getView() != null) { 980 return mMainFragment.getView(); 981 } 982 return focused; 983 } else if (direction == View.FOCUS_DOWN && mShowingHeaders) { 984 // disable focus_down moving into PageFragment. 985 return focused; 986 } else { 987 return null; 988 } 989 } 990 }; 991 992 private final boolean isHeadersDataReady() { 993 return mAdapter != null && mAdapter.size() != 0; 994 } 995 996 private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = 997 new BrowseFrameLayout.OnChildFocusListener() { 998 999 @Override 1000 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 1001 if (getChildFragmentManager().isDestroyed()) { 1002 return true; 1003 } 1004 // Make sure not changing focus when requestFocus() is called. 1005 if (mCanShowHeaders && mShowingHeaders) { 1006 if (mHeadersSupportFragment != null && mHeadersSupportFragment.getView() != null && 1007 mHeadersSupportFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 1008 return true; 1009 } 1010 } 1011 if (mMainFragment != null && mMainFragment.getView() != null && 1012 mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 1013 return true; 1014 } 1015 if (getTitleView() != null && 1016 getTitleView().requestFocus(direction, previouslyFocusedRect)) { 1017 return true; 1018 } 1019 return false; 1020 } 1021 1022 @Override 1023 public void onRequestChildFocus(View child, View focused) { 1024 if (getChildFragmentManager().isDestroyed()) { 1025 return; 1026 } 1027 if (!mCanShowHeaders || isInHeadersTransition()) return; 1028 int childId = child.getId(); 1029 if (childId == R.id.browse_container_dock && mShowingHeaders) { 1030 startHeadersTransitionInternal(false); 1031 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 1032 startHeadersTransitionInternal(true); 1033 } 1034 } 1035 }; 1036 1037 @Override 1038 public void onSaveInstanceState(Bundle outState) { 1039 super.onSaveInstanceState(outState); 1040 outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition); 1041 outState.putBoolean(IS_PAGE_ROW, mIsPageRow); 1042 1043 if (mBackStackChangedListener != null) { 1044 mBackStackChangedListener.save(outState); 1045 } else { 1046 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 1047 } 1048 } 1049 1050 @Override 1051 public void onCreate(Bundle savedInstanceState) { 1052 super.onCreate(savedInstanceState); 1053 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 1054 mContainerListMarginStart = (int) ta.getDimension( 1055 R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() 1056 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); 1057 mContainerListAlignTop = (int) ta.getDimension( 1058 R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() 1059 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); 1060 ta.recycle(); 1061 1062 readArguments(getArguments()); 1063 1064 if (mCanShowHeaders) { 1065 if (mHeadersBackStackEnabled) { 1066 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 1067 mBackStackChangedListener = new BackStackListener(); 1068 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 1069 mBackStackChangedListener.load(savedInstanceState); 1070 } else { 1071 if (savedInstanceState != null) { 1072 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 1073 } 1074 } 1075 } 1076 1077 mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1); 1078 } 1079 1080 @Override 1081 public void onDestroyView() { 1082 mMainFragmentRowsAdapter = null; 1083 mMainFragmentAdapter = null; 1084 mMainFragment = null; 1085 mHeadersSupportFragment = null; 1086 super.onDestroyView(); 1087 } 1088 1089 @Override 1090 public void onDestroy() { 1091 if (mBackStackChangedListener != null) { 1092 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 1093 } 1094 super.onDestroy(); 1095 } 1096 1097 @Override 1098 public View onCreateView(LayoutInflater inflater, ViewGroup container, 1099 Bundle savedInstanceState) { 1100 1101 if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) { 1102 mHeadersSupportFragment = new HeadersSupportFragment(); 1103 1104 createMainFragment(mAdapter, mSelectedPosition); 1105 FragmentTransaction ft = getChildFragmentManager().beginTransaction() 1106 .replace(R.id.browse_headers_dock, mHeadersSupportFragment); 1107 1108 if (mMainFragment != null) { 1109 ft.replace(R.id.scale_frame, mMainFragment); 1110 } else { 1111 // Empty adapter used to guard against lazy adapter loading. When this 1112 // fragment is instantiated, mAdapter might not have the data or might not 1113 // have been set. In either of those cases mFragmentAdapter will be null. 1114 // This way we can maintain the invariant that mMainFragmentAdapter is never 1115 // null and it avoids doing null checks all over the code. 1116 mMainFragmentAdapter = new MainFragmentAdapter(null); 1117 mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); 1118 } 1119 1120 ft.commit(); 1121 } else { 1122 mHeadersSupportFragment = (HeadersSupportFragment) getChildFragmentManager() 1123 .findFragmentById(R.id.browse_headers_dock); 1124 mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame); 1125 mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) 1126 .getMainFragmentAdapter(); 1127 mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); 1128 1129 mIsPageRow = savedInstanceState != null ? 1130 savedInstanceState.getBoolean(IS_PAGE_ROW, false) : false; 1131 1132 mSelectedPosition = savedInstanceState != null ? 1133 savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0; 1134 1135 if (!mIsPageRow) { 1136 if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { 1137 mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment) 1138 .getMainFragmentRowsAdapter(); 1139 } else { 1140 mMainFragmentRowsAdapter = null; 1141 } 1142 } else { 1143 mMainFragmentRowsAdapter = null; 1144 } 1145 } 1146 1147 mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders); 1148 if (mHeaderPresenterSelector != null) { 1149 mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector); 1150 } 1151 mHeadersSupportFragment.setAdapter(mAdapter); 1152 mHeadersSupportFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener); 1153 mHeadersSupportFragment.setOnHeaderClickedListener(mHeaderClickedListener); 1154 1155 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 1156 1157 getProgressBarManager().setRootView((ViewGroup)root); 1158 1159 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 1160 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 1161 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 1162 1163 installTitleView(inflater, mBrowseFrame, savedInstanceState); 1164 1165 mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame); 1166 mScaleFrameLayout.setPivotX(0); 1167 mScaleFrameLayout.setPivotY(mContainerListAlignTop); 1168 1169 setupMainFragment(); 1170 1171 if (mBrandColorSet) { 1172 mHeadersSupportFragment.setBackgroundColor(mBrandColor); 1173 } 1174 1175 mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 1176 @Override 1177 public void run() { 1178 showHeaders(true); 1179 } 1180 }); 1181 mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 1182 @Override 1183 public void run() { 1184 showHeaders(false); 1185 } 1186 }); 1187 mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 1188 @Override 1189 public void run() { 1190 setEntranceTransitionEndState(); 1191 } 1192 }); 1193 1194 return root; 1195 } 1196 1197 private void setupMainFragment() { 1198 if (mMainFragmentRowsAdapter != null) { 1199 mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter)); 1200 mMainFragmentRowsAdapter.setOnItemViewSelectedListener( 1201 new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); 1202 mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); 1203 } 1204 } 1205 1206 @Override 1207 boolean isReadyForPrepareEntranceTransition() { 1208 return mMainFragment != null && mMainFragment.getView() != null; 1209 } 1210 1211 @Override 1212 boolean isReadyForStartEntranceTransition() { 1213 return mMainFragment != null && mMainFragment.getView() != null 1214 && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady); 1215 } 1216 1217 private void createHeadersTransition() { 1218 mHeadersTransition = TransitionHelper.loadTransition(getActivity(), 1219 mShowingHeaders ? 1220 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 1221 1222 TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() { 1223 @Override 1224 public void onTransitionStart(Object transition) { 1225 } 1226 @Override 1227 public void onTransitionEnd(Object transition) { 1228 mHeadersTransition = null; 1229 if (mMainFragmentAdapter != null) { 1230 mMainFragmentAdapter.onTransitionEnd(); 1231 if (!mShowingHeaders && mMainFragment != null) { 1232 View mainFragmentView = mMainFragment.getView(); 1233 if (mainFragmentView != null && !mainFragmentView.hasFocus()) { 1234 mainFragmentView.requestFocus(); 1235 } 1236 } 1237 } 1238 if (mHeadersSupportFragment != null) { 1239 mHeadersSupportFragment.onTransitionEnd(); 1240 if (mShowingHeaders) { 1241 VerticalGridView headerGridView = mHeadersSupportFragment.getVerticalGridView(); 1242 if (headerGridView != null && !headerGridView.hasFocus()) { 1243 headerGridView.requestFocus(); 1244 } 1245 } 1246 } 1247 1248 // Animate TitleView once header animation is complete. 1249 updateTitleViewVisibility(); 1250 1251 if (mBrowseTransitionListener != null) { 1252 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 1253 } 1254 } 1255 }); 1256 } 1257 1258 void updateTitleViewVisibility() { 1259 if (!mShowingHeaders) { 1260 boolean showTitleView; 1261 if (mIsPageRow && mMainFragmentAdapter != null) { 1262 // page fragment case: 1263 showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView; 1264 } else { 1265 // regular row view case: 1266 showTitleView = isFirstRowWithContent(mSelectedPosition); 1267 } 1268 if (showTitleView) { 1269 showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE); 1270 } else { 1271 showTitle(false); 1272 } 1273 } else { 1274 // when HeaderFragment is showing, showBranding and showSearch are slightly different 1275 boolean showBranding; 1276 boolean showSearch; 1277 if (mIsPageRow && mMainFragmentAdapter != null) { 1278 showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView; 1279 } else { 1280 showBranding = isFirstRowWithContent(mSelectedPosition); 1281 } 1282 showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition); 1283 int flags = 0; 1284 if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE; 1285 if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE; 1286 if (flags != 0) { 1287 showTitle(flags); 1288 } else { 1289 showTitle(false); 1290 } 1291 } 1292 } 1293 1294 boolean isFirstRowWithContentOrPageRow(int rowPosition) { 1295 if (mAdapter == null || mAdapter.size() == 0) { 1296 return true; 1297 } 1298 for (int i = 0; i < mAdapter.size(); i++) { 1299 final Row row = (Row) mAdapter.get(i); 1300 if (row.isRenderedAsRowView() || row instanceof PageRow) { 1301 return rowPosition == i; 1302 } 1303 } 1304 return true; 1305 } 1306 1307 boolean isFirstRowWithContent(int rowPosition) { 1308 if (mAdapter == null || mAdapter.size() == 0) { 1309 return true; 1310 } 1311 for (int i = 0; i < mAdapter.size(); i++) { 1312 final Row row = (Row) mAdapter.get(i); 1313 if (row.isRenderedAsRowView()) { 1314 return rowPosition == i; 1315 } 1316 } 1317 return true; 1318 } 1319 1320 /** 1321 * Sets the {@link PresenterSelector} used to render the row headers. 1322 * 1323 * @param headerPresenterSelector The PresenterSelector that will determine 1324 * the Presenter for each row header. 1325 */ 1326 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 1327 mHeaderPresenterSelector = headerPresenterSelector; 1328 if (mHeadersSupportFragment != null) { 1329 mHeadersSupportFragment.setPresenterSelector(mHeaderPresenterSelector); 1330 } 1331 } 1332 1333 private void setHeadersOnScreen(boolean onScreen) { 1334 MarginLayoutParams lp; 1335 View containerList; 1336 containerList = mHeadersSupportFragment.getView(); 1337 lp = (MarginLayoutParams) containerList.getLayoutParams(); 1338 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1339 containerList.setLayoutParams(lp); 1340 } 1341 1342 private void showHeaders(boolean show) { 1343 if (DEBUG) Log.v(TAG, "showHeaders " + show); 1344 mHeadersSupportFragment.setHeadersEnabled(show); 1345 setHeadersOnScreen(show); 1346 expandMainFragment(!show); 1347 } 1348 1349 private void expandMainFragment(boolean expand) { 1350 MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams(); 1351 params.setMarginStart(!expand ? mContainerListMarginStart : 0); 1352 mScaleFrameLayout.setLayoutParams(params); 1353 mMainFragmentAdapter.setExpand(expand); 1354 1355 setMainFragmentAlignment(); 1356 final float scaleFactor = !expand 1357 && mMainFragmentScaleEnabled 1358 && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1; 1359 mScaleFrameLayout.setLayoutScaleY(scaleFactor); 1360 mScaleFrameLayout.setChildScale(scaleFactor); 1361 } 1362 1363 private HeadersSupportFragment.OnHeaderClickedListener mHeaderClickedListener = 1364 new HeadersSupportFragment.OnHeaderClickedListener() { 1365 @Override 1366 public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 1367 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 1368 return; 1369 } 1370 startHeadersTransitionInternal(false); 1371 mMainFragment.getView().requestFocus(); 1372 } 1373 }; 1374 1375 class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener { 1376 MainFragmentRowsAdapter mMainFragmentRowsAdapter; 1377 1378 public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) { 1379 mMainFragmentRowsAdapter = fragmentRowsAdapter; 1380 } 1381 1382 @Override 1383 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 1384 RowPresenter.ViewHolder rowViewHolder, Row row) { 1385 int position = mMainFragmentRowsAdapter.getSelectedPosition(); 1386 if (DEBUG) Log.v(TAG, "row selected position " + position); 1387 onRowSelected(position); 1388 if (mExternalOnItemViewSelectedListener != null) { 1389 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 1390 rowViewHolder, row); 1391 } 1392 } 1393 }; 1394 1395 private HeadersSupportFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener = 1396 new HeadersSupportFragment.OnHeaderViewSelectedListener() { 1397 @Override 1398 public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 1399 int position = mHeadersSupportFragment.getSelectedPosition(); 1400 if (DEBUG) Log.v(TAG, "header selected position " + position); 1401 onRowSelected(position); 1402 } 1403 }; 1404 1405 private void onRowSelected(int position) { 1406 if (position != mSelectedPosition) { 1407 mSetSelectionRunnable.post( 1408 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 1409 } 1410 } 1411 1412 private void setSelection(int position, boolean smooth) { 1413 if (position == NO_POSITION) { 1414 return; 1415 } 1416 1417 mHeadersSupportFragment.setSelectedPosition(position, smooth); 1418 replaceMainFragment(position); 1419 1420 if (mMainFragmentRowsAdapter != null) { 1421 mMainFragmentRowsAdapter.setSelectedPosition(position, smooth); 1422 } 1423 mSelectedPosition = position; 1424 1425 updateTitleViewVisibility(); 1426 } 1427 1428 private void replaceMainFragment(int position) { 1429 if (createMainFragment(mAdapter, position)) { 1430 swapToMainFragment(); 1431 expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); 1432 setupMainFragment(); 1433 performPendingStates(); 1434 } 1435 } 1436 1437 private void swapToMainFragment() { 1438 final VerticalGridView gridView = mHeadersSupportFragment.getVerticalGridView(); 1439 if (isShowingHeaders() && gridView != null 1440 && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 1441 // if user is scrolling HeadersSupportFragment, swap to empty fragment and wait scrolling 1442 // finishes. 1443 getChildFragmentManager().beginTransaction() 1444 .replace(R.id.scale_frame, new Fragment()).commit(); 1445 gridView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1446 @Override 1447 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1448 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 1449 gridView.removeOnScrollListener(this); 1450 FragmentManager fm = getChildFragmentManager(); 1451 Fragment currentFragment = fm.findFragmentById(R.id.scale_frame); 1452 if (currentFragment != mMainFragment) { 1453 fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit(); 1454 } 1455 } 1456 } 1457 }); 1458 } else { 1459 // Otherwise swap immediately 1460 getChildFragmentManager().beginTransaction() 1461 .replace(R.id.scale_frame, mMainFragment).commit(); 1462 } 1463 } 1464 1465 /** 1466 * Sets the selected row position with smooth animation. 1467 */ 1468 public void setSelectedPosition(int position) { 1469 setSelectedPosition(position, true); 1470 } 1471 1472 /** 1473 * Gets position of currently selected row. 1474 * @return Position of currently selected row. 1475 */ 1476 public int getSelectedPosition() { 1477 return mSelectedPosition; 1478 } 1479 1480 /** 1481 * Sets the selected row position. 1482 */ 1483 public void setSelectedPosition(int position, boolean smooth) { 1484 mSetSelectionRunnable.post( 1485 position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); 1486 } 1487 1488 /** 1489 * Selects a Row and perform an optional task on the Row. For example 1490 * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code> 1491 * scrolls to 11th row and selects 6th item on that row. The method will be ignored if 1492 * RowsSupportFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, 1493 * ViewGroup, Bundle)}). 1494 * 1495 * @param rowPosition Which row to select. 1496 * @param smooth True to scroll to the row, false for no animation. 1497 * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers 1498 * fragment will be collapsed. 1499 */ 1500 public void setSelectedPosition(int rowPosition, boolean smooth, 1501 final Presenter.ViewHolderTask rowHolderTask) { 1502 if (mMainFragmentAdapterRegistry == null) { 1503 return; 1504 } 1505 if (rowHolderTask != null) { 1506 startHeadersTransition(false); 1507 } 1508 if (mMainFragmentRowsAdapter != null) { 1509 mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask); 1510 } 1511 } 1512 1513 @Override 1514 public void onStart() { 1515 super.onStart(); 1516 mHeadersSupportFragment.setAlignment(mContainerListAlignTop); 1517 setMainFragmentAlignment(); 1518 1519 if (mCanShowHeaders && mShowingHeaders && mHeadersSupportFragment.getView() != null) { 1520 mHeadersSupportFragment.getView().requestFocus(); 1521 } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment.getView() != null) { 1522 mMainFragment.getView().requestFocus(); 1523 } 1524 1525 if (mCanShowHeaders) { 1526 showHeaders(mShowingHeaders); 1527 } 1528 1529 if (isEntranceTransitionEnabled()) { 1530 setEntranceTransitionStartState(); 1531 } 1532 } 1533 1534 private void onExpandTransitionStart(boolean expand, final Runnable callback) { 1535 if (expand) { 1536 callback.run(); 1537 return; 1538 } 1539 // Run a "pre" layout when we go non-expand, in order to get the initial 1540 // positions of added rows. 1541 new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute(); 1542 } 1543 1544 private void setMainFragmentAlignment() { 1545 int alignOffset = mContainerListAlignTop; 1546 if (mMainFragmentScaleEnabled 1547 && mMainFragmentAdapter.isScalingEnabled() 1548 && mShowingHeaders) { 1549 alignOffset = (int) (alignOffset / mScaleFactor + 0.5f); 1550 } 1551 mMainFragmentAdapter.setAlignment(alignOffset); 1552 } 1553 1554 /** 1555 * Enables/disables headers transition on back key support. This is enabled by 1556 * default. The BrowseSupportFragment will add a back stack entry when headers are 1557 * showing. Running a headers transition when the back key is pressed only 1558 * works when the headers state is {@link #HEADERS_ENABLED} or 1559 * {@link #HEADERS_HIDDEN}. 1560 * <p> 1561 * NOTE: If an Activity has its own onBackPressed() handling, you must 1562 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 1563 * and {@link BrowseTransitionListener} in your own back stack handling. 1564 */ 1565 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 1566 mHeadersBackStackEnabled = headersBackStackEnabled; 1567 } 1568 1569 /** 1570 * Returns true if headers transition on back key support is enabled. 1571 */ 1572 public final boolean isHeadersTransitionOnBackEnabled() { 1573 return mHeadersBackStackEnabled; 1574 } 1575 1576 private void readArguments(Bundle args) { 1577 if (args == null) { 1578 return; 1579 } 1580 if (args.containsKey(ARG_TITLE)) { 1581 setTitle(args.getString(ARG_TITLE)); 1582 } 1583 if (args.containsKey(ARG_HEADERS_STATE)) { 1584 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 1585 } 1586 } 1587 1588 /** 1589 * Sets the state for the headers column in the browse fragment. Must be one 1590 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1591 * {@link #HEADERS_DISABLED}. 1592 * 1593 * @param headersState The state of the headers for the browse fragment. 1594 */ 1595 public void setHeadersState(int headersState) { 1596 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1597 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1598 } 1599 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1600 1601 if (headersState != mHeadersState) { 1602 mHeadersState = headersState; 1603 switch (headersState) { 1604 case HEADERS_ENABLED: 1605 mCanShowHeaders = true; 1606 mShowingHeaders = true; 1607 break; 1608 case HEADERS_HIDDEN: 1609 mCanShowHeaders = true; 1610 mShowingHeaders = false; 1611 break; 1612 case HEADERS_DISABLED: 1613 mCanShowHeaders = false; 1614 mShowingHeaders = false; 1615 break; 1616 default: 1617 Log.w(TAG, "Unknown headers state: " + headersState); 1618 break; 1619 } 1620 if (mHeadersSupportFragment != null) { 1621 mHeadersSupportFragment.setHeadersGone(!mCanShowHeaders); 1622 } 1623 } 1624 } 1625 1626 /** 1627 * Returns the state of the headers column in the browse fragment. 1628 */ 1629 public int getHeadersState() { 1630 return mHeadersState; 1631 } 1632 1633 @Override 1634 protected Object createEntranceTransition() { 1635 return TransitionHelper.loadTransition(getActivity(), 1636 R.transition.lb_browse_entrance_transition); 1637 } 1638 1639 @Override 1640 protected void runEntranceTransition(Object entranceTransition) { 1641 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 1642 } 1643 1644 @Override 1645 protected void onEntranceTransitionPrepare() { 1646 mHeadersSupportFragment.onTransitionPrepare(); 1647 // setEntranceTransitionStartState() might be called when mMainFragment is null, 1648 // make sure it is called. 1649 mMainFragmentAdapter.setEntranceTransitionState(false); 1650 mMainFragmentAdapter.onTransitionPrepare(); 1651 } 1652 1653 @Override 1654 protected void onEntranceTransitionStart() { 1655 mHeadersSupportFragment.onTransitionStart(); 1656 mMainFragmentAdapter.onTransitionStart(); 1657 } 1658 1659 @Override 1660 protected void onEntranceTransitionEnd() { 1661 if (mMainFragmentAdapter != null) { 1662 mMainFragmentAdapter.onTransitionEnd(); 1663 } 1664 1665 if (mHeadersSupportFragment != null) { 1666 mHeadersSupportFragment.onTransitionEnd(); 1667 } 1668 } 1669 1670 void setSearchOrbViewOnScreen(boolean onScreen) { 1671 View searchOrbView = getTitleViewAdapter().getSearchAffordanceView(); 1672 if (searchOrbView != null) { 1673 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1674 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1675 searchOrbView.setLayoutParams(lp); 1676 } 1677 } 1678 1679 void setEntranceTransitionStartState() { 1680 setHeadersOnScreen(false); 1681 setSearchOrbViewOnScreen(false); 1682 mMainFragmentAdapter.setEntranceTransitionState(false); 1683 } 1684 1685 void setEntranceTransitionEndState() { 1686 setHeadersOnScreen(mShowingHeaders); 1687 setSearchOrbViewOnScreen(true); 1688 mMainFragmentAdapter.setEntranceTransitionState(true); 1689 } 1690 1691 private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { 1692 1693 private final View mView; 1694 private final Runnable mCallback; 1695 private int mState; 1696 private MainFragmentAdapter mainFragmentAdapter; 1697 1698 final static int STATE_INIT = 0; 1699 final static int STATE_FIRST_DRAW = 1; 1700 final static int STATE_SECOND_DRAW = 2; 1701 1702 ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) { 1703 mView = view; 1704 mCallback = callback; 1705 mainFragmentAdapter = adapter; 1706 } 1707 1708 void execute() { 1709 mView.getViewTreeObserver().addOnPreDrawListener(this); 1710 mainFragmentAdapter.setExpand(false); 1711 mState = STATE_INIT; 1712 } 1713 1714 @Override 1715 public boolean onPreDraw() { 1716 if (getView() == null || getActivity() == null) { 1717 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1718 return true; 1719 } 1720 if (mState == STATE_INIT) { 1721 mainFragmentAdapter.setExpand(true); 1722 mState = STATE_FIRST_DRAW; 1723 } else if (mState == STATE_FIRST_DRAW) { 1724 mCallback.run(); 1725 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1726 mState = STATE_SECOND_DRAW; 1727 } 1728 return false; 1729 } 1730 } 1731} 1732