BrowseFragment.java revision 35d7a6c31606ed4ca38deda3cacae080a95c95c9
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 @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(new ListRowDataAdapter(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 } else if (mMainFragment != null && mMainFragment.getView() != null) { 971 return mMainFragment.getView(); 972 } 973 return focused; 974 } else if (direction == View.FOCUS_DOWN && mShowingHeaders) { 975 // disable focus_down moving into PageFragment. 976 return focused; 977 } else { 978 return null; 979 } 980 } 981 }; 982 983 private final boolean isHeadersDataReady() { 984 return mAdapter != null && mAdapter.size() != 0; 985 } 986 987 private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = 988 new BrowseFrameLayout.OnChildFocusListener() { 989 990 @Override 991 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 992 if (getChildFragmentManager().isDestroyed()) { 993 return true; 994 } 995 // Make sure not changing focus when requestFocus() is called. 996 if (mCanShowHeaders && mShowingHeaders) { 997 if (mHeadersFragment != null && mHeadersFragment.getView() != null && 998 mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 999 return true; 1000 } 1001 } 1002 if (mMainFragment != null && mMainFragment.getView() != null && 1003 mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 1004 return true; 1005 } 1006 if (getTitleView() != null && 1007 getTitleView().requestFocus(direction, previouslyFocusedRect)) { 1008 return true; 1009 } 1010 return false; 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) : 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 private 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 private 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 private void onRowSelected(int position) { 1399 if (position != mSelectedPosition) { 1400 mSetSelectionRunnable.post( 1401 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 1402 } 1403 } 1404 1405 private 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 mState = STATE_INIT; 1705 } 1706 1707 @Override 1708 public boolean onPreDraw() { 1709 if (getView() == null || getActivity() == null) { 1710 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1711 return true; 1712 } 1713 if (mState == STATE_INIT) { 1714 mainFragmentAdapter.setExpand(true); 1715 mState = STATE_FIRST_DRAW; 1716 } else if (mState == STATE_FIRST_DRAW) { 1717 mCallback.run(); 1718 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1719 mState = STATE_SECOND_DRAW; 1720 } 1721 return false; 1722 } 1723 } 1724} 1725