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