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