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