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