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