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