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