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