BrowseFragment.java revision 46443cb5b092f1d9156342645088eead9da026f6
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.support.v17.leanback.R; 17import android.support.v17.leanback.transition.LeanbackTransitionHelper; 18import android.support.v17.leanback.transition.TransitionHelper; 19import android.support.v17.leanback.transition.TransitionListener; 20import android.support.v17.leanback.widget.BrowseFrameLayout; 21import android.support.v17.leanback.widget.HorizontalGridView; 22import android.support.v17.leanback.widget.ItemBridgeAdapter; 23import android.support.v17.leanback.widget.OnItemViewClickedListener; 24import android.support.v17.leanback.widget.OnItemViewSelectedListener; 25import android.support.v17.leanback.widget.Presenter; 26import android.support.v17.leanback.widget.PresenterSelector; 27import android.support.v17.leanback.widget.RowPresenter; 28import android.support.v17.leanback.widget.TitleView; 29import android.support.v17.leanback.widget.VerticalGridView; 30import android.support.v17.leanback.widget.Row; 31import android.support.v17.leanback.widget.ObjectAdapter; 32import android.support.v17.leanback.widget.OnItemSelectedListener; 33import android.support.v17.leanback.widget.OnItemClickedListener; 34import android.support.v17.leanback.widget.SearchOrbView; 35import android.support.v4.view.ViewCompat; 36import android.util.Log; 37import android.app.Activity; 38import android.app.Fragment; 39import android.app.FragmentManager; 40import android.app.FragmentManager.BackStackEntry; 41import android.content.Context; 42import android.content.res.TypedArray; 43import android.os.Bundle; 44import android.view.LayoutInflater; 45import android.view.View; 46import android.view.View.OnClickListener; 47import android.view.ViewGroup; 48import android.view.ViewGroup.MarginLayoutParams; 49import android.view.ViewTreeObserver; 50import android.graphics.Color; 51import android.graphics.Rect; 52import android.graphics.drawable.Drawable; 53 54import static android.support.v7.widget.RecyclerView.NO_POSITION; 55 56/** 57 * A fragment for creating Leanback browse screens. It is composed of a 58 * RowsFragment and a HeadersFragment. 59 * <p> 60 * A BrowseFragment renders the elements of its {@link ObjectAdapter} as a set 61 * of rows in a vertical list. The elements in this adapter must be subclasses 62 * of {@link Row}. 63 * <p> 64 * The HeadersFragment can be set to be either shown or hidden by default, or 65 * may be disabled entirely. See {@link #setHeadersState} for details. 66 * <p> 67 * By default the BrowseFragment includes support for returning to the headers 68 * when the user presses Back. For Activities that customize {@link 69 * android.app.Activity#onBackPressed()}, you must disable this default Back key support by 70 * calling {@link #setHeadersTransitionOnBackEnabled(boolean)} with false and 71 * use {@link BrowseFragment.BrowseTransitionListener} and 72 * {@link #startHeadersTransition(boolean)}. 73 */ 74public class BrowseFragment extends BaseFragment { 75 76 // BUNDLE attribute for saving header show/hide status when backstack is used: 77 static final String HEADER_STACK_INDEX = "headerStackIndex"; 78 // BUNDLE attribute for saving header show/hide status when backstack is not used: 79 static final String HEADER_SHOW = "headerShow"; 80 // BUNDLE attribute for title is showing 81 static final String TITLE_SHOW = "titleShow"; 82 83 final class BackStackListener implements FragmentManager.OnBackStackChangedListener { 84 int mLastEntryCount; 85 int mIndexOfHeadersBackStack; 86 87 BackStackListener() { 88 mLastEntryCount = getFragmentManager().getBackStackEntryCount(); 89 mIndexOfHeadersBackStack = -1; 90 } 91 92 void load(Bundle savedInstanceState) { 93 if (savedInstanceState != null) { 94 mIndexOfHeadersBackStack = savedInstanceState.getInt(HEADER_STACK_INDEX, -1); 95 mShowingHeaders = mIndexOfHeadersBackStack == -1; 96 } else { 97 if (!mShowingHeaders) { 98 getFragmentManager().beginTransaction() 99 .addToBackStack(mWithHeadersBackStackName).commit(); 100 } 101 } 102 } 103 104 void save(Bundle outState) { 105 outState.putInt(HEADER_STACK_INDEX, mIndexOfHeadersBackStack); 106 } 107 108 109 @Override 110 public void onBackStackChanged() { 111 if (getFragmentManager() == null) { 112 Log.w(TAG, "getFragmentManager() is null, stack:", new Exception()); 113 return; 114 } 115 int count = getFragmentManager().getBackStackEntryCount(); 116 // if backstack is growing and last pushed entry is "headers" backstack, 117 // remember the index of the entry. 118 if (count > mLastEntryCount) { 119 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(count - 1); 120 if (mWithHeadersBackStackName.equals(entry.getName())) { 121 mIndexOfHeadersBackStack = count - 1; 122 } 123 } else if (count < mLastEntryCount) { 124 // if popped "headers" backstack, initiate the show header transition if needed 125 if (mIndexOfHeadersBackStack >= count) { 126 mIndexOfHeadersBackStack = -1; 127 if (!mShowingHeaders) { 128 startHeadersTransitionInternal(true); 129 } 130 } 131 } 132 mLastEntryCount = count; 133 } 134 } 135 136 /** 137 * Listener for transitions between browse headers and rows. 138 */ 139 public static class BrowseTransitionListener { 140 /** 141 * Callback when headers transition starts. 142 * 143 * @param withHeaders True if the transition will result in headers 144 * being shown, false otherwise. 145 */ 146 public void onHeadersTransitionStart(boolean withHeaders) { 147 } 148 /** 149 * Callback when headers transition stops. 150 * 151 * @param withHeaders True if the transition will result in headers 152 * being shown, false otherwise. 153 */ 154 public void onHeadersTransitionStop(boolean withHeaders) { 155 } 156 } 157 158 private class SetSelectionRunnable implements Runnable { 159 static final int TYPE_INVALID = -1; 160 static final int TYPE_INTERNAL_SYNC = 0; 161 static final int TYPE_USER_REQUEST = 1; 162 163 private int mPosition; 164 private int mType; 165 private boolean mSmooth; 166 167 SetSelectionRunnable() { 168 reset(); 169 } 170 171 void post(int position, int type, boolean smooth) { 172 // Posting the set selection, rather than calling it immediately, prevents an issue 173 // with adapter changes. Example: a row is added before the current selected row; 174 // first the fast lane view updates its selection, then the rows fragment has that 175 // new selection propagated immediately; THEN the rows view processes the same adapter 176 // change and moves the selection again. 177 if (type >= mType) { 178 mPosition = position; 179 mType = type; 180 mSmooth = smooth; 181 mBrowseFrame.removeCallbacks(this); 182 mBrowseFrame.post(this); 183 } 184 } 185 186 @Override 187 public void run() { 188 setSelection(mPosition, mSmooth); 189 reset(); 190 } 191 192 private void reset() { 193 mPosition = -1; 194 mType = TYPE_INVALID; 195 mSmooth = false; 196 } 197 } 198 199 private static final String TAG = "BrowseFragment"; 200 201 private static final String LB_HEADERS_BACKSTACK = "lbHeadersBackStack_"; 202 203 private static boolean DEBUG = false; 204 205 /** The headers fragment is enabled and shown by default. */ 206 public static final int HEADERS_ENABLED = 1; 207 208 /** The headers fragment is enabled and hidden by default. */ 209 public static final int HEADERS_HIDDEN = 2; 210 211 /** The headers fragment is disabled and will never be shown. */ 212 public static final int HEADERS_DISABLED = 3; 213 214 private static final float SLIDE_DISTANCE_FACTOR = 2; 215 216 private RowsFragment mRowsFragment; 217 private HeadersFragment mHeadersFragment; 218 219 private ObjectAdapter mAdapter; 220 221 private String mTitle; 222 private Drawable mBadgeDrawable; 223 private int mHeadersState = HEADERS_ENABLED; 224 private int mBrandColor = Color.TRANSPARENT; 225 private boolean mBrandColorSet; 226 227 private BrowseFrameLayout mBrowseFrame; 228 private TitleView mTitleView; 229 private boolean mShowingTitle = true; 230 private boolean mHeadersBackStackEnabled = true; 231 private String mWithHeadersBackStackName; 232 private boolean mShowingHeaders = true; 233 private boolean mCanShowHeaders = true; 234 private int mContainerListMarginStart; 235 private int mContainerListAlignTop; 236 private boolean mRowScaleEnabled = true; 237 private SearchOrbView.Colors mSearchAffordanceColors; 238 private boolean mSearchAffordanceColorSet; 239 private OnItemSelectedListener mExternalOnItemSelectedListener; 240 private OnClickListener mExternalOnSearchClickedListener; 241 private OnItemClickedListener mOnItemClickedListener; 242 private OnItemViewSelectedListener mExternalOnItemViewSelectedListener; 243 private OnItemViewClickedListener mOnItemViewClickedListener; 244 private int mSelectedPosition = -1; 245 246 private PresenterSelector mHeaderPresenterSelector; 247 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 248 249 // transition related: 250 private Object mSceneWithTitle; 251 private Object mSceneWithoutTitle; 252 private Object mSceneWithHeaders; 253 private Object mSceneWithoutHeaders; 254 private Object mSceneAfterEntranceTransition; 255 private Object mTitleUpTransition; 256 private Object mTitleDownTransition; 257 private Object mHeadersTransition; 258 private BackStackListener mBackStackChangedListener; 259 private BrowseTransitionListener mBrowseTransitionListener; 260 261 private static final String ARG_TITLE = BrowseFragment.class.getCanonicalName() + ".title"; 262 private static final String ARG_BADGE_URI = BrowseFragment.class.getCanonicalName() + ".badge"; 263 private static final String ARG_HEADERS_STATE = 264 BrowseFragment.class.getCanonicalName() + ".headersState"; 265 266 /** 267 * Create arguments for a browse fragment. 268 * 269 * @param args The Bundle to place arguments into, or null if the method 270 * should return a new Bundle. 271 * @param title The title of the BrowseFragment. 272 * @param headersState The initial state of the headers of the 273 * BrowseFragment. Must be one of {@link #HEADERS_ENABLED}, {@link 274 * #HEADERS_HIDDEN}, or {@link #HEADERS_DISABLED}. 275 * @return A Bundle with the given arguments for creating a BrowseFragment. 276 */ 277 public static Bundle createArgs(Bundle args, String title, int headersState) { 278 if (args == null) { 279 args = new Bundle(); 280 } 281 args.putString(ARG_TITLE, title); 282 args.putInt(ARG_HEADERS_STATE, headersState); 283 return args; 284 } 285 286 /** 287 * Sets the brand color for the browse fragment. The brand color is used as 288 * the primary color for UI elements in the browse fragment. For example, 289 * the background color of the headers fragment uses the brand color. 290 * 291 * @param color The color to use as the brand color of the fragment. 292 */ 293 public void setBrandColor(int color) { 294 mBrandColor = color; 295 mBrandColorSet = true; 296 297 if (mHeadersFragment != null) { 298 mHeadersFragment.setBackgroundColor(mBrandColor); 299 } 300 } 301 302 /** 303 * Returns the brand color for the browse fragment. 304 * The default is transparent. 305 */ 306 public int getBrandColor() { 307 return mBrandColor; 308 } 309 310 /** 311 * Sets the adapter containing the rows for the fragment. 312 * 313 * <p>The items referenced by the adapter must be be derived from 314 * {@link Row}. These rows will be used by the rows fragment and the headers 315 * fragment (if not disabled) to render the browse rows. 316 * 317 * @param adapter An ObjectAdapter for the browse rows. All items must 318 * derive from {@link Row}. 319 */ 320 public void setAdapter(ObjectAdapter adapter) { 321 mAdapter = adapter; 322 if (mRowsFragment != null) { 323 mRowsFragment.setAdapter(adapter); 324 mHeadersFragment.setAdapter(adapter); 325 } 326 } 327 328 /** 329 * Returns the adapter containing the rows for the fragment. 330 */ 331 public ObjectAdapter getAdapter() { 332 return mAdapter; 333 } 334 335 /** 336 * Sets an item selection listener. This listener will be called when an 337 * item or row is selected by a user. 338 * 339 * @param listener The listener to call when an item or row is selected. 340 * @deprecated Use {@link #setOnItemViewSelectedListener(OnItemViewSelectedListener)} 341 */ 342 public void setOnItemSelectedListener(OnItemSelectedListener listener) { 343 mExternalOnItemSelectedListener = listener; 344 } 345 346 /** 347 * Sets an item selection listener. 348 */ 349 public void setOnItemViewSelectedListener(OnItemViewSelectedListener listener) { 350 mExternalOnItemViewSelectedListener = listener; 351 } 352 353 /** 354 * Returns an item selection listener. 355 */ 356 public OnItemViewSelectedListener getOnItemViewSelectedListener() { 357 return mExternalOnItemViewSelectedListener; 358 } 359 360 /** 361 * Sets an item clicked listener on the fragment. 362 * 363 * <p>OnItemClickedListener will override {@link View.OnClickListener} that 364 * an item presenter may set during 365 * {@link Presenter#onCreateViewHolder(ViewGroup)}. So in general, you 366 * should choose to use an {@link OnItemClickedListener} or a 367 * {@link View.OnClickListener} on your item views, but not both. 368 * 369 * @param listener The listener to call when an item is clicked. 370 * @deprecated Use {@link #setOnItemViewClickedListener(OnItemViewClickedListener)} 371 */ 372 public void setOnItemClickedListener(OnItemClickedListener listener) { 373 mOnItemClickedListener = listener; 374 if (mRowsFragment != null) { 375 mRowsFragment.setOnItemClickedListener(listener); 376 } 377 } 378 379 /** 380 * Returns the item clicked listener. 381 * @deprecated Use {@link #getOnItemViewClickedListener()} 382 */ 383 public OnItemClickedListener getOnItemClickedListener() { 384 return mOnItemClickedListener; 385 } 386 387 /** 388 * Sets an item clicked listener on the fragment. 389 * OnItemViewClickedListener will override {@link View.OnClickListener} that 390 * item presenter sets during {@link Presenter#onCreateViewHolder(ViewGroup)}. 391 * So in general, developer should choose one of the listeners but not both. 392 */ 393 public void setOnItemViewClickedListener(OnItemViewClickedListener listener) { 394 mOnItemViewClickedListener = listener; 395 if (mRowsFragment != null) { 396 mRowsFragment.setOnItemViewClickedListener(listener); 397 } 398 } 399 400 /** 401 * Returns the item Clicked listener. 402 */ 403 public OnItemViewClickedListener getOnItemViewClickedListener() { 404 return mOnItemViewClickedListener; 405 } 406 407 /** 408 * Sets a click listener for the search affordance. 409 * 410 * <p>The presence of a listener will change the visibility of the search 411 * affordance in the fragment title. When set to non-null, the title will 412 * contain an element that a user may click to begin a search. 413 * 414 * <p>The listener's {@link View.OnClickListener#onClick onClick} method 415 * will be invoked when the user clicks on the search element. 416 * 417 * @param listener The listener to call when the search element is clicked. 418 */ 419 public void setOnSearchClickedListener(View.OnClickListener listener) { 420 mExternalOnSearchClickedListener = listener; 421 if (mTitleView != null) { 422 mTitleView.setOnSearchClickedListener(listener); 423 } 424 } 425 426 /** 427 * Sets the {@link SearchOrbView.Colors} used to draw the search affordance. 428 */ 429 public void setSearchAffordanceColors(SearchOrbView.Colors colors) { 430 mSearchAffordanceColors = colors; 431 mSearchAffordanceColorSet = true; 432 if (mTitleView != null) { 433 mTitleView.setSearchAffordanceColors(mSearchAffordanceColors); 434 } 435 } 436 437 /** 438 * Returns the {@link SearchOrbView.Colors} used to draw the search affordance. 439 */ 440 public SearchOrbView.Colors getSearchAffordanceColors() { 441 if (mSearchAffordanceColorSet) { 442 return mSearchAffordanceColors; 443 } 444 if (mTitleView == null) { 445 throw new IllegalStateException("Fragment views not yet created"); 446 } 447 return mTitleView.getSearchAffordanceColors(); 448 } 449 450 /** 451 * Sets the color used to draw the search affordance. 452 * A default brighter color will be set by the framework. 453 * 454 * @param color The color to use for the search affordance. 455 */ 456 public void setSearchAffordanceColor(int color) { 457 setSearchAffordanceColors(new SearchOrbView.Colors(color)); 458 } 459 460 /** 461 * Returns the color used to draw the search affordance. 462 */ 463 public int getSearchAffordanceColor() { 464 return getSearchAffordanceColors().color; 465 } 466 467 /** 468 * Start a headers transition. 469 * 470 * <p>This method will begin a transition to either show or hide the 471 * headers, depending on the value of withHeaders. If headers are disabled 472 * for this browse fragment, this method will throw an exception. 473 * 474 * @param withHeaders True if the headers should transition to being shown, 475 * false if the transition should result in headers being hidden. 476 */ 477 public void startHeadersTransition(boolean withHeaders) { 478 if (!mCanShowHeaders) { 479 throw new IllegalStateException("Cannot start headers transition"); 480 } 481 if (isInHeadersTransition() || mShowingHeaders == withHeaders) { 482 return; 483 } 484 startHeadersTransitionInternal(withHeaders); 485 } 486 487 /** 488 * Returns true if the headers transition is currently running. 489 */ 490 public boolean isInHeadersTransition() { 491 return mHeadersTransition != null; 492 } 493 494 /** 495 * Returns true if headers are shown. 496 */ 497 public boolean isShowingHeaders() { 498 return mShowingHeaders; 499 } 500 501 /** 502 * Set a listener for browse fragment transitions. 503 * 504 * @param listener The listener to call when a browse headers transition 505 * begins or ends. 506 */ 507 public void setBrowseTransitionListener(BrowseTransitionListener listener) { 508 mBrowseTransitionListener = listener; 509 } 510 511 /** 512 * Enables scaling of rows when headers are present. 513 * By default enabled to increase density. 514 * 515 * @param enable true to enable row scaling 516 */ 517 public void enableRowScaling(boolean enable) { 518 mRowScaleEnabled = enable; 519 if (mRowsFragment != null) { 520 mRowsFragment.enableRowScaling(mRowScaleEnabled); 521 } 522 } 523 524 private void startHeadersTransitionInternal(final boolean withHeaders) { 525 if (getFragmentManager().isDestroyed()) { 526 return; 527 } 528 mShowingHeaders = withHeaders; 529 mRowsFragment.onExpandTransitionStart(!withHeaders, new Runnable() { 530 @Override 531 public void run() { 532 mHeadersFragment.onTransitionStart(); 533 createHeadersTransition(); 534 if (mBrowseTransitionListener != null) { 535 mBrowseTransitionListener.onHeadersTransitionStart(withHeaders); 536 } 537 sTransitionHelper.runTransition(withHeaders ? mSceneWithHeaders : mSceneWithoutHeaders, 538 mHeadersTransition); 539 if (mHeadersBackStackEnabled) { 540 if (!withHeaders) { 541 getFragmentManager().beginTransaction() 542 .addToBackStack(mWithHeadersBackStackName).commit(); 543 } else { 544 int index = mBackStackChangedListener.mIndexOfHeadersBackStack; 545 if (index >= 0) { 546 BackStackEntry entry = getFragmentManager().getBackStackEntryAt(index); 547 getFragmentManager().popBackStackImmediate(entry.getId(), 548 FragmentManager.POP_BACK_STACK_INCLUSIVE); 549 } 550 } 551 } 552 } 553 }); 554 } 555 556 private boolean isVerticalScrolling() { 557 // don't run transition 558 return mHeadersFragment.getVerticalGridView().getScrollState() 559 != HorizontalGridView.SCROLL_STATE_IDLE 560 || mRowsFragment.getVerticalGridView().getScrollState() 561 != HorizontalGridView.SCROLL_STATE_IDLE; 562 } 563 564 private final BrowseFrameLayout.OnFocusSearchListener mOnFocusSearchListener = 565 new BrowseFrameLayout.OnFocusSearchListener() { 566 @Override 567 public View onFocusSearch(View focused, int direction) { 568 // if headers is running transition, focus stays 569 if (mCanShowHeaders && isInHeadersTransition()) { 570 return focused; 571 } 572 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 573 574 final View searchOrbView = mTitleView.getSearchAffordanceView(); 575 if (focused == searchOrbView && direction == View.FOCUS_DOWN) { 576 return mCanShowHeaders && mShowingHeaders ? 577 mHeadersFragment.getVerticalGridView() : 578 mRowsFragment.getVerticalGridView(); 579 } else if (focused != searchOrbView && searchOrbView.getVisibility() == View.VISIBLE 580 && direction == View.FOCUS_UP) { 581 return searchOrbView; 582 } 583 584 // If headers fragment is disabled, just return null. 585 if (!mCanShowHeaders) { 586 return null; 587 } 588 boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; 589 int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 590 int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; 591 if (direction == towardStart) { 592 if (isVerticalScrolling() || mShowingHeaders) { 593 return focused; 594 } 595 return mHeadersFragment.getVerticalGridView(); 596 } else if (direction == towardEnd) { 597 if (isVerticalScrolling() || !mShowingHeaders) { 598 return focused; 599 } 600 return mRowsFragment.getVerticalGridView(); 601 } else { 602 return null; 603 } 604 } 605 }; 606 607 private final BrowseFrameLayout.OnChildFocusListener mOnChildFocusListener = 608 new BrowseFrameLayout.OnChildFocusListener() { 609 610 @Override 611 public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { 612 if (getChildFragmentManager().isDestroyed()) { 613 return true; 614 } 615 // Make sure not changing focus when requestFocus() is called. 616 if (mCanShowHeaders && mShowingHeaders) { 617 if (mHeadersFragment != null && mHeadersFragment.getView() != null && 618 mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 619 return true; 620 } 621 } 622 if (mRowsFragment != null && mRowsFragment.getView() != null && 623 mRowsFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 624 return true; 625 } 626 if (mTitleView != null && 627 mTitleView.requestFocus(direction, previouslyFocusedRect)) { 628 return true; 629 } 630 return false; 631 }; 632 633 @Override 634 public void onRequestChildFocus(View child, View focused) { 635 if (getChildFragmentManager().isDestroyed()) { 636 return; 637 } 638 if (!mCanShowHeaders || isInHeadersTransition()) return; 639 int childId = child.getId(); 640 if (childId == R.id.browse_container_dock && mShowingHeaders) { 641 startHeadersTransitionInternal(false); 642 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 643 startHeadersTransitionInternal(true); 644 } 645 } 646 }; 647 648 @Override 649 public void onSaveInstanceState(Bundle outState) { 650 if (mBackStackChangedListener != null) { 651 mBackStackChangedListener.save(outState); 652 } else { 653 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 654 } 655 outState.putBoolean(TITLE_SHOW, mShowingTitle); 656 } 657 658 @Override 659 public void onCreate(Bundle savedInstanceState) { 660 super.onCreate(savedInstanceState); 661 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 662 mContainerListMarginStart = (int) ta.getDimension( 663 R.styleable.LeanbackTheme_browseRowsMarginStart, getActivity().getResources() 664 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_start)); 665 mContainerListAlignTop = (int) ta.getDimension( 666 R.styleable.LeanbackTheme_browseRowsMarginTop, getActivity().getResources() 667 .getDimensionPixelSize(R.dimen.lb_browse_rows_margin_top)); 668 ta.recycle(); 669 670 readArguments(getArguments()); 671 672 if (mCanShowHeaders) { 673 if (mHeadersBackStackEnabled) { 674 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 675 mBackStackChangedListener = new BackStackListener(); 676 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 677 mBackStackChangedListener.load(savedInstanceState); 678 } else { 679 if (savedInstanceState != null) { 680 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 681 } 682 } 683 } 684 685 } 686 687 @Override 688 public void onDestroy() { 689 if (mBackStackChangedListener != null) { 690 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 691 } 692 super.onDestroy(); 693 } 694 695 @Override 696 public View onCreateView(LayoutInflater inflater, ViewGroup container, 697 Bundle savedInstanceState) { 698 if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) { 699 mRowsFragment = new RowsFragment(); 700 mHeadersFragment = new HeadersFragment(); 701 getChildFragmentManager().beginTransaction() 702 .replace(R.id.browse_headers_dock, mHeadersFragment) 703 .replace(R.id.browse_container_dock, mRowsFragment).commit(); 704 } else { 705 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 706 .findFragmentById(R.id.browse_headers_dock); 707 mRowsFragment = (RowsFragment) getChildFragmentManager() 708 .findFragmentById(R.id.browse_container_dock); 709 } 710 711 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 712 713 mRowsFragment.setAdapter(mAdapter); 714 if (mHeaderPresenterSelector != null) { 715 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 716 } 717 mHeadersFragment.setAdapter(mAdapter); 718 719 mRowsFragment.enableRowScaling(mRowScaleEnabled); 720 mRowsFragment.setOnItemSelectedListener(mRowSelectedListener); 721 mRowsFragment.setOnItemViewSelectedListener(mRowViewSelectedListener); 722 mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener); 723 mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener); 724 mRowsFragment.setOnItemClickedListener(mOnItemClickedListener); 725 mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener); 726 727 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 728 729 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 730 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 731 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 732 733 mTitleView = (TitleView) root.findViewById(R.id.browse_title_group); 734 mTitleView.setTitle(mTitle); 735 mTitleView.setBadgeDrawable(mBadgeDrawable); 736 if (mSearchAffordanceColorSet) { 737 mTitleView.setSearchAffordanceColors(mSearchAffordanceColors); 738 } 739 if (mExternalOnSearchClickedListener != null) { 740 mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener); 741 } 742 743 if (mBrandColorSet) { 744 mHeadersFragment.setBackgroundColor(mBrandColor); 745 } 746 747 mSceneWithTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 748 @Override 749 public void run() { 750 mTitleView.setVisibility(View.VISIBLE); 751 } 752 }); 753 mSceneWithoutTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 754 @Override 755 public void run() { 756 mTitleView.setVisibility(View.INVISIBLE); 757 } 758 }); 759 mSceneWithHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 760 @Override 761 public void run() { 762 showHeaders(true); 763 } 764 }); 765 mSceneWithoutHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 766 @Override 767 public void run() { 768 showHeaders(false); 769 } 770 }); 771 mSceneAfterEntranceTransition = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 772 @Override 773 public void run() { 774 setEntranceTransitionEndState(); 775 } 776 }); 777 Context context = getActivity(); 778 mTitleUpTransition = LeanbackTransitionHelper.loadTitleOutTransition(context, 779 sTransitionHelper); 780 mTitleDownTransition = LeanbackTransitionHelper.loadTitleInTransition(context, 781 sTransitionHelper); 782 783 if (savedInstanceState != null) { 784 mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW); 785 } 786 mTitleView.setVisibility(mShowingTitle ? View.VISIBLE: View.INVISIBLE); 787 788 return root; 789 } 790 791 private void createHeadersTransition() { 792 mHeadersTransition = sTransitionHelper.loadTransition(getActivity(), 793 mShowingHeaders ? 794 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 795 796 sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() { 797 @Override 798 public void onTransitionStart(Object transition) { 799 } 800 @Override 801 public void onTransitionEnd(Object transition) { 802 mHeadersTransition = null; 803 mRowsFragment.onTransitionEnd(); 804 mHeadersFragment.onTransitionEnd(); 805 if (mShowingHeaders) { 806 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 807 if (headerGridView != null && !headerGridView.hasFocus()) { 808 headerGridView.requestFocus(); 809 } 810 } else { 811 VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView(); 812 if (rowsGridView != null && !rowsGridView.hasFocus()) { 813 rowsGridView.requestFocus(); 814 } 815 } 816 if (mBrowseTransitionListener != null) { 817 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 818 } 819 } 820 }); 821 } 822 823 /** 824 * Sets the {@link PresenterSelector} used to render the row headers. 825 * 826 * @param headerPresenterSelector The PresenterSelector that will determine 827 * the Presenter for each row header. 828 */ 829 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 830 mHeaderPresenterSelector = headerPresenterSelector; 831 if (mHeadersFragment != null) { 832 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 833 } 834 } 835 836 private void setRowsAlignedLeft(boolean alignLeft) { 837 MarginLayoutParams lp; 838 View containerList; 839 containerList = mRowsFragment.getView(); 840 lp = (MarginLayoutParams) containerList.getLayoutParams(); 841 lp.setMarginStart(alignLeft ? 0 : mContainerListMarginStart); 842 containerList.setLayoutParams(lp); 843 } 844 845 private void setHeadersOnScreen(boolean onScreen) { 846 MarginLayoutParams lp; 847 View containerList; 848 containerList = mHeadersFragment.getView(); 849 lp = (MarginLayoutParams) containerList.getLayoutParams(); 850 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 851 containerList.setLayoutParams(lp); 852 } 853 854 private void showHeaders(boolean show) { 855 if (DEBUG) Log.v(TAG, "showHeaders " + show); 856 mHeadersFragment.setHeadersEnabled(show); 857 setHeadersOnScreen(show); 858 setRowsAlignedLeft(!show); 859 mRowsFragment.setExpand(!show); 860 } 861 862 private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = 863 new HeadersFragment.OnHeaderClickedListener() { 864 @Override 865 public void onHeaderClicked() { 866 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 867 return; 868 } 869 startHeadersTransitionInternal(false); 870 mRowsFragment.getVerticalGridView().requestFocus(); 871 } 872 }; 873 874 private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() { 875 @Override 876 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 877 RowPresenter.ViewHolder rowViewHolder, Row row) { 878 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 879 if (DEBUG) Log.v(TAG, "row selected position " + position); 880 onRowSelected(position); 881 if (mExternalOnItemViewSelectedListener != null) { 882 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 883 rowViewHolder, row); 884 } 885 } 886 }; 887 888 private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() { 889 @Override 890 public void onItemSelected(Object item, Row row) { 891 if (mExternalOnItemSelectedListener != null) { 892 mExternalOnItemSelectedListener.onItemSelected(item, row); 893 } 894 } 895 }; 896 897 private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() { 898 @Override 899 public void onItemSelected(Object item, Row row) { 900 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 901 if (DEBUG) Log.v(TAG, "header selected position " + position); 902 onRowSelected(position); 903 } 904 }; 905 906 private void onRowSelected(int position) { 907 if (position != mSelectedPosition) { 908 mSetSelectionRunnable.post( 909 position, SetSelectionRunnable.TYPE_INTERNAL_SYNC, true); 910 911 if (getAdapter() == null || getAdapter().size() == 0 || position == 0) { 912 if (!mShowingTitle) { 913 sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition); 914 mShowingTitle = true; 915 } 916 } else if (mShowingTitle) { 917 sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition); 918 mShowingTitle = false; 919 } 920 } 921 } 922 923 private void setSelection(int position, boolean smooth) { 924 if (position != NO_POSITION) { 925 mRowsFragment.setSelectedPosition(position, smooth); 926 mHeadersFragment.setSelectedPosition(position, smooth); 927 } 928 mSelectedPosition = position; 929 } 930 931 /** 932 * Sets the selected row position with smooth animation. 933 */ 934 public void setSelectedPosition(int position) { 935 setSelectedPosition(position, true); 936 } 937 938 /** 939 * Sets the selected row position. 940 */ 941 public void setSelectedPosition(int position, boolean smooth) { 942 mSetSelectionRunnable.post( 943 position, SetSelectionRunnable.TYPE_USER_REQUEST, smooth); 944 } 945 946 @Override 947 public void onStart() { 948 super.onStart(); 949 mHeadersFragment.setWindowAlignmentFromTop(mContainerListAlignTop); 950 mHeadersFragment.setItemAlignment(); 951 mRowsFragment.setWindowAlignmentFromTop(mContainerListAlignTop); 952 mRowsFragment.setItemAlignment(); 953 954 mRowsFragment.setScalePivots(0, mContainerListAlignTop); 955 956 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 957 mHeadersFragment.getView().requestFocus(); 958 } else if ((!mCanShowHeaders || !mShowingHeaders) 959 && mRowsFragment.getView() != null) { 960 mRowsFragment.getView().requestFocus(); 961 } 962 if (mCanShowHeaders) { 963 showHeaders(mShowingHeaders); 964 } 965 if (isEntranceTransitionEnabled()) { 966 setEntranceTransitionStartState(); 967 } 968 } 969 970 @Override 971 public void onPause() { 972 mTitleView.enableAnimation(false); 973 super.onPause(); 974 } 975 976 @Override 977 public void onResume() { 978 super.onResume(); 979 mTitleView.enableAnimation(true); 980 } 981 982 /** 983 * Enable/disable headers transition on back key support. This is enabled by 984 * default. The BrowseFragment will add a back stack entry when headers are 985 * showing. Running a headers transition when the back key is pressed only 986 * works when the headers state is {@link #HEADERS_ENABLED} or 987 * {@link #HEADERS_HIDDEN}. 988 * <p> 989 * NOTE: If an Activity has its own onBackPressed() handling, you must 990 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 991 * and {@link BrowseTransitionListener} in your own back stack handling. 992 */ 993 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 994 mHeadersBackStackEnabled = headersBackStackEnabled; 995 } 996 997 /** 998 * Returns true if headers transition on back key support is enabled. 999 */ 1000 public final boolean isHeadersTransitionOnBackEnabled() { 1001 return mHeadersBackStackEnabled; 1002 } 1003 1004 private void readArguments(Bundle args) { 1005 if (args == null) { 1006 return; 1007 } 1008 if (args.containsKey(ARG_TITLE)) { 1009 setTitle(args.getString(ARG_TITLE)); 1010 } 1011 if (args.containsKey(ARG_HEADERS_STATE)) { 1012 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 1013 } 1014 } 1015 1016 /** 1017 * Sets the drawable displayed in the browse fragment title. 1018 * 1019 * @param drawable The Drawable to display in the browse fragment title. 1020 */ 1021 public void setBadgeDrawable(Drawable drawable) { 1022 if (mBadgeDrawable != drawable) { 1023 mBadgeDrawable = drawable; 1024 if (mTitleView != null) { 1025 mTitleView.setBadgeDrawable(drawable); 1026 } 1027 } 1028 } 1029 1030 /** 1031 * Returns the badge drawable used in the fragment title. 1032 */ 1033 public Drawable getBadgeDrawable() { 1034 return mBadgeDrawable; 1035 } 1036 1037 /** 1038 * Sets a title for the browse fragment. 1039 * 1040 * @param title The title of the browse fragment. 1041 */ 1042 public void setTitle(String title) { 1043 mTitle = title; 1044 if (mTitleView != null) { 1045 mTitleView.setTitle(title); 1046 } 1047 } 1048 1049 /** 1050 * Returns the title for the browse fragment. 1051 */ 1052 public String getTitle() { 1053 return mTitle; 1054 } 1055 1056 /** 1057 * Sets the state for the headers column in the browse fragment. Must be one 1058 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1059 * {@link #HEADERS_DISABLED}. 1060 * 1061 * @param headersState The state of the headers for the browse fragment. 1062 */ 1063 public void setHeadersState(int headersState) { 1064 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1065 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1066 } 1067 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1068 1069 if (headersState != mHeadersState) { 1070 mHeadersState = headersState; 1071 switch (headersState) { 1072 case HEADERS_ENABLED: 1073 mCanShowHeaders = true; 1074 mShowingHeaders = true; 1075 break; 1076 case HEADERS_HIDDEN: 1077 mCanShowHeaders = true; 1078 mShowingHeaders = false; 1079 break; 1080 case HEADERS_DISABLED: 1081 mCanShowHeaders = false; 1082 mShowingHeaders = false; 1083 break; 1084 default: 1085 Log.w(TAG, "Unknown headers state: " + headersState); 1086 break; 1087 } 1088 if (mHeadersFragment != null) { 1089 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 1090 } 1091 } 1092 } 1093 1094 /** 1095 * Returns the state of the headers column in the browse fragment. 1096 */ 1097 public int getHeadersState() { 1098 return mHeadersState; 1099 } 1100 1101 @Override 1102 protected Object createEntranceTransition() { 1103 return sTransitionHelper.loadTransition(getActivity(), 1104 R.transition.lb_browse_entrance_transition); 1105 } 1106 1107 @Override 1108 protected void runEntranceTransition(Object entranceTransition) { 1109 sTransitionHelper.runTransition(mSceneAfterEntranceTransition, 1110 entranceTransition); 1111 } 1112 1113 @Override 1114 protected void onEntranceTransitionStart() { 1115 mHeadersFragment.onTransitionStart(); 1116 mRowsFragment.onTransitionStart(); 1117 } 1118 1119 @Override 1120 protected void onEntranceTransitionEnd() { 1121 mRowsFragment.onTransitionEnd(); 1122 mHeadersFragment.onTransitionEnd(); 1123 } 1124 1125 void setSearchOrbViewOnScreen(boolean onScreen) { 1126 View searchOrbView = mTitleView.getSearchAffordanceView(); 1127 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1128 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1129 searchOrbView.setLayoutParams(lp); 1130 } 1131 1132 void setEntranceTransitionStartState() { 1133 setHeadersOnScreen(false); 1134 setSearchOrbViewOnScreen(false); 1135 mRowsFragment.setEntranceTransitionState(false); 1136 } 1137 1138 void setEntranceTransitionEndState() { 1139 setHeadersOnScreen(mShowingHeaders); 1140 setSearchOrbViewOnScreen(true); 1141 mRowsFragment.setEntranceTransitionState(true); 1142 } 1143 1144} 1145 1146