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