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