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