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