BrowseFragment.java revision 3536c103188b29017e1f118ff7a76f115829d11f
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 return getTitleView() != null && 1010 getTitleView().requestFocus(direction, previouslyFocusedRect); 1011 } 1012 1013 @Override 1014 public void onRequestChildFocus(View child, View focused) { 1015 if (getChildFragmentManager().isDestroyed()) { 1016 return; 1017 } 1018 if (!mCanShowHeaders || isInHeadersTransition()) return; 1019 int childId = child.getId(); 1020 if (childId == R.id.browse_container_dock && mShowingHeaders) { 1021 startHeadersTransitionInternal(false); 1022 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 1023 startHeadersTransitionInternal(true); 1024 } 1025 } 1026 }; 1027 1028 @Override 1029 public void onSaveInstanceState(Bundle outState) { 1030 super.onSaveInstanceState(outState); 1031 outState.putInt(CURRENT_SELECTED_POSITION, mSelectedPosition); 1032 outState.putBoolean(IS_PAGE_ROW, mIsPageRow); 1033 1034 if (mBackStackChangedListener != null) { 1035 mBackStackChangedListener.save(outState); 1036 } else { 1037 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 1038 } 1039 } 1040 1041 @Override 1042 public void onCreate(Bundle savedInstanceState) { 1043 super.onCreate(savedInstanceState); 1044 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 1045 mContainerListMarginStart = (int) ta.getDimension( 1046 R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() 1047 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); 1048 mContainerListAlignTop = (int) ta.getDimension( 1049 R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() 1050 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); 1051 ta.recycle(); 1052 1053 readArguments(getArguments()); 1054 1055 if (mCanShowHeaders) { 1056 if (mHeadersBackStackEnabled) { 1057 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 1058 mBackStackChangedListener = new BackStackListener(); 1059 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 1060 mBackStackChangedListener.load(savedInstanceState); 1061 } else { 1062 if (savedInstanceState != null) { 1063 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 1064 } 1065 } 1066 } 1067 1068 mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1); 1069 } 1070 1071 @Override 1072 public void onDestroyView() { 1073 mMainFragmentRowsAdapter = null; 1074 mMainFragmentAdapter = null; 1075 mMainFragment = null; 1076 mHeadersFragment = null; 1077 super.onDestroyView(); 1078 } 1079 1080 @Override 1081 public void onDestroy() { 1082 if (mBackStackChangedListener != null) { 1083 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 1084 } 1085 super.onDestroy(); 1086 } 1087 1088 @Override 1089 public View onCreateView(LayoutInflater inflater, ViewGroup container, 1090 Bundle savedInstanceState) { 1091 1092 if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) { 1093 mHeadersFragment = new HeadersFragment(); 1094 1095 createMainFragment(mAdapter, mSelectedPosition); 1096 FragmentTransaction ft = getChildFragmentManager().beginTransaction() 1097 .replace(R.id.browse_headers_dock, mHeadersFragment); 1098 1099 if (mMainFragment != null) { 1100 ft.replace(R.id.scale_frame, mMainFragment); 1101 } else { 1102 // Empty adapter used to guard against lazy adapter loading. When this 1103 // fragment is instantiated, mAdapter might not have the data or might not 1104 // have been set. In either of those cases mFragmentAdapter will be null. 1105 // This way we can maintain the invariant that mMainFragmentAdapter is never 1106 // null and it avoids doing null checks all over the code. 1107 mMainFragmentAdapter = new MainFragmentAdapter(null); 1108 mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); 1109 } 1110 1111 ft.commit(); 1112 } else { 1113 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 1114 .findFragmentById(R.id.browse_headers_dock); 1115 mMainFragment = getChildFragmentManager().findFragmentById(R.id.scale_frame); 1116 mMainFragmentAdapter = ((MainFragmentAdapterProvider)mMainFragment) 1117 .getMainFragmentAdapter(); 1118 mMainFragmentAdapter.setFragmentHost(new FragmentHostImpl()); 1119 1120 mIsPageRow = savedInstanceState != null 1121 && savedInstanceState.getBoolean(IS_PAGE_ROW, false); 1122 1123 mSelectedPosition = savedInstanceState != null ? 1124 savedInstanceState.getInt(CURRENT_SELECTED_POSITION, 0) : 0; 1125 1126 if (!mIsPageRow) { 1127 if (mMainFragment instanceof MainFragmentRowsAdapterProvider) { 1128 mMainFragmentRowsAdapter = ((MainFragmentRowsAdapterProvider) mMainFragment) 1129 .getMainFragmentRowsAdapter(); 1130 } else { 1131 mMainFragmentRowsAdapter = null; 1132 } 1133 } else { 1134 mMainFragmentRowsAdapter = null; 1135 } 1136 } 1137 1138 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 1139 if (mHeaderPresenterSelector != null) { 1140 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 1141 } 1142 mHeadersFragment.setAdapter(mAdapter); 1143 mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener); 1144 mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener); 1145 1146 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 1147 1148 getProgressBarManager().setRootView((ViewGroup)root); 1149 1150 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 1151 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 1152 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 1153 1154 installTitleView(inflater, mBrowseFrame, savedInstanceState); 1155 1156 mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame); 1157 mScaleFrameLayout.setPivotX(0); 1158 mScaleFrameLayout.setPivotY(mContainerListAlignTop); 1159 1160 setupMainFragment(); 1161 1162 if (mBrandColorSet) { 1163 mHeadersFragment.setBackgroundColor(mBrandColor); 1164 } 1165 1166 mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 1167 @Override 1168 public void run() { 1169 showHeaders(true); 1170 } 1171 }); 1172 mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 1173 @Override 1174 public void run() { 1175 showHeaders(false); 1176 } 1177 }); 1178 mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 1179 @Override 1180 public void run() { 1181 setEntranceTransitionEndState(); 1182 } 1183 }); 1184 1185 return root; 1186 } 1187 1188 private void setupMainFragment() { 1189 if (mMainFragmentRowsAdapter != null) { 1190 if (mAdapter != null) { 1191 mMainFragmentRowsAdapter.setAdapter(new ListRowDataAdapter(mAdapter)); 1192 } 1193 mMainFragmentRowsAdapter.setOnItemViewSelectedListener( 1194 new MainFragmentItemViewSelectedListener(mMainFragmentRowsAdapter)); 1195 mMainFragmentRowsAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); 1196 } 1197 } 1198 1199 @Override 1200 boolean isReadyForPrepareEntranceTransition() { 1201 return mMainFragment != null && mMainFragment.getView() != null; 1202 } 1203 1204 @Override 1205 boolean isReadyForStartEntranceTransition() { 1206 return mMainFragment != null && mMainFragment.getView() != null 1207 && (!mIsPageRow || mMainFragmentAdapter.mFragmentHost.mDataReady); 1208 } 1209 1210 void createHeadersTransition() { 1211 mHeadersTransition = TransitionHelper.loadTransition(getActivity(), 1212 mShowingHeaders ? 1213 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 1214 1215 TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() { 1216 @Override 1217 public void onTransitionStart(Object transition) { 1218 } 1219 @Override 1220 public void onTransitionEnd(Object transition) { 1221 mHeadersTransition = null; 1222 if (mMainFragmentAdapter != null) { 1223 mMainFragmentAdapter.onTransitionEnd(); 1224 if (!mShowingHeaders && mMainFragment != null) { 1225 View mainFragmentView = mMainFragment.getView(); 1226 if (mainFragmentView != null && !mainFragmentView.hasFocus()) { 1227 mainFragmentView.requestFocus(); 1228 } 1229 } 1230 } 1231 if (mHeadersFragment != null) { 1232 mHeadersFragment.onTransitionEnd(); 1233 if (mShowingHeaders) { 1234 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 1235 if (headerGridView != null && !headerGridView.hasFocus()) { 1236 headerGridView.requestFocus(); 1237 } 1238 } 1239 } 1240 1241 // Animate TitleView once header animation is complete. 1242 updateTitleViewVisibility(); 1243 1244 if (mBrowseTransitionListener != null) { 1245 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 1246 } 1247 } 1248 }); 1249 } 1250 1251 void updateTitleViewVisibility() { 1252 if (!mShowingHeaders) { 1253 boolean showTitleView; 1254 if (mIsPageRow && mMainFragmentAdapter != null) { 1255 // page fragment case: 1256 showTitleView = mMainFragmentAdapter.mFragmentHost.mShowTitleView; 1257 } else { 1258 // regular row view case: 1259 showTitleView = isFirstRowWithContent(mSelectedPosition); 1260 } 1261 if (showTitleView) { 1262 showTitle(TitleViewAdapter.FULL_VIEW_VISIBLE); 1263 } else { 1264 showTitle(false); 1265 } 1266 } else { 1267 // when HeaderFragment is showing, showBranding and showSearch are slightly different 1268 boolean showBranding; 1269 boolean showSearch; 1270 if (mIsPageRow && mMainFragmentAdapter != null) { 1271 showBranding = mMainFragmentAdapter.mFragmentHost.mShowTitleView; 1272 } else { 1273 showBranding = isFirstRowWithContent(mSelectedPosition); 1274 } 1275 showSearch = isFirstRowWithContentOrPageRow(mSelectedPosition); 1276 int flags = 0; 1277 if (showBranding) flags |= TitleViewAdapter.BRANDING_VIEW_VISIBLE; 1278 if (showSearch) flags |= TitleViewAdapter.SEARCH_VIEW_VISIBLE; 1279 if (flags != 0) { 1280 showTitle(flags); 1281 } else { 1282 showTitle(false); 1283 } 1284 } 1285 } 1286 1287 boolean isFirstRowWithContentOrPageRow(int rowPosition) { 1288 if (mAdapter == null || mAdapter.size() == 0) { 1289 return true; 1290 } 1291 for (int i = 0; i < mAdapter.size(); i++) { 1292 final Row row = (Row) mAdapter.get(i); 1293 if (row.isRenderedAsRowView() || row instanceof PageRow) { 1294 return rowPosition == i; 1295 } 1296 } 1297 return true; 1298 } 1299 1300 boolean isFirstRowWithContent(int rowPosition) { 1301 if (mAdapter == null || mAdapter.size() == 0) { 1302 return true; 1303 } 1304 for (int i = 0; i < mAdapter.size(); i++) { 1305 final Row row = (Row) mAdapter.get(i); 1306 if (row.isRenderedAsRowView()) { 1307 return rowPosition == i; 1308 } 1309 } 1310 return true; 1311 } 1312 1313 /** 1314 * Sets the {@link PresenterSelector} used to render the row headers. 1315 * 1316 * @param headerPresenterSelector The PresenterSelector that will determine 1317 * the Presenter for each row header. 1318 */ 1319 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 1320 mHeaderPresenterSelector = headerPresenterSelector; 1321 if (mHeadersFragment != null) { 1322 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 1323 } 1324 } 1325 1326 private void setHeadersOnScreen(boolean onScreen) { 1327 MarginLayoutParams lp; 1328 View containerList; 1329 containerList = mHeadersFragment.getView(); 1330 lp = (MarginLayoutParams) containerList.getLayoutParams(); 1331 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1332 containerList.setLayoutParams(lp); 1333 } 1334 1335 void showHeaders(boolean show) { 1336 if (DEBUG) Log.v(TAG, "showHeaders " + show); 1337 mHeadersFragment.setHeadersEnabled(show); 1338 setHeadersOnScreen(show); 1339 expandMainFragment(!show); 1340 } 1341 1342 private void expandMainFragment(boolean expand) { 1343 MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams(); 1344 params.setMarginStart(!expand ? mContainerListMarginStart : 0); 1345 mScaleFrameLayout.setLayoutParams(params); 1346 mMainFragmentAdapter.setExpand(expand); 1347 1348 setMainFragmentAlignment(); 1349 final float scaleFactor = !expand 1350 && mMainFragmentScaleEnabled 1351 && mMainFragmentAdapter.isScalingEnabled() ? mScaleFactor : 1; 1352 mScaleFrameLayout.setLayoutScaleY(scaleFactor); 1353 mScaleFrameLayout.setChildScale(scaleFactor); 1354 } 1355 1356 private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = 1357 new HeadersFragment.OnHeaderClickedListener() { 1358 @Override 1359 public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 1360 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 1361 return; 1362 } 1363 startHeadersTransitionInternal(false); 1364 mMainFragment.getView().requestFocus(); 1365 } 1366 }; 1367 1368 class MainFragmentItemViewSelectedListener implements OnItemViewSelectedListener { 1369 MainFragmentRowsAdapter mMainFragmentRowsAdapter; 1370 1371 public MainFragmentItemViewSelectedListener(MainFragmentRowsAdapter fragmentRowsAdapter) { 1372 mMainFragmentRowsAdapter = fragmentRowsAdapter; 1373 } 1374 1375 @Override 1376 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 1377 RowPresenter.ViewHolder rowViewHolder, Row row) { 1378 int position = mMainFragmentRowsAdapter.getSelectedPosition(); 1379 if (DEBUG) Log.v(TAG, "row selected position " + position); 1380 onRowSelected(position); 1381 if (mExternalOnItemViewSelectedListener != null) { 1382 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 1383 rowViewHolder, row); 1384 } 1385 } 1386 }; 1387 1388 private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener = 1389 new HeadersFragment.OnHeaderViewSelectedListener() { 1390 @Override 1391 public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 1392 int position = mHeadersFragment.getSelectedPosition(); 1393 if (DEBUG) Log.v(TAG, "header selected position " + position); 1394 onRowSelected(position); 1395 } 1396 }; 1397 1398 void onRowSelected(int position) { 1399 if (position != mSelectedPosition) { 1400 mSetSelectionRunnable.post( 1401 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 1402 } 1403 } 1404 1405 void setSelection(int position, boolean smooth) { 1406 if (position == NO_POSITION) { 1407 return; 1408 } 1409 1410 mHeadersFragment.setSelectedPosition(position, smooth); 1411 replaceMainFragment(position); 1412 1413 if (mMainFragmentRowsAdapter != null) { 1414 mMainFragmentRowsAdapter.setSelectedPosition(position, smooth); 1415 } 1416 mSelectedPosition = position; 1417 1418 updateTitleViewVisibility(); 1419 } 1420 1421 private void replaceMainFragment(int position) { 1422 if (createMainFragment(mAdapter, position)) { 1423 swapToMainFragment(); 1424 expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); 1425 setupMainFragment(); 1426 performPendingStates(); 1427 } 1428 } 1429 1430 private void swapToMainFragment() { 1431 final VerticalGridView gridView = mHeadersFragment.getVerticalGridView(); 1432 if (isShowingHeaders() && gridView != null 1433 && gridView.getScrollState() != RecyclerView.SCROLL_STATE_IDLE) { 1434 // if user is scrolling HeadersFragment, swap to empty fragment and wait scrolling 1435 // finishes. 1436 getChildFragmentManager().beginTransaction() 1437 .replace(R.id.scale_frame, new Fragment()).commit(); 1438 gridView.addOnScrollListener(new RecyclerView.OnScrollListener() { 1439 @Override 1440 public void onScrollStateChanged(RecyclerView recyclerView, int newState) { 1441 if (newState == RecyclerView.SCROLL_STATE_IDLE) { 1442 gridView.removeOnScrollListener(this); 1443 FragmentManager fm = getChildFragmentManager(); 1444 Fragment currentFragment = fm.findFragmentById(R.id.scale_frame); 1445 if (currentFragment != mMainFragment) { 1446 fm.beginTransaction().replace(R.id.scale_frame, mMainFragment).commit(); 1447 } 1448 } 1449 } 1450 }); 1451 } else { 1452 // Otherwise swap immediately 1453 getChildFragmentManager().beginTransaction() 1454 .replace(R.id.scale_frame, mMainFragment).commit(); 1455 } 1456 } 1457 1458 /** 1459 * Sets the selected row position with smooth animation. 1460 */ 1461 public void setSelectedPosition(int position) { 1462 setSelectedPosition(position, true); 1463 } 1464 1465 /** 1466 * Gets position of currently selected row. 1467 * @return Position of currently selected row. 1468 */ 1469 public int getSelectedPosition() { 1470 return mSelectedPosition; 1471 } 1472 1473 /** 1474 * Sets the selected row position. 1475 */ 1476 public void setSelectedPosition(int position, boolean smooth) { 1477 mSetSelectionRunnable.post( 1478 position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); 1479 } 1480 1481 /** 1482 * Selects a Row and perform an optional task on the Row. For example 1483 * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code> 1484 * scrolls to 11th row and selects 6th item on that row. The method will be ignored if 1485 * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, 1486 * ViewGroup, Bundle)}). 1487 * 1488 * @param rowPosition Which row to select. 1489 * @param smooth True to scroll to the row, false for no animation. 1490 * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers 1491 * fragment will be collapsed. 1492 */ 1493 public void setSelectedPosition(int rowPosition, boolean smooth, 1494 final Presenter.ViewHolderTask rowHolderTask) { 1495 if (mMainFragmentAdapterRegistry == null) { 1496 return; 1497 } 1498 if (rowHolderTask != null) { 1499 startHeadersTransition(false); 1500 } 1501 if (mMainFragmentRowsAdapter != null) { 1502 mMainFragmentRowsAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask); 1503 } 1504 } 1505 1506 @Override 1507 public void onStart() { 1508 super.onStart(); 1509 mHeadersFragment.setAlignment(mContainerListAlignTop); 1510 setMainFragmentAlignment(); 1511 1512 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 1513 mHeadersFragment.getView().requestFocus(); 1514 } else if ((!mCanShowHeaders || !mShowingHeaders) && mMainFragment.getView() != null) { 1515 mMainFragment.getView().requestFocus(); 1516 } 1517 1518 if (mCanShowHeaders) { 1519 showHeaders(mShowingHeaders); 1520 } 1521 1522 if (isEntranceTransitionEnabled()) { 1523 setEntranceTransitionStartState(); 1524 } 1525 } 1526 1527 private void onExpandTransitionStart(boolean expand, final Runnable callback) { 1528 if (expand) { 1529 callback.run(); 1530 return; 1531 } 1532 // Run a "pre" layout when we go non-expand, in order to get the initial 1533 // positions of added rows. 1534 new ExpandPreLayout(callback, mMainFragmentAdapter, getView()).execute(); 1535 } 1536 1537 private void setMainFragmentAlignment() { 1538 int alignOffset = mContainerListAlignTop; 1539 if (mMainFragmentScaleEnabled 1540 && mMainFragmentAdapter.isScalingEnabled() 1541 && mShowingHeaders) { 1542 alignOffset = (int) (alignOffset / mScaleFactor + 0.5f); 1543 } 1544 mMainFragmentAdapter.setAlignment(alignOffset); 1545 } 1546 1547 /** 1548 * Enables/disables headers transition on back key support. This is enabled by 1549 * default. The BrowseFragment will add a back stack entry when headers are 1550 * showing. Running a headers transition when the back key is pressed only 1551 * works when the headers state is {@link #HEADERS_ENABLED} or 1552 * {@link #HEADERS_HIDDEN}. 1553 * <p> 1554 * NOTE: If an Activity has its own onBackPressed() handling, you must 1555 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 1556 * and {@link BrowseTransitionListener} in your own back stack handling. 1557 */ 1558 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 1559 mHeadersBackStackEnabled = headersBackStackEnabled; 1560 } 1561 1562 /** 1563 * Returns true if headers transition on back key support is enabled. 1564 */ 1565 public final boolean isHeadersTransitionOnBackEnabled() { 1566 return mHeadersBackStackEnabled; 1567 } 1568 1569 private void readArguments(Bundle args) { 1570 if (args == null) { 1571 return; 1572 } 1573 if (args.containsKey(ARG_TITLE)) { 1574 setTitle(args.getString(ARG_TITLE)); 1575 } 1576 if (args.containsKey(ARG_HEADERS_STATE)) { 1577 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 1578 } 1579 } 1580 1581 /** 1582 * Sets the state for the headers column in the browse fragment. Must be one 1583 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1584 * {@link #HEADERS_DISABLED}. 1585 * 1586 * @param headersState The state of the headers for the browse fragment. 1587 */ 1588 public void setHeadersState(int headersState) { 1589 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1590 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1591 } 1592 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1593 1594 if (headersState != mHeadersState) { 1595 mHeadersState = headersState; 1596 switch (headersState) { 1597 case HEADERS_ENABLED: 1598 mCanShowHeaders = true; 1599 mShowingHeaders = true; 1600 break; 1601 case HEADERS_HIDDEN: 1602 mCanShowHeaders = true; 1603 mShowingHeaders = false; 1604 break; 1605 case HEADERS_DISABLED: 1606 mCanShowHeaders = false; 1607 mShowingHeaders = false; 1608 break; 1609 default: 1610 Log.w(TAG, "Unknown headers state: " + headersState); 1611 break; 1612 } 1613 if (mHeadersFragment != null) { 1614 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 1615 } 1616 } 1617 } 1618 1619 /** 1620 * Returns the state of the headers column in the browse fragment. 1621 */ 1622 public int getHeadersState() { 1623 return mHeadersState; 1624 } 1625 1626 @Override 1627 protected Object createEntranceTransition() { 1628 return TransitionHelper.loadTransition(getActivity(), 1629 R.transition.lb_browse_entrance_transition); 1630 } 1631 1632 @Override 1633 protected void runEntranceTransition(Object entranceTransition) { 1634 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 1635 } 1636 1637 @Override 1638 protected void onEntranceTransitionPrepare() { 1639 mHeadersFragment.onTransitionPrepare(); 1640 // setEntranceTransitionStartState() might be called when mMainFragment is null, 1641 // make sure it is called. 1642 mMainFragmentAdapter.setEntranceTransitionState(false); 1643 mMainFragmentAdapter.onTransitionPrepare(); 1644 } 1645 1646 @Override 1647 protected void onEntranceTransitionStart() { 1648 mHeadersFragment.onTransitionStart(); 1649 mMainFragmentAdapter.onTransitionStart(); 1650 } 1651 1652 @Override 1653 protected void onEntranceTransitionEnd() { 1654 if (mMainFragmentAdapter != null) { 1655 mMainFragmentAdapter.onTransitionEnd(); 1656 } 1657 1658 if (mHeadersFragment != null) { 1659 mHeadersFragment.onTransitionEnd(); 1660 } 1661 } 1662 1663 void setSearchOrbViewOnScreen(boolean onScreen) { 1664 View searchOrbView = getTitleViewAdapter().getSearchAffordanceView(); 1665 if (searchOrbView != null) { 1666 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1667 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1668 searchOrbView.setLayoutParams(lp); 1669 } 1670 } 1671 1672 void setEntranceTransitionStartState() { 1673 setHeadersOnScreen(false); 1674 setSearchOrbViewOnScreen(false); 1675 mMainFragmentAdapter.setEntranceTransitionState(false); 1676 } 1677 1678 void setEntranceTransitionEndState() { 1679 setHeadersOnScreen(mShowingHeaders); 1680 setSearchOrbViewOnScreen(true); 1681 mMainFragmentAdapter.setEntranceTransitionState(true); 1682 } 1683 1684 private class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { 1685 1686 private final View mView; 1687 private final Runnable mCallback; 1688 private int mState; 1689 private MainFragmentAdapter mainFragmentAdapter; 1690 1691 final static int STATE_INIT = 0; 1692 final static int STATE_FIRST_DRAW = 1; 1693 final static int STATE_SECOND_DRAW = 2; 1694 1695 ExpandPreLayout(Runnable callback, MainFragmentAdapter adapter, View view) { 1696 mView = view; 1697 mCallback = callback; 1698 mainFragmentAdapter = adapter; 1699 } 1700 1701 void execute() { 1702 mView.getViewTreeObserver().addOnPreDrawListener(this); 1703 mainFragmentAdapter.setExpand(false); 1704 // always trigger onPreDraw even adapter setExpand() does nothing. 1705 mView.invalidate(); 1706 mState = STATE_INIT; 1707 } 1708 1709 @Override 1710 public boolean onPreDraw() { 1711 if (getView() == null || getActivity() == null) { 1712 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1713 return true; 1714 } 1715 if (mState == STATE_INIT) { 1716 mainFragmentAdapter.setExpand(true); 1717 // always trigger onPreDraw even adapter setExpand() does nothing. 1718 mView.invalidate(); 1719 mState = STATE_FIRST_DRAW; 1720 } else if (mState == STATE_FIRST_DRAW) { 1721 mCallback.run(); 1722 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1723 mState = STATE_SECOND_DRAW; 1724 } 1725 return false; 1726 } 1727 } 1728} 1729