BrowseFragment.java revision a9f6062bd2dd02b3de253b57c69302893bf1f2e3
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 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 /** 308 * Factory class for {@link BrowseFragment.AbstractMainFragmentAdapter}. Developers can provide 309 * a custom implementation into {@link BrowseFragment}. {@link BrowseFragment} will use this 310 * factory to create fragments {@link BrowseFragment.AbstractMainFragmentAdapter#getFragment()} 311 * to display on the main content section. 312 */ 313 public static abstract class MainFragmentAdapterFactory { 314 private BrowseFragment.AbstractMainFragmentAdapter rowsFragmentAdapter; 315 private BrowseFragment.AbstractMainFragmentAdapter pageFragmentAdapter; 316 317 public BrowseFragment.AbstractMainFragmentAdapter getAdapter( 318 ObjectAdapter adapter, int position) { 319 if (adapter == null || adapter.size() == 0) { 320 return getRowsFragmentAdapter(); 321 } 322 323 if (position < 0 || position > adapter.size()) { 324 throw new IllegalArgumentException( 325 String.format("Invalid position %d requested", position)); 326 } 327 328 Object item = adapter.get(position); 329 if (item instanceof PageRow) { 330 if (pageFragmentAdapter == null) { 331 pageFragmentAdapter = getPageFragmentAdapter(); 332 } 333 return pageFragmentAdapter; 334 } else { 335 return getRowsFragmentAdapter(); 336 } 337 } 338 339 private BrowseFragment.AbstractMainFragmentAdapter getRowsFragmentAdapter() { 340 if (rowsFragmentAdapter == null) { 341 rowsFragmentAdapter = new RowsFragmentAdapter(); 342 } 343 return rowsFragmentAdapter; 344 } 345 346 public abstract BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter(); 347 } 348 349 private static final String TAG = "BrowseFragment"; 350 351 private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_"; 352 353 private static boolean DEBUG = false; 354 355 /** The headers fragment is enabled and shown by default. */ 356 public static final int HEADERS_ENABLED = 1; 357 358 /** The headers fragment is enabled and hidden by default. */ 359 public static final int HEADERS_HIDDEN = 2; 360 361 /** The headers fragment is disabled and will never be shown. */ 362 public static final int HEADERS_DISABLED = 3; 363 364 private MainFragmentAdapterFactory mMainFragmentAdapterFactory 365 = new MainFragmentAdapterFactory() { 366 @Override 367 public BrowseFragment.AbstractMainFragmentAdapter getPageFragmentAdapter() { 368 return null; 369 } 370 }; 371 372 private AbstractMainFragmentAdapter mMainFragmentAdapter; 373 private Fragment mMainFragment; 374 private HeadersFragment mHeadersFragment; 375 376 private ObjectAdapter mAdapter; 377 378 private int mHeadersState = HEADERS_ENABLED; 379 private int mBrandColor = Color.TRANSPARENT; 380 private boolean mBrandColorSet; 381 382 private BrowseFrameLayout mBrowseFrame; 383 private ScaleFrameLayout mScaleFrameLayout; 384 private boolean mHeadersBackStackEnabled = true; 385 private String mWithHeadersBackStackName; 386 private boolean mShowingHeaders = true; 387 private boolean mCanShowHeaders = true; 388 private int mContainerListMarginStart; 389 private int mContainerListAlignTop; 390 private boolean mRowScaleEnabled = true; 391 private OnItemViewSelectedListener mExternalOnItemViewSelectedListener; 392 private OnItemViewClickedListener mOnItemViewClickedListener; 393 private int mSelectedPosition = 0; 394 private float mRowScaleFactor; 395 396 private PresenterSelector mHeaderPresenterSelector; 397 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 398 399 // transition related: 400 private Object mSceneWithHeaders; 401 private Object mSceneWithoutHeaders; 402 private Object mSceneAfterEntranceTransition; 403 private Object mHeadersTransition; 404 private BackStackListener mBackStackChangedListener; 405 private BrowseTransitionListener mBrowseTransitionListener; 406 407 private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title"; 408 private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge"; 409 private static final String ARG_HEADERS_STATE = 410 BrowseFragment.class.getCanonicalName() + ".headersState"; 411 412 /** 413 * Creates arguments for a browse fragment. 414 * 415 * @param args The Bundle to place arguments into, or null if the method 416 * should return a new Bundle. 417 * @param title The title of the BrowseFragment. 418 * @param headersState The initial state of the headers of the 419 * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link 420 * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}. 421 * @return A Bundle with the given arguments for creating a BrowseFragment. 422 */ 423 public static Bundle createArgs(Bundle args, String title, int headersState) { 424 if (args == null) { 425 args = new Bundle(); 426 } 427 args.putString(ARG_TITLE, title); 428 args.putInt(ARG_HEADERS_STATE, headersState); 429 return args; 430 } 431 432 /** 433 * Sets the brand color for the browse fragment. The brand color is used as 434 * the primary color for UI elements in the browse fragment. For example, 435 * the background color of the headers fragment uses the brand color. 436 * 437 * @param color The color to use as the brand color of the fragment. 438 */ 439 public void setBrandColor(@ColorInt int color) { 440 mBrandColor = color; 441 mBrandColorSet = true; 442 443 if (mHeadersFragment != null) { 444 mHeadersFragment.setBackgroundColor(mBrandColor); 445 } 446 } 447 448 /** 449 * Returns the brand color for the browse fragment. 450 * The default is transparent. 451 */ 452 @ColorInt 453 public int getBrandColor() { 454 return mBrandColor; 455 } 456 457 /** 458 * Sets the adapter containing the rows for the fragment. 459 * 460 * <p>The items referenced by the adapter must be be derived from 461 * {@link Row}. These rows will be used by the rows fragment and the headers 462 * fragment (if not disabled) to render the browse rows. 463 * 464 * @param adapter An ObjectAdapter for the browse rows. All items must 465 * derive from {@link Row}. 466 */ 467 public void setAdapter(ObjectAdapter adapter) { 468 mAdapter = adapter; 469 if (mMainFragment != null) { 470 mMainFragmentAdapter.setAdapter(adapter); 471 mHeadersFragment.setAdapter(adapter); 472 } 473 } 474 475 public void setMainFragmentAdapterFactory(MainFragmentAdapterFactory factory) { 476 this.mMainFragmentAdapterFactory = factory; 477 } 478 /** 479 * Returns the adapter containing the rows for the fragment. 480 */ 481 public ObjectAdapter getAdapter() { 482 return mAdapter; 483 } 484 485 /** 486 * Sets an item selection listener. 487 */ 488 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 489 mExternalOnItemViewSelectedListener = listener; 490 } 491 492 /** 493 * Returns an item selection listener. 494 */ 495 public OnItemViewSelectedListener getOnItemViewSelectedListener() { 496 return mExternalOnItemViewSelectedListener; 497 } 498 499 /** 500 * Get RowsFragment if it's bound to BrowseFragment or null if either BrowseFragment has 501 * not been created yet or a different fragment is bound to it. 502 * 503 * @return RowsFragment if it's bound to BrowseFragment or null otherwise. 504 */ 505 public RowsFragment getRowsFragment() { 506 if (mMainFragment instanceof RowsFragment) { 507 return (RowsFragment) mMainFragment; 508 } 509 510 return null; 511 } 512 513 /** 514 * Get currently bound HeadersFragment or null if HeadersFragment has not been created yet. 515 * @return Currently bound HeadersFragment or null if HeadersFragment has not been created yet. 516 */ 517 public HeadersFragment getHeadersFragment() { 518 return mHeadersFragment; 519 } 520 521 /** 522 * Sets an item clicked listener on the fragment. 523 * OnItemViewClickedListener will override {@link View.OnClickListener} that 524 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 525 * So in general, developer should choose one of the listeners but not both. 526 */ 527 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 528 mOnItemViewClickedListener = listener; 529 if (mMainFragmentAdapter != null) { 530 mMainFragmentAdapter.setOnItemViewClickedListener(listener); 531 } 532 } 533 534 /** 535 * Returns the item Clicked listener. 536 */ 537 public OnItemViewClickedListener getOnItemViewClickedListener() { 538 return mOnItemViewClickedListener; 539 } 540 541 /** 542 * Starts a headers transition. 543 * 544 * <p>This method will begin a transition to either show or hide the 545 * headers, depending on the value of withHeaders. If headers are disabled 546 * for this browse fragment, this method will throw an exception. 547 * 548 * @param withHeaders True if the headers should transition to being shown, 549 * false if the transition should result in headers being hidden. 550 */ 551 public void startHeadersTransition(boolean withHeaders) { 552 if (!mCanShowHeaders) { 553 throw new IllegalStateException("Cannot start headers transition"); 554 } 555 if (isInHeadersTransition() || mShowingHeaders == withHeaders) { 556 return; 557 } 558 startHeadersTransitionInternal(withHeaders); 559 } 560 561 /** 562 * Returns true if the headers transition is currently running. 563 */ 564 public boolean isInHeadersTransition() { 565 return mHeadersTransition != null; 566 } 567 568 /** 569 * Returns true if headers are shown. 570 */ 571 public boolean isShowingHeaders() { 572 return mShowingHeaders; 573 } 574 575 /** 576 * Sets a listener for browse fragment transitions. 577 * 578 * @param listener The listener to call when a browse headers transition 579 * begins or ends. 580 */ 581 public void setBrowseTransitionListener(BrowseTransitionListener listener) { 582 mBrowseTransitionListener = listener; 583 } 584 585 /** 586 * Enables scaling of rows when headers are present. 587 * By default enabled to increase density. 588 * 589 * @param enable true to enable row scaling 590 */ 591 public void enableRowScaling(boolean enable) { 592 mRowScaleEnabled = enable; 593 } 594 595 private void startHeadersTransitionInternal(final boolean withHeaders) { 596 if (getFragmentManager().isDestroyed()) { 597 return; 598 } 599 mShowingHeaders = withHeaders; 600 mMainFragmentAdapter.onTransitionPrepare(); 601 mMainFragmentAdapter.onTransitionStart(); 602 onExpandTransitionStart(!withHeaders, new Runnable() { 603 @Override 604 public void run() { 605 mHeadersFragment.onTransitionPrepare(); 606 mHeadersFragment.onTransitionStart(); 607 createHeadersTransition(); 608 if (mBrowseTransitionListener != null) { 609 mBrowseTransitionListener.onHeadersTransitionStart(withHeaders); 610 } 611 TransitionHelper.runTransition( 612 withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, mHeadersTransition); 613 if (mHeadersBackStackEnabled) { 614 if (!withHeaders) { 615 getFragmentManager().beginTransaction() 616 .addToBackStack(mWithHeadersBackStackName).commit(); 617 } else { 618 int index = mBackStackChangedListener.mIndexOfHeadersBackStack; 619 if (index >= 0) { 620 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index); 621 getFragmentManager().popBackStackImmediate(entry.getId(), 622 FragmentManager.POP_BACK_STACK_INCLUSIVE); 623 } 624 } 625 } 626 } 627 }); 628 } 629 630 boolean isVerticalScrolling() { 631 // don't run transition 632 return mHeadersFragment.isScrolling() || mMainFragmentAdapter.isScrolling(); 633 } 634 635 636 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 637 new BrowseFrameLayout.OnFocusSearchListener() { 638 @Override 639 public View onFocusSearch(View focused, int direction) { 640 // if headers is running transition, focus stays 641 if (mCanShowHeaders && isInHeadersTransition()) { 642 return focused; 643 } 644 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 645 646 if (getTitleView() != null && focused != getTitleView() && 647 direction == View.FOCUS_UP) { 648 return getTitleView(); 649 } 650 if (getTitleView() != null && getTitleView().hasFocus() && 651 direction == View.FOCUS_DOWN) { 652 return mCanShowHeaders && mShowingHeaders ? 653 mHeadersFragment.getVerticalGridView() : mMainFragment.getView(); 654 } 655 656 boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; 657 int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 658 int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; 659 if (mCanShowHeaders && direction == towardStart) { 660 if (isVerticalScrolling() || mShowingHeaders) { 661 return focused; 662 } 663 return mHeadersFragment.getVerticalGridView(); 664 } else if (direction == towardEnd) { 665 if (isVerticalScrolling()) { 666 return focused; 667 } 668 return mMainFragment.getView(); 669 } else { 670 return null; 671 } 672 } 673 }; 674 675 private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = 676 new BrowseFrameLayout.OnChildFocusListener() { 677 678 @Override 679 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 680 if (getChildFragmentManager().isDestroyed()) { 681 return true; 682 } 683 // Make sure not changing focus when requestFocus() is called. 684 if (mCanShowHeaders && mShowingHeaders) { 685 if (mHeadersFragment != null && mHeadersFragment.getView() != null && 686 mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 687 return true; 688 } 689 } 690 if (mMainFragment != null && mMainFragment.getView() != null && 691 mMainFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 692 return true; 693 } 694 if (getTitleView() != null && 695 getTitleView().requestFocus(direction, previouslyFocusedRect)) { 696 return true; 697 } 698 return false; 699 } 700 701 @Override 702 public void onRequestChildFocus(View child, View focused) { 703 if (getChildFragmentManager().isDestroyed()) { 704 return; 705 } 706 if (!mCanShowHeaders || isInHeadersTransition()) return; 707 int childId = child.getId(); 708 if (childId == R.id.browse_container_dock && mShowingHeaders) { 709 startHeadersTransitionInternal(false); 710 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 711 startHeadersTransitionInternal(true); 712 } 713 } 714 }; 715 716 @Override 717 public void onSaveInstanceState(Bundle outState) { 718 super.onSaveInstanceState(outState); 719 if (mBackStackChangedListener != null) { 720 mBackStackChangedListener.save(outState); 721 } else { 722 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 723 } 724 } 725 726 @Override 727 public void onCreate(Bundle savedInstanceState) { 728 super.onCreate(savedInstanceState); 729 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 730 mContainerListMarginStart = (int) ta.getDimension( 731 R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() 732 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); 733 mContainerListAlignTop = (int) ta.getDimension( 734 R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() 735 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); 736 ta.recycle(); 737 738 readArguments(getArguments()); 739 740 if (mCanShowHeaders) { 741 if (mHeadersBackStackEnabled) { 742 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 743 mBackStackChangedListener = new BackStackListener(); 744 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 745 mBackStackChangedListener.load(savedInstanceState); 746 } else { 747 if (savedInstanceState != null) { 748 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 749 } 750 } 751 } 752 753 mRowScaleFactor = getResources().getFraction(R.fraction.lb_browse_rows_scale, 1, 1); 754 } 755 756 @Override 757 public void onDestroy() { 758 if (mBackStackChangedListener != null) { 759 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 760 } 761 super.onDestroy(); 762 } 763 764 @Override 765 public View onCreateView(LayoutInflater inflater, ViewGroup container, 766 Bundle savedInstanceState) { 767 if (getChildFragmentManager().findFragmentById(R.id.scale_frame) == null) { 768 mHeadersFragment = new HeadersFragment(); 769 mMainFragmentAdapter = mMainFragmentAdapterFactory.getAdapter( 770 mAdapter, mSelectedPosition); 771 mMainFragment = mMainFragmentAdapter.getFragment(); 772 getChildFragmentManager().beginTransaction() 773 .replace(R.id.browse_headers_dock, mHeadersFragment) 774 .replace(R.id.scale_frame, mMainFragment) 775 .commit(); 776 } else { 777 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 778 .findFragmentById(R.id.scale_frame); 779 mMainFragment = getChildFragmentManager() 780 .findFragmentById(R.id.browse_container_dock); 781 } 782 783 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 784 if (mHeaderPresenterSelector != null) { 785 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 786 } 787 mHeadersFragment.setAdapter(mAdapter); 788 mHeadersFragment.setOnHeaderViewSelectedListener(mHeaderViewSelectedListener); 789 mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener); 790 791 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 792 793 setTitleView((TitleView) root.findViewById(R.id.browse_title_group)); 794 795 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 796 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 797 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 798 799 mScaleFrameLayout = (ScaleFrameLayout) root.findViewById(R.id.scale_frame); 800 mScaleFrameLayout.setPivotX(0); 801 mScaleFrameLayout.setPivotY(mContainerListAlignTop); 802 803 setupMainFragment(); 804 805 if (mBrandColorSet) { 806 mHeadersFragment.setBackgroundColor(mBrandColor); 807 } 808 809 mSceneWithHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 810 @Override 811 public void run() { 812 showHeaders(true); 813 } 814 }); 815 mSceneWithoutHeaders = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 816 @Override 817 public void run() { 818 showHeaders(false); 819 } 820 }); 821 mSceneAfterEntranceTransition = TransitionHelper.createScene(mBrowseFrame, new Runnable() { 822 @Override 823 public void run() { 824 setEntranceTransitionEndState(); 825 } 826 }); 827 return root; 828 } 829 830 private void setupMainFragment() { 831 mMainFragmentAdapter.setAdapter(mAdapter); 832 mMainFragmentAdapter.setOnItemViewSelectedListener(mRowViewSelectedListener); 833 mMainFragmentAdapter.setOnItemViewClickedListener(mOnItemViewClickedListener); 834 } 835 836 private void createHeadersTransition() { 837 mHeadersTransition = TransitionHelper.loadTransition(getActivity(), 838 mShowingHeaders ? 839 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 840 841 TransitionHelper.addTransitionListener(mHeadersTransition, new TransitionListener() { 842 @Override 843 public void onTransitionStart(Object transition) { 844 } 845 @Override 846 public void onTransitionEnd(Object transition) { 847 mHeadersTransition = null; 848 mMainFragmentAdapter.onTransitionEnd(); 849 mHeadersFragment.onTransitionEnd(); 850 if (mShowingHeaders) { 851 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 852 if (headerGridView != null && !headerGridView.hasFocus()) { 853 headerGridView.requestFocus(); 854 } 855 } else { 856 View rowsGridView = mMainFragment.getView(); 857 if (rowsGridView != null && !rowsGridView.hasFocus()) { 858 rowsGridView.requestFocus(); 859 } 860 } 861 if (mBrowseTransitionListener != null) { 862 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 863 } 864 } 865 }); 866 } 867 868 /** 869 * Sets the {@link PresenterSelector} used to render the row headers. 870 * 871 * @param headerPresenterSelector The PresenterSelector that will determine 872 * the Presenter for each row header. 873 */ 874 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 875 mHeaderPresenterSelector = headerPresenterSelector; 876 if (mHeadersFragment != null) { 877 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 878 } 879 } 880 881 private void setHeadersOnScreen(boolean onScreen) { 882 MarginLayoutParams lp; 883 View containerList; 884 containerList = mHeadersFragment.getView(); 885 lp = (MarginLayoutParams) containerList.getLayoutParams(); 886 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 887 containerList.setLayoutParams(lp); 888 } 889 890 private void showHeaders(boolean show) { 891 if (DEBUG) Log.v(TAG, "showHeaders " + show); 892 mHeadersFragment.setHeadersEnabled(show); 893 setHeadersOnScreen(show); 894 expandMainFragment(!show); 895 } 896 897 private void expandMainFragment(boolean expand) { 898 MarginLayoutParams params = (MarginLayoutParams) mScaleFrameLayout.getLayoutParams(); 899 params.leftMargin = !expand ? mContainerListMarginStart : 0; 900 mScaleFrameLayout.setLayoutParams(params); 901 mMainFragmentAdapter.setExpand(expand); 902 903 setMainFragmentAlignment(); 904 final float scaleFactor = !expand ? mRowScaleFactor : 1; 905 mScaleFrameLayout.setLayoutScaleY(scaleFactor); 906 mScaleFrameLayout.setChildScale(scaleFactor); 907 } 908 909 private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = 910 new HeadersFragment.OnHeaderClickedListener() { 911 @Override 912 public void onHeaderClicked(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 913 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 914 return; 915 } 916 startHeadersTransitionInternal(false); 917 mMainFragment.getView().requestFocus(); 918 } 919 }; 920 921 private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() { 922 @Override 923 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 924 RowPresenter.ViewHolder rowViewHolder, Row row) { 925 if (mMainFragment == null) { 926 return; 927 } 928 929 int position = ((RowsFragment) mMainFragment) 930 .getVerticalGridView().getSelectedPosition(); 931 if (DEBUG) Log.v(TAG, "row selected position " + position); 932 onRowSelected(position); 933 if (mExternalOnItemViewSelectedListener != null) { 934 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 935 rowViewHolder, row); 936 } 937 } 938 }; 939 940 private HeadersFragment.OnHeaderViewSelectedListener mHeaderViewSelectedListener = 941 new HeadersFragment.OnHeaderViewSelectedListener() { 942 @Override 943 public void onHeaderSelected(RowHeaderPresenter.ViewHolder viewHolder, Row row) { 944 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 945 if (DEBUG) Log.v(TAG, "header selected position " + position); 946 onRowSelected(position); 947 } 948 }; 949 950 private void onRowSelected(int position) { 951 if (position != mSelectedPosition) { 952 mSetSelectionRunnable.post( 953 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 954 955 if (getAdapter() == null || getAdapter().size() == 0 || position == 0) { 956 showTitle(true); 957 } else { 958 showTitle(false); 959 } 960 } 961 } 962 963 private void setSelection(int position, boolean smooth) { 964 if (position == NO_POSITION) { 965 return; 966 } 967 968 mHeadersFragment.setSelectedPosition(position, smooth); 969 AbstractMainFragmentAdapter newFragmentAdapter = mMainFragmentAdapterFactory.getAdapter( 970 mAdapter, position); 971 if (mMainFragmentAdapter != newFragmentAdapter) { 972 mMainFragmentAdapter = newFragmentAdapter; 973 mMainFragment = mMainFragmentAdapter.getFragment(); 974 swapBrowseContent(mMainFragment); 975 expandMainFragment(!(mCanShowHeaders && mShowingHeaders)); 976 setupMainFragment(); 977 mMainFragmentAdapter.setAlignment(mContainerListAlignTop); 978 } 979 mMainFragmentAdapter.setSelectedPosition(position, smooth); 980 mSelectedPosition = position; 981 } 982 983 private void swapBrowseContent(Fragment fragment) { 984 getChildFragmentManager().beginTransaction().replace(R.id.scale_frame, fragment).commit(); 985 } 986 987 /** 988 * Sets the selected row position with smooth animation. 989 */ 990 public void setSelectedPosition(int position) { 991 setSelectedPosition(position, true); 992 } 993 994 /** 995 * Gets position of currently selected row. 996 * @return Position of currently selected row. 997 */ 998 public int getSelectedPosition() { 999 return mSelectedPosition; 1000 } 1001 1002 /** 1003 * Sets the selected row position. 1004 */ 1005 public void setSelectedPosition(int position, boolean smooth) { 1006 mSetSelectionRunnable.post( 1007 position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); 1008 } 1009 1010 /** 1011 * Selects a Row and perform an optional task on the Row. For example 1012 * <code>setSelectedPosition(10, true, new ListRowPresenterSelectItemViewHolderTask(5))</code> 1013 * scrolls to 11th row and selects 6th item on that row. The method will be ignored if 1014 * RowsFragment has not been created (i.e. before {@link #onCreateView(LayoutInflater, 1015 * ViewGroup, Bundle)}). 1016 * 1017 * @param rowPosition Which row to select. 1018 * @param smooth True to scroll to the row, false for no animation. 1019 * @param rowHolderTask Optional task to perform on the Row. When the task is not null, headers 1020 * fragment will be collapsed. 1021 */ 1022 public void setSelectedPosition(int rowPosition, boolean smooth, 1023 final Presenter.ViewHolderTask rowHolderTask) { 1024 if (mMainFragmentAdapterFactory == null) { 1025 return; 1026 } 1027 if (rowHolderTask != null) { 1028 startHeadersTransition(false); 1029 } 1030 mMainFragmentAdapter.setSelectedPosition(rowPosition, smooth, rowHolderTask); 1031 } 1032 1033 @Override 1034 public void onStart() { 1035 super.onStart(); 1036 mHeadersFragment.setAlignment(mContainerListAlignTop); 1037 setMainFragmentAlignment(); 1038 1039 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 1040 mHeadersFragment.getView().requestFocus(); 1041 } else if ((!mCanShowHeaders || !mShowingHeaders) 1042 && mMainFragment.getView() != null) { 1043 mMainFragment.getView().requestFocus(); 1044 } 1045 1046 if (mCanShowHeaders) { 1047 showHeaders(mShowingHeaders); 1048 } 1049 1050 if (isEntranceTransitionEnabled()) { 1051 setEntranceTransitionStartState(); 1052 } 1053 } 1054 1055 private void onExpandTransitionStart(boolean expand, final Runnable callback) { 1056 if (expand) { 1057 callback.run(); 1058 return; 1059 } 1060 // Run a "pre" layout when we go non-expand, in order to get the initial 1061 // positions of added rows. 1062 new ExpandPreLayout(callback, mMainFragmentAdapter).execute(); 1063 } 1064 1065 private void setMainFragmentAlignment() { 1066 int alignOffset = mContainerListAlignTop; 1067 if (mRowScaleEnabled && mShowingHeaders) { 1068 alignOffset = (int) (alignOffset / mRowScaleFactor + 0.5f); 1069 } 1070 mMainFragmentAdapter.setAlignment(alignOffset); 1071 } 1072 1073 /** 1074 * Enables/disables headers transition on back key support. This is enabled by 1075 * default. The BrowseFragment will add a back stack entry when headers are 1076 * showing. Running a headers transition when the back key is pressed only 1077 * works when the headers state is {@link #HEADERS_ENABLED} or 1078 * {@link #HEADERS_HIDDEN}. 1079 * <p> 1080 * NOTE: If an Activity has its own onBackPressed() handling, you must 1081 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 1082 * and {@link BrowseTransitionListener} in your own back stack handling. 1083 */ 1084 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 1085 mHeadersBackStackEnabled = headersBackStackEnabled; 1086 } 1087 1088 /** 1089 * Returns true if headers transition on back key support is enabled. 1090 */ 1091 public final boolean isHeadersTransitionOnBackEnabled() { 1092 return mHeadersBackStackEnabled; 1093 } 1094 1095 private void readArguments(Bundle args) { 1096 if (args == null) { 1097 return; 1098 } 1099 if (args.containsKey(ARG_TITLE)) { 1100 setTitle(args.getString(ARG_TITLE)); 1101 } 1102 if (args.containsKey(ARG_HEADERS_STATE)) { 1103 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 1104 } 1105 } 1106 1107 /** 1108 * Sets the state for the headers column in the browse fragment. Must be one 1109 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1110 * {@link #HEADERS_DISABLED}. 1111 * 1112 * @param headersState The state of the headers for the browse fragment. 1113 */ 1114 public void setHeadersState(int headersState) { 1115 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1116 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1117 } 1118 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1119 1120 if (headersState != mHeadersState) { 1121 mHeadersState = headersState; 1122 switch (headersState) { 1123 case HEADERS_ENABLED: 1124 mCanShowHeaders = true; 1125 mShowingHeaders = true; 1126 break; 1127 case HEADERS_HIDDEN: 1128 mCanShowHeaders = true; 1129 mShowingHeaders = false; 1130 break; 1131 case HEADERS_DISABLED: 1132 mCanShowHeaders = false; 1133 mShowingHeaders = false; 1134 break; 1135 default: 1136 Log.w(TAG, "Unknown headers state: " + headersState); 1137 break; 1138 } 1139 if (mHeadersFragment != null) { 1140 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 1141 } 1142 } 1143 } 1144 1145 /** 1146 * Returns the state of the headers column in the browse fragment. 1147 */ 1148 public int getHeadersState() { 1149 return mHeadersState; 1150 } 1151 1152 @Override 1153 protected Object createEntranceTransition() { 1154 return TransitionHelper.loadTransition(getActivity(), 1155 R.transition.lb_browse_entrance_transition); 1156 } 1157 1158 @Override 1159 protected void runEntranceTransition(Object entranceTransition) { 1160 TransitionHelper.runTransition(mSceneAfterEntranceTransition, entranceTransition); 1161 } 1162 1163 @Override 1164 protected void onEntranceTransitionPrepare() { 1165 mHeadersFragment.onTransitionPrepare(); 1166 mMainFragmentAdapter.onTransitionPrepare(); 1167 } 1168 1169 @Override 1170 protected void onEntranceTransitionStart() { 1171 mHeadersFragment.onTransitionStart(); 1172 mMainFragmentAdapter.onTransitionStart(); 1173 } 1174 1175 @Override 1176 protected void onEntranceTransitionEnd() { 1177 mMainFragmentAdapter.onTransitionEnd(); 1178 mHeadersFragment.onTransitionEnd(); 1179 } 1180 1181 void setSearchOrbViewOnScreen(boolean onScreen) { 1182 View searchOrbView = getTitleView().getSearchAffordanceView(); 1183 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1184 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1185 searchOrbView.setLayoutParams(lp); 1186 } 1187 1188 void setEntranceTransitionStartState() { 1189 setHeadersOnScreen(false); 1190 setSearchOrbViewOnScreen(false); 1191 mMainFragmentAdapter.setEntranceTransitionState(false); 1192 } 1193 1194 void setEntranceTransitionEndState() { 1195 setHeadersOnScreen(mShowingHeaders); 1196 setSearchOrbViewOnScreen(true); 1197 mMainFragmentAdapter.setEntranceTransitionState(true); 1198 } 1199 1200 private static class ExpandPreLayout implements ViewTreeObserver.OnPreDrawListener { 1201 1202 private final View mView; 1203 private final Runnable mCallback; 1204 private int mState; 1205 private AbstractMainFragmentAdapter mainFragmentAdapter; 1206 1207 final static int STATE_INIT = 0; 1208 final static int STATE_FIRST_DRAW = 1; 1209 final static int STATE_SECOND_DRAW = 2; 1210 1211 ExpandPreLayout(Runnable callback, AbstractMainFragmentAdapter adapter) { 1212 mView = adapter.getFragment().getView(); 1213 mCallback = callback; 1214 mainFragmentAdapter = adapter; 1215 } 1216 1217 void execute() { 1218 mView.getViewTreeObserver().addOnPreDrawListener(this); 1219 mainFragmentAdapter.setExpand(false); 1220 mState = STATE_INIT; 1221 } 1222 1223 @Override 1224 public boolean onPreDraw() { 1225 if (mView == null) { 1226 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1227 return true; 1228 } 1229 if (mState == STATE_INIT) { 1230 mainFragmentAdapter.setExpand(true); 1231 mState = STATE_FIRST_DRAW; 1232 } else if (mState == STATE_FIRST_DRAW) { 1233 mCallback.run(); 1234 mView.getViewTreeObserver().removeOnPreDrawListener(this); 1235 mState = STATE_SECOND_DRAW; 1236 } 1237 return false; 1238 } 1239 } 1240} 1241