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