BrowseFragment.java revision 3faa5780307cf10ff0e4a1d89a9ba099cdad2e15
1/* 2 * Copyright (C) 2014 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except 5 * in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the License 10 * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express 11 * or implied. See the License for the specific language governing permissions and limitations under 12 * the License. 13 */ 14package android.support.v17.leanback.app; 15 16import android.app.Fragment; 17import android.app.FragmentManager; 18import android.app.FragmentManager.BackStackEntry; 19import android.content.res.TypedArray; 20import android.graphics.Color; 21import android.graphics.Rect; 22import android.os.Bundle; 23import android.support.annotation.ColorInt; 24import android.support.v17.leanback.R; 25import android.support.v17.leanback.transition.TransitionHelper; 26import android.support.v17.leanback.transition.TransitionListener; 27import android.support.v17.leanback.widget.BrowseFrameLayout; 28import android.support.v17.leanback.widget.ListRow; 29import android.support.v17.leanback.widget.ObjectAdapter; 30import android.support.v17.leanback.widget.OnItemViewClickedListener; 31import android.support.v17.leanback.widget.OnItemViewSelectedListener; 32import android.support.v17.leanback.widget.PageRow; 33import android.support.v17.leanback.widget.Presenter; 34import android.support.v17.leanback.widget.PresenterSelector; 35import android.support.v17.leanback.widget.Row; 36import android.support.v17.leanback.widget.RowHeaderPresenter; 37import android.support.v17.leanback.widget.RowPresenter; 38import android.support.v17.leanback.widget.ScaleFrameLayout; 39import android.support.v17.leanback.widget.TitleView; 40import android.support.v17.leanback.widget.VerticalGridView; 41import android.support.v4.view.ViewCompat; 42import android.util.Log; 43import android.view.LayoutInflater; 44import android.view.View; 45import android.view.ViewGroup; 46import android.view.ViewGroup.MarginLayoutParams; 47import android.view.ViewTreeObserver; 48 49import static android.support.v7.widget.RecyclerView.NO_POSITION; 50 51/** 52 * A fragment for creating Leanback browse screens. It is composed of a 53 * RowsFragment and a HeadersFragment. 54 * <p> 55 * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set 56 * of rows in a vertical list. The elements in this adapter must be subclasses 57 * of {@link Row}. 58 * <p> 59 * The HeadersFragment can be set to be either shown or hidden by default, or 60 * may be disabled entirely. See {@link #setHeadersState} for details. 61 * <p> 62 * By default the BrowseFragment includes support for returning to the headers 63 * when the user presses Back. For Activities that customize {@link 64 * android.app.Activity#onBackPressed()}, you must disable this default Back key support by 65 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and 66 * use {@link BrowseFragment.BrowseTransitionListener} and 67 * {@link #startHeadersTransition(boolean)}. 68 * <p> 69 * The recommended theme to use with a BrowseFragment is 70 * {@link android.support.v17.leanback.R.style#Theme_Leanback_Browse}. 71 * </p> 72 */ 73public class BrowseFragment extends BaseFragment { 74 75 // BUNDLE attribute for saving header show/hide status when backstack is used: 76 static final String HEADER_STACK_INDEX = "headerStackIndex"; 77 // BUNDLE attribute for saving header show/hide status when backstack is not used: 78 static final String HEADER_SHOW = "headerShow"; 79 80 final class BackStackListener implements FragmentManager.OnBackStackChangedListener { 81 int mLastEntryCount; 82 int mIndexOfHeadersBackStack; 83 84 BackStackListener() { 85 mLastEntryCount = getFragmentManager().getBackStackEntryCount(); 86 mIndexOfHeadersBackStack = -1; 87 } 88 89 void load(Bundle savedInstanceState) { 90 if (savedInstanceState != null) { 91 mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1); 92 mShowingHeaders = mIndexOfHeadersBackStack == -1; 93 } else { 94 if (!mShowingHeaders) { 95 getFragmentManager().beginTransaction() 96 .addToBackStack(mWithHeadersBackStackName).commit(); 97 } 98 } 99 } 100 101 void save(Bundle outState) { 102 outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack); 103 } 104 105 106 @Override 107 public void onBackStackChanged() { 108 if (getFragmentManager() == null) { 109 Log.w(TAG, "getFragmentManager() is null, stack:", new Exception()); 110 return; 111 } 112 int count = getFragmentManager().getBackStackEntryCount(); 113 // if backstack is growing and last pushed entry is "headers" backstack, 114 // remember the index of the entry. 115 if (count > mLastEntryCount) { 116 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1); 117 if (mWithHeadersBackStackName.equals(entry.getName())) { 118 mIndexOfHeadersBackStack = count - 1; 119 } 120 } else if (count < mLastEntryCount) { 121 // if popped "headers" backstack, initiate the show header transition if needed 122 if (mIndexOfHeadersBackStack >= count) { 123 mIndexOfHeadersBackStack = -1; 124 if (!mShowingHeaders) { 125 startHeadersTransitionInternal(true); 126 } 127 } 128 } 129 mLastEntryCount = count; 130 } 131 } 132 133 /** 134 * Listener for transitions between browse headers and rows. 135 */ 136 public static class BrowseTransitionListener { 137 /** 138 * Callback when headers transition starts. 139 * 140 * @param withHeaders True if the transition will result in headers 141 * being shown, false otherwise. 142 */ 143 public void onHeadersTransitionStart(boolean withHeaders) { 144 } 145 /** 146 * Callback when headers transition stops. 147 * 148 * @param withHeaders True if the transition will result in headers 149 * being shown, false otherwise. 150 */ 151 public void onHeadersTransitionStop(boolean withHeaders) { 152 } 153 } 154 155 private class SetSelectionRunnable implements Runnable { 156 static final int TYPE_INVALID = -1; 157 static final int TYPE_INTERNAL_SYNC = 0; 158 static final int TYPE_USER_REQUEST = 1; 159 160 private int mPosition; 161 private int mType; 162 private boolean mSmooth; 163 164 SetSelectionRunnable() { 165 reset(); 166 } 167 168 void post(int position, int type, boolean smooth) { 169 // Posting the set selection, rather than calling it immediately, prevents an issue 170 // with adapter changes. Example: a row is added before the current selected row; 171 // first the fast lane view updates its selection, then the rows fragment has that 172 // new selection propagated immediately; THEN the rows view processes the same adapter 173 // change and moves the selection again. 174 if (type >= mType) { 175 mPosition = position; 176 mType = type; 177 mSmooth = smooth; 178 mBrowseFrame.removeCallbacks(this); 179 mBrowseFrame.post(this); 180 } 181 } 182 183 @Override 184 public void run() { 185 setSelection(mPosition, mSmooth); 186 reset(); 187 } 188 189 private void reset() { 190 mPosition = -1; 191 mType = TYPE_INVALID; 192 mSmooth = false; 193 } 194 } 195 196 /** 197 * Interface that defines the interaction between {@link BrowseFragment} and it's main 198 * content fragment. The key method is {@link AbstractMainFragmentAdapter#getFragment()}, 199 * it will be used to get the fragment to be shown in the content section. Clients can 200 * provide any implementation of fragment and customize it's interaction with 201 * {@link BrowseFragment} by overriding the necessary methods. 202 * 203 * <p> 204 * Clients are expected to provide 205 * an instance of {@link MainFragmentAdapterFactory} which will be responsible for providing 206 * implementations of {@link AbstractMainFragmentAdapter} for given content types. Currently 207 * we support different types of content - {@link ListRow}, {@link PageRow} or any subtype 208 * of {@link Row}. We provide an out of the box adapter implementation for any rows other than 209 * {@link PageRow} - {@link RowsFragmentAdapter}. 210 * 211 * <p> 212 * {@link PageRow} is intended to give full flexibility to developers in terms of Fragment 213 * design. Users will have to provide an implementation of {@link AbstractMainFragmentAdapter} 214 * and provide that through {@link MainFragmentAdapterFactory}. 215 * {@link AbstractMainFragmentAdapter} implementation can supply any fragment and override 216 * just those interactions that makes sense. 217 */ 218 public static abstract class AbstractMainFragmentAdapter { 219 private boolean mScalingEnabled; 220 public abstract Fragment getFragment(); 221 222 /** 223 * Sets an item clicked listener on the fragment. 224 */ 225 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 226 } 227 228 /** 229 * Sets an item selection listener. 230 */ 231 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 232 } 233 234 /** 235 * Selects a Row and perform an optional task on the Row. 236 */ 237 public void setSelectedPosition(int rowPosition, 238 boolean smooth, 239 final Presenter.ViewHolderTask rowHolderTask) { 240 } 241 242 /** 243 * Selects a Row. 244 */ 245 public void setSelectedPosition(int rowPosition, boolean smooth) { 246 } 247 248 /** 249 * Returns the selected position. 250 */ 251 public int getSelectedPosition() { 252 return 0; 253 } 254 255 /** 256 * Returns whether its scrolling. 257 */ 258 public boolean isScrolling() { 259 return false; 260 } 261 262 /** 263 * Set the visibility of titles/hovercard of browse rows. 264 */ 265 public void setExpand(boolean expand) { 266 } 267 268 /** 269 * Set the visibility titles/hoverca of browse rows. 270 */ 271 public void setAdapter(ObjectAdapter adapter) { 272 } 273 274 /** 275 * For rows that willing to participate entrance transition, this function 276 * hide views if afterTransition is true, show views if afterTransition is false. 277 */ 278 public void setEntranceTransitionState(boolean state) { 279 } 280 281 /** 282 * Sets the window alignment and also the pivots for scale operation. 283 */ 284 public void setAlignment(int windowAlignOffsetFromTop) { 285 } 286 287 /** 288 * Callback indicating transition prepare start. 289 */ 290 public boolean onTransitionPrepare() { 291 return false; 292 } 293 294 /** 295 * Callback indicating transition start. 296 */ 297 public void onTransitionStart() { 298 } 299 300 /** 301 * Callback indicating transition end. 302 */ 303 public void onTransitionEnd() { 304 } 305 306 /** 307 * Returns whether row scaling is enabled. 308 */ 309 public boolean isScalingEnabled() { 310 return mScalingEnabled; 311 } 312 313 /** 314 * Sets the row scaling property. 315 */ 316 public void setScalingEnabled(boolean scalingEnabled) { 317 this.mScalingEnabled = scalingEnabled; 318 } 319 } 320 321 /** 322 * Factory class for {@link BrowseFragment.AbstractMainFragmentAdapter}. Developers can provide 323 * a custom implementation into {@link BrowseFragment}. {@link BrowseFragment} will use this 324 * factory to create fragments {@link BrowseFragment.AbstractMainFragmentAdapter#getFragment()} 325 * to display on the main content section. 326 */ 327 public static abstract class MainFragmentAdapterFactory { 328 private BrowseFragment.AbstractMainFragmentAdapter rowsFragmentAdapter; 329 private BrowseFragment.AbstractMainFragmentAdapter pageFragmentAdapter; 330 331 public BrowseFragment.AbstractMainFragmentAdapter getAdapter( 332 ObjectAdapter adapter, int position) { 333 if (adapter == null || adapter.size() == 0) { 334 return getRowsFragmentAdapter(); 335 } 336 337 if (position < 0 || position > adapter.size()) { 338 throw new IllegalArgumentException( 339 String.format("Invalid position %d requested", position)); 340 } 341 342 Object item = adapter.get(position); 343 if (item instanceof PageRow) { 344 if (pageFragmentAdapter == null) { 345 pageFragmentAdapter = getPageFragmentAdapter(); 346 } 347 return pageFragmentAdapter; 348 } else { 349 return getRowsFragmentAdapter(); 350 } 351 } 352 353 private BrowseFragment.AbstractMainFragmentAdapter getRowsFragmentAdapter() { 354 if (rowsFragmentAdapter == null) { 355 rowsFragmentAdapter = new RowsFragmentAdapter(); 356 } 357 return rowsFragmentAdapter; 358 } 359 360 public abstract BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter(); 361 } 362 363 private static final String TAG = "BrowseFragment"; 364 365 private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_"; 366 367 private static boolean DEBUG = false; 368 369 /** The headers fragment is enabled and shown by default. */ 370 public static final int HEADERS_ENABLED = 1; 371 372 /** The headers fragment is enabled and hidden by default. */ 373 public static final int HEADERS_HIDDEN = 2; 374 375 /** The headers fragment is disabled and will never be shown. */ 376 public static final int HEADERS_DISABLED = 3; 377 378 private MainFragmentAdapterFactory mMainFragmentAdapterFactory 379 = new MainFragmentAdapterFactory() { 380 @Override 381 public BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter() { 382 return null; 383 } 384 }; 385 386 private AbstractMainFragmentAdapter mMainFragmentAdapter; 387 private Fragment mMainFragment; 388 private HeadersFragment mHeadersFragment; 389 390 private ObjectAdapter mAdapter; 391 392 private int mHeadersState = HEADERS_ENABLED; 393 private int mBrandColor = Color.TRANSPARENT; 394 private boolean mBrandColorSet; 395 396 private BrowseFrameLayout mBrowseFrame; 397 private ScaleFrameLayout mScaleFrameLayout; 398 private boolean mHeadersBackStackEnabled = true; 399 private String mWithHeadersBackStackName; 400 private boolean mShowingHeaders = true; 401 private boolean mCanShowHeaders = true; 402 private int mContainerListMarginStart; 403 private int mContainerListAlignTop; 404 private boolean mMainFragmentScaleEnabled = true; 405 private OnItemViewSelectedListener mExternalOnItemViewSelectedListener; 406 private OnItemViewClickedListener mOnItemViewClickedListener; 407 private int mSelectedPosition = 0; 408 private float mScaleFactor; 409 410 private PresenterSelector mHeaderPresenterSelector; 411 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 412 413 // transition related: 414 private Object mSceneWithHeaders; 415 private Object mSceneWithoutHeaders; 416 private Object mSceneAfterEntranceTransition; 417 private Object mHeadersTransition; 418 private BackStackListener mBackStackChangedListener; 419 private BrowseTransitionListener mBrowseTransitionListener; 420 421 private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title"; 422 private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge"; 423 private static final String ARG_HEADERS_STATE = 424 BrowseFragment.class.getCanonicalName() + ".headersState"; 425 426 /** 427 * Creates arguments for a browse fragment. 428 * 429 * @param args The Bundle to place arguments into, or null if the method 430 * should return a new Bundle. 431 * @param title The title of the BrowseFragment. 432 * @param headersState The initial state of the headers of the 433 * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link 434 * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}. 435 * @return A Bundle with the given arguments for creating a BrowseFragment. 436 */ 437 public static Bundle createArgs(Bundle args, String title, int headersState) { 438 if (args == null) { 439 args = new Bundle(); 440 } 441 args.putString(ARG_TITLE, title); 442 args.putInt(ARG_HEADERS_STATE, headersState); 443 return args; 444 } 445 446 /** 447 * Sets the brand color for the browse fragment. The brand color is used as 448 * the primary color for UI elements in the browse fragment. For example, 449 * the background color of the headers fragment uses the brand color. 450 * 451 * @param color The color to use as the brand color of the fragment. 452 */ 453 public void setBrandColor(@ColorInt int color) { 454 mBrandColor = color; 455 mBrandColorSet = true; 456 457 if (mHeadersFragment != null) { 458 mHeadersFragment.setBackgroundColor(mBrandColor); 459 } 460 } 461 462 /** 463 * Returns the brand color for the browse fragment. 464 * The default is transparent. 465 */ 466 @ColorInt 467 public int getBrandColor() { 468 return mBrandColor; 469 } 470 471 /** 472 * Sets the adapter containing the rows for the fragment. 473 * 474 * <p>The items referenced by the adapter must be be derived from 475 * {@link Row}. These rows will be used by the rows fragment and the headers 476 * fragment (if not disabled) to render the browse rows. 477 * 478 * @param adapter An ObjectAdapter for the browse rows. All items must 479 * derive from {@link Row}. 480 */ 481 public void setAdapter(ObjectAdapter adapter) { 482 mAdapter = adapter; 483 if (mMainFragment != null) { 484 mMainFragmentAdapter.setAdapter(adapter); 485 mHeadersFragment.setAdapter(adapter); 486 } 487 } 488 489 public void setMainFragmentAdapterFactory(MainFragmentAdapterFactory factory) { 490 this.mMainFragmentAdapterFactory = factory; 491 } 492 /** 493 * Returns the adapter containing the rows for the fragment. 494 */ 495 public ObjectAdapter getAdapter() { 496 return mAdapter; 497 } 498 499 /** 500 * Sets an item selection listener. 501 */ 502 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 503 mExternalOnItemViewSelectedListener = listener; 504 } 505 506 /** 507 * Returns an item selection listener. 508 */ 509 public OnItemViewSelectedListener getOnItemViewSelectedListener() { 510 return mExternalOnItemViewSelectedListener; 511 } 512 513 /** 514 * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has 515 * not been created yet or a different fragment is bound to it. 516 * 517 * @return RowsFragment if it's bound to BrowseFragment or null otherwise. 518 */ 519 public RowsFragment getRowsFragment() { 520 if (mMainFragment instanceof RowsFragment) { 521 return (RowsFragment) mMainFragment; 522 } 523 524 return null; 525 } 526 527 /** 528 * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet. 529 * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet. 530 */ 531 public HeadersFragment getHeadersFragment() { 532 return mHeadersFragment; 533 } 534 535 /** 536 * Sets an item clicked listener on the fragment. 537 * OnItemViewClickedListener will override {@link View.OnClickListener} that 538 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 539 * So in general, developer should choose one of the listeners but not both. 540 */ 541 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 542 mOnItemViewClickedListener = listener; 543 if (mMainFragmentAdapter != null) { 544 mMainFragmentAdapter.setOnItemViewClickedListener(listener); 545 } 546 } 547 548 /** 549 * Returns the item Clicked listener. 550 */ 551 public OnItemViewClickedListener getOnItemViewClickedListener() { 552 return mOnItemViewClickedListener; 553 } 554 555 /** 556 * Starts a headers transition. 557 * 558 * <p>This method will begin a transition to either show or hide the 559 * headers, depending on the value of withHeaders. If headers are disabled 560 * for this browse fragment, this method will throw an exception. 561 * 562 * @param withHeaders True if the headers should transition to being shown, 563 * false if the transition should result in headers being hidden. 564 */ 565 public void startHeadersTransition(boolean withHeaders) { 566 if (!mCanShowHeaders) { 567 throw new IllegalStateException("Cannot start headers transition"); 568 } 569 if (isInHeadersTransition() || mShowingHeaders == withHeaders) { 570 return; 571 } 572 startHeadersTransitionInternal(withHeaders); 573 } 574 575 /** 576 * Returns true if the headers transition is currently running. 577 */ 578 public boolean isInHeadersTransition() { 579 return mHeadersTransition != null; 580 } 581 582 /** 583 * Returns true if headers are shown. 584 */ 585 public boolean isShowingHeaders() { 586 return mShowingHeaders; 587 } 588 589 /** 590 * Sets a listener for browse fragment transitions. 591 * 592 * @param listener The listener to call when a browse headers transition 593 * begins or ends. 594 */ 595 public void setBrowseTransitionListener(BrowseTransitionListener listener) { 596 mBrowseTransitionListener = listener; 597 } 598 599 /** 600 * @deprecated use {@link BrowseFragment#enableMainFragmentScaling(boolean)} instead. 601 * 602 * @param enable true to enable row scaling 603 */ 604 public void enableRowScaling(boolean enable) { 605 enableMainFragmentScaling(enable); 606 } 607 608 /** 609 * Enables scaling of main fragment when headers are present. For the page/row fragment, 610 * scaling is enabled only when both this method and 611 * {@link AbstractMainFragmentAdapter#isScalingEnabled()} are enabled. 612 * 613 * @param enable true to enable row scaling 614 */ 615 public void enableMainFragmentScaling(boolean enable) { 616 mMainFragmentScaleEnabled = enable; 617 } 618 619 private void startHeadersTransitionInternal(final boolean withHeaders) { 620 if (getFragmentManager().isDestroyed()) { 621 return; 622 } 623 mShowingHeaders = withHeaders; 624 mMainFragmentAdapter.onTransitionPrepare(); 625 mMainFragmentAdapter.onTransitionStart(); 626 onExpandTransitionStart(!withHeaders, new Runnable() { 627 @Override 628 public void run() { 629 mHeadersFragment.onTransitionPrepare(); 630 mHeadersFragment.onTransitionStart(); 631 createHeadersTransition(); 632 if (mBrowseTransitionListener != null) { 633 mBrowseTransitionListener.onHeadersTransitionStart(withHeaders); 634 } 635 TransitionHelper.runTransition( 636 withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition); 637 if (mHeadersBackStackEnabled) { 638 if (!withHeaders) { 639 getFragmentManager().beginTransaction() 640 .addToBackStack(mWithHeadersBackStackName).commit(); 641 } else { 642 int index = mBackStackChangedListener.mIndexOfHeadersBackStack; 643 if (index >= 0) { 644 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index); 645 getFragmentManager().popBackStackImmediate(entry.getId(), 646 FragmentManager.POP_BACK_STACK_INCLUSIVE); 647 } 648 } 649 } 650 } 651 }); 652 } 653 654 boolean isVerticalScrolling() { 655 // don't run transition 656 return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling(); 657 } 658 659 660 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 661 new BrowseFrameLayout.OnFocusSearchListener() { 662 @Override 663 public View onFocusSearch(View focused, int direction) { 664 // if headers is running transition, focus stays 665 if (mCanShowHeaders && isInHeadersTransition()) { 666 return focused; 667 } 668 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 669 670 if (getTitleView() != null && focused != getTitleView() && 671 direction == View.FOCUS_UP) { 672 return getTitleView(); 673 } 674 if (getTitleView() != null && getTitleView().hasFocus() && 675 direction == View.FOCUS_DOWN) { 676 return mCanShowHeaders && mShowingHeaders ? 677 mHeadersFragment.getVerticalGridView() : mMainFragment.getView(); 678 } 679 680 boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; 681 int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 682 int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; 683 if (mCanShowHeaders && direction == towardStart) { 684 if (isVerticalScrolling() || mShowingHeaders) { 685 return focused; 686 } 687 return mHeadersFragment.getVerticalGridView(); 688 } else if (direction == towardEnd) { 689 if (isVerticalScrolling()) { 690 return focused; 691 } 692 return mMainFragment.getView(); 693 } else { 694 return null; 695 } 696 } 697 }; 698 699 private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = 700 new BrowseFrameLayout.OnChildFocusListener() { 701 702 @Override 703 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 704 if (getChildFragmentManager().isDestroyed()) { 705 return true; 706 } 707 // Make sure not changing focus when requestFocus() is called. 708 if (mCanShowHeaders && mShowingHeaders) { 709 if (mHeadersFragment != null && mHeadersFragment.getView() != null && 710 mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 711 return true; 712 } 713 } 714 if (mMainFragment != null && mMainFragment.getView() != null && 715 mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 716 return true; 717 } 718 if (getTitleView() != null && 719 getTitleView().requestFocus(direction, previouslyFocusedRect)) { 720 return true; 721 } 722 return false; 723 } 724 725 @Override 726 public void onRequestChildFocus(View child, View focused) { 727 if (getChildFragmentManager().isDestroyed()) { 728 return; 729 } 730 if (!mCanShowHeaders || isInHeadersTransition()) return; 731 int childId = child.getId(); 732 if (childId == R.id.browse_container_dock && mShowingHeaders) { 733 startHeadersTransitionInternal(false); 734 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 735 startHeadersTransitionInternal(true); 736 } 737 } 738 }; 739 740 @Override 741 public void onSaveInstanceState(Bundle outState) { 742 super.onSaveInstanceState(outState); 743 if (mBackStackChangedListener != null) { 744 mBackStackChangedListener.save(outState); 745 } else { 746 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 747 } 748 } 749 750 @Override 751 public void onCreate(Bundle savedInstanceState) { 752 super.onCreate(savedInstanceState); 753 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 754 mContainerListMarginStart = (int) ta.getDimension( 755 R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() 756 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); 757 mContainerListAlignTop = (int) ta.getDimension( 758 R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() 759 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); 760 ta.recycle(); 761 762 readArguments(getArguments()); 763 764 if (mCanShowHeaders) { 765 if (mHeadersBackStackEnabled) { 766 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 767 mBackStackChangedListener = new BackStackListener(); 768 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 769 mBackStackChangedListener.load(savedInstanceState); 770 } else { 771 if (savedInstanceState != null) { 772 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 773 } 774 } 775 } 776 777 mScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1); 778 } 779 780 @Override 781 public void onDestroy() { 782 if (mBackStackChangedListener != null) { 783 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 784 } 785 super.onDestroy(); 786 } 787 788 @Override 789 public View onCreateView(LayoutInflater inflater, ViewGroup container, 790 Bundle savedInstanceState) { 791 if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) { 792 mHeadersFragment = new HeadersFragment(); 793 mMainFragmentAdapter = mMainFragmentAdapterFactory.getAdapter( 794 mAdapter, mSelectedPosition); 795 mMainFragment = mMainFragmentAdapter.getFragment(); 796 getChildFragmentManager().beginTransaction() 797 .replace(R.id.browse_headers_dock, mHeadersFragment) 798 .replace(R.id.scale_frame, mMainFragment) 799 .commit(); 800 } else { 801 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 802 .findFragmentById(R.id.scale_frame); 803 mMainFragment = getChildFragmentManager() 804 .findFragmentById(R.id.browse_container_dock); 805 } 806 807 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 808 if (mHeaderPresenterSelector != null) { 809 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 810 } 811 mHeadersFragment.setAdapter(mAdapter); 812 mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener); 813 mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener); 814 815 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 816 817 setTitleView((TitleView) root.findViewById(R.id.browse_title_group)); 818 819 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 820 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 821 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 822 823 mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame); 824 mScaleFrameLayout.setPivotX(0); 825 mScaleFrameLayout.setPivotY(mContainerListAlignTop); 826 827 setupMainFragment(); 828 829 if (mBrandColorSet) { 830 mHeadersFragment.setBackgroundColor(mBrandColor); 831 } 832 833 mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 834 @Override 835 public void run() { 836 showHeaders(true); 837 } 838 }); 839 mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 840 @Override 841 public void run() { 842 showHeaders(false); 843 } 844 }); 845 mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 846 @Override 847 public void run() { 848 setEntranceTransitionEndState(); 849 } 850 }); 851 return root; 852 } 853 854 private void setupMainFragment() { 855 mMainFragmentAdapter.setAdapter(mAdapter); 856 mMainFragmentAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener); 857 mMainFragmentAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); 858 } 859 860 private void createHeadersTransition() { 861 mHeadersTransition = TransitionHelper.loadTransition(getActivity(), 862 mShowingHeaders ? 863 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 864 865 TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() { 866 @Override 867 public void onTransitionStart(Object transition) { 868 } 869 @Override 870 public void onTransitionEnd(Object transition) { 871 mHeadersTransition = null; 872 mMainFragmentAdapter.onTransitionEnd(); 873 mHeadersFragment.onTransitionEnd(); 874 if (mShowingHeaders) { 875 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 876 if (headerGridView != null && !headerGridView.hasFocus()) { 877 headerGridView.requestFocus(); 878 } 879 } else { 880 View rowsGridView = mMainFragment.getView(); 881 if (rowsGridView != null && !rowsGridView.hasFocus()) { 882 rowsGridView.requestFocus(); 883 } 884 } 885 if (mBrowseTransitionListener != null) { 886 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 887 } 888 } 889 }); 890 } 891 892 /** 893 * Sets the {@link PresenterSelector} used to render the row headers. 894 * 895 * @param headerPresenterSelector The PresenterSelector that will determine 896 * the Presenter for each row header. 897 */ 898 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 899 mHeaderPresenterSelector = headerPresenterSelector; 900 if (mHeadersFragment != null) { 901 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 902 } 903 } 904 905 private void setHeadersOnScreen(boolean onScreen) { 906 MarginLayoutParams lp; 907 View containerList; 908 containerList = mHeadersFragment.getView(); 909 lp = (MarginLayoutParams) containerList.getLayoutParams(); 910 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 911 containerList.setLayoutParams(lp); 912 } 913 914 private void showHeaders(boolean show) { 915 if (DEBUG) Log.v(TAG, "showHeaders " + show); 916 mHeadersFragment.setHeadersEnabled(show); 917 setHeadersOnScreen(show); 918 expandMainFragment(!show); 919 } 920 921 private void expandMainFragment(boolean expand) { 922 MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams(); 923 params.leftMargin = !expand ? mContainerListMarginStart : 0; 924 mScaleFrameLayout.setLayoutParams(params); 925 mMainFragmentAdapter.setExpand(expand); 926 927 setMainFragmentAlignment(); 928 final float scaleFactor = !expand 929 && mMainFragmentScaleEnabled && mMainFragmentAdapter.isScalingEnabled() 930 ? mScaleFactor : 1; 931 mScaleFrameLayout.setLayoutScaleY(scaleFactor); 932 mScaleFrameLayout.setChildScale(scaleFactor); 933 } 934 935 private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = 936 new HeadersFragment.OnHeaderClickedListener() { 937 @Override 938 public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 939 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 940 return; 941 } 942 startHeadersTransitionInternal(false); 943 mMainFragment.getView().requestFocus(); 944 } 945 }; 946 947 private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() { 948 @Override 949 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 950 RowPresenter.ViewHolder rowViewHolder, Row row) { 951 if (mMainFragment == null) { 952 return; 953 } 954 955 int position = ((RowsFragment) mMainFragment) 956 .getVerticalGridView().getSelectedPosition(); 957 if (DEBUG) Log.v(TAG, "row selected position " + position); 958 onRowSelected(position); 959 if (mExternalOnItemViewSelectedListener != null) { 960 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 961 rowViewHolder, row); 962 } 963 } 964 }; 965 966 private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener = 967 new HeadersFragment.OnHeaderViewSelectedListener() { 968 @Override 969 public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 970 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 971 if (DEBUG) Log.v(TAG, "header selected position " + position); 972 onRowSelected(position); 973 } 974 }; 975 976 private void onRowSelected(int position) { 977 if (position != mSelectedPosition) { 978 mSetSelectionRunnable.post( 979 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 980 981 if (getAdapter() == null || getAdapter().size() == 0 || position == 0) { 982 showTitle(true); 983 } else { 984 showTitle(false); 985 } 986 } 987 } 988 989 private void setSelection(int position, boolean smooth) { 990 if (position == NO_POSITION) { 991 return; 992 } 993 994 mHeadersFragment.setSelectedPosition(position, smooth); 995 AbstractMainFragmentAdapter newFragmentAdapter = mMainFragmentAdapterFactory.getAdapter( 996 mAdapter, position); 997 998 if (mMainFragmentAdapter != newFragmentAdapter) { 999 mMainFragmentAdapter = newFragmentAdapter; 1000 mMainFragment = mMainFragmentAdapter.getFragment(); 1001 swapBrowseContent(mMainFragment); 1002 expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); 1003 setupMainFragment(); 1004 } 1005 mMainFragmentAdapter.setSelectedPosition(position, smooth); 1006 mSelectedPosition = position; 1007 } 1008 1009 private void swapBrowseContent(Fragment fragment) { 1010 getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit(); 1011 } 1012 1013 /** 1014 * Sets the selected row position with smooth animation. 1015 */ 1016 public void setSelectedPosition(int position) { 1017 setSelectedPosition(position, true); 1018 } 1019 1020 /** 1021 * Gets position of currently selected row. 1022 * @return Position of currently selected row. 1023 */ 1024 public int getSelectedPosition() { 1025 return mSelectedPosition; 1026 } 1027 1028 /** 1029 * Sets the selected row position. 1030 */ 1031 public void setSelectedPosition(int position, boolean smooth) { 1032 mSetSelectionRunnable.post( 1033 position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); 1034 } 1035 1036 /** 1037 * Selects a Row and perform an optional task on the Row. For example 1038 * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code> 1039 * scrolls to 11th row and selects 6th item on that row. The method will be ignored if 1040 * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, 1041 * ViewGroup, Bundle)}). 1042 * 1043 * @param rowPosition Which row to select. 1044 * @param smooth True to scroll to the row, false for no animation. 1045 * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers 1046 * fragment will be collapsed. 1047 */ 1048 public void setSelectedPosition(int rowPosition, boolean smooth, 1049 final Presenter.ViewHolderTask rowHolderTask) { 1050 if (mMainFragmentAdapterFactory == null) { 1051 return; 1052 } 1053 if (rowHolderTask != null) { 1054 startHeadersTransition(false); 1055 } 1056 mMainFragmentAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask); 1057 } 1058 1059 @Override 1060 public void onStart() { 1061 super.onStart(); 1062 mHeadersFragment.setAlignment(mContainerListAlignTop); 1063 setMainFragmentAlignment(); 1064 1065 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 1066 mHeadersFragment.getView().requestFocus(); 1067 } else if ((!mCanShowHeaders || !mShowingHeaders) 1068 && mMainFragment.getView() != null) { 1069 mMainFragment.getView().requestFocus(); 1070 } 1071 1072 if (mCanShowHeaders) { 1073 showHeaders(mShowingHeaders); 1074 } 1075 1076 if (isEntranceTransitionEnabled()) { 1077 setEntranceTransitionStartState(); 1078 } 1079 } 1080 1081 private void onExpandTransitionStart(boolean expand, final Runnable callback) { 1082 if (expand) { 1083 callback.run(); 1084 return; 1085 } 1086 // Run a "pre" layout when we go non-expand, in order to get the initial 1087 // positions of added rows. 1088 new ExpandPreLayout(callback, mMainFragmentAdapter).execute(); 1089 } 1090 1091 private void setMainFragmentAlignment() { 1092 int alignOffset = mContainerListAlignTop; 1093 if (mMainFragmentScaleEnabled 1094 && mMainFragmentAdapter.isScalingEnabled() 1095 && mShowingHeaders) { 1096 alignOffset = (int) (alignOffset / mScaleFactor + 0.5f); 1097 } 1098 mMainFragmentAdapter.setAlignment(alignOffset); 1099 } 1100 1101 /** 1102 * Enables/disables headers transition on back key support. This is enabled by 1103 * default. The BrowseFragment will add a back stack entry when headers are 1104 * showing. Running a headers transition when the back key is pressed only 1105 * works when the headers state is {@link #HEADERS_ENABLED} or 1106 * {@link #HEADERS_HIDDEN}. 1107 * <p> 1108 * NOTE: If an Activity has its own onBackPressed() handling, you must 1109 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 1110 * and {@link BrowseTransitionListener} in your own back stack handling. 1111 */ 1112 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 1113 mHeadersBackStackEnabled = headersBackStackEnabled; 1114 } 1115 1116 /** 1117 * Returns true if headers transition on back key support is enabled. 1118 */ 1119 public final boolean isHeadersTransitionOnBackEnabled() { 1120 return mHeadersBackStackEnabled; 1121 } 1122 1123 private void readArguments(Bundle args) { 1124 if (args == null) { 1125 return; 1126 } 1127 if (args.containsKey(ARG_TITLE)) { 1128 setTitle(args.getString(ARG_TITLE)); 1129 } 1130 if (args.containsKey(ARG_HEADERS_STATE)) { 1131 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 1132 } 1133 } 1134 1135 /** 1136 * Sets the state for the headers column in the browse fragment. Must be one 1137 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1138 * {@link #HEADERS_DISABLED}. 1139 * 1140 * @param headersState The state of the headers for the browse fragment. 1141 */ 1142 public void setHeadersState(int headersState) { 1143 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1144 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1145 } 1146 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1147 1148 if (headersState != mHeadersState) { 1149 mHeadersState = headersState; 1150 switch (headersState) { 1151 case HEADERS_ENABLED: 1152 mCanShowHeaders = true; 1153 mShowingHeaders = true; 1154 break; 1155 case HEADERS_HIDDEN: 1156 mCanShowHeaders = true; 1157 mShowingHeaders = false; 1158 break; 1159 case HEADERS_DISABLED: 1160 mCanShowHeaders = false; 1161 mShowingHeaders = false; 1162 break; 1163 default: 1164 Log.w(TAG, "Unknown headers state: " + headersState); 1165 break; 1166 } 1167 if (mHeadersFragment != null) { 1168 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 1169 } 1170 } 1171 } 1172 1173 /** 1174 * Returns the state of the headers column in the browse fragment. 1175 */ 1176 public int getHeadersState() { 1177 return mHeadersState; 1178 } 1179 1180 @Override 1181 protected Object createEntranceTransition() { 1182 return TransitionHelper.loadTransition(getActivity(), 1183 R.transition.lb_browse_entrance_transition); 1184 } 1185 1186 @Override 1187 protected void runEntranceTransition(Object entranceTransition) { 1188 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 1189 } 1190 1191 @Override 1192 protected void onEntranceTransitionPrepare() { 1193 mHeadersFragment.onTransitionPrepare(); 1194 mMainFragmentAdapter.onTransitionPrepare(); 1195 } 1196 1197 @Override 1198 protected void onEntranceTransitionStart() { 1199 mHeadersFragment.onTransitionStart(); 1200 mMainFragmentAdapter.onTransitionStart(); 1201 } 1202 1203 @Override 1204 protected void onEntranceTransitionEnd() { 1205 mMainFragmentAdapter.onTransitionEnd(); 1206 mHeadersFragment.onTransitionEnd(); 1207 } 1208 1209 void setSearchOrbViewOnScreen(boolean onScreen) { 1210 View searchOrbView = getTitleView().getSearchAffordanceView(); 1211 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1212 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1213 searchOrbView.setLayoutParams(lp); 1214 } 1215 1216 void setEntranceTransitionStartState() { 1217 setHeadersOnScreen(false); 1218 setSearchOrbViewOnScreen(false); 1219 mMainFragmentAdapter.setEntranceTransitionState(false); 1220 } 1221 1222 void setEntranceTransitionEndState() { 1223 setHeadersOnScreen(mShowingHeaders); 1224 setSearchOrbViewOnScreen(true); 1225 mMainFragmentAdapter.setEntranceTransitionState(true); 1226 } 1227 1228 private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { 1229 1230 private final View mView; 1231 private final Runnable mCallback; 1232 private int mState; 1233 private AbstractMainFragmentAdapter mainFragmentAdapter; 1234 1235 final static int STATE_INIT = 0; 1236 final static int STATE_FIRST_DRAW = 1; 1237 final static int STATE_SECOND_DRAW = 2; 1238 1239 ExpandPreLayout(Runnable callback, AbstractMainFragmentAdapter adapter) { 1240 mView = adapter.getFragment().getView(); 1241 mCallback = callback; 1242 mainFragmentAdapter = adapter; 1243 } 1244 1245 void execute() { 1246 mView.getViewTreeObserver().addOnPreDrawListener(this); 1247 mainFragmentAdapter.setExpand(false); 1248 mState = STATE_INIT; 1249 } 1250 1251 @Override 1252 public boolean onPreDraw() { 1253 if (mView == null) { 1254 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1255 return true; 1256 } 1257 if (mState == STATE_INIT) { 1258 mainFragmentAdapter.setExpand(true); 1259 mState = STATE_FIRST_DRAW; 1260 } else if (mState == STATE_FIRST_DRAW) { 1261 mCallback.run(); 1262 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1263 mState = STATE_SECOND_DRAW; 1264 } 1265 return false; 1266 } 1267 } 1268} 1269