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