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