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