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