BrowseFragment.java revision 4735bfcd924fec2d694523f34fac5f8151257dc7
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 fragment is disabled, just return null. 526 if (!mCanShowHeaders) return null; 527 528 final View searchOrbView = mTitleView.getSearchAffordanceView(); 529 // if headers is running transition, focus stays 530 if (isInHeadersTransition()) return focused; 531 if (DEBUG) Log.v(TAG, "onFocusSearch focused " + focused + " + direction " + direction); 532 boolean isRtl = ViewCompat.getLayoutDirection(focused) == View.LAYOUT_DIRECTION_RTL; 533 int towardStart = isRtl ? View.FOCUS_RIGHT : View.FOCUS_LEFT; 534 int towardEnd = isRtl ? View.FOCUS_LEFT : View.FOCUS_RIGHT; 535 if (direction == towardStart) { 536 if (isVerticalScrolling() || mShowingHeaders) { 537 return focused; 538 } 539 return mHeadersFragment.getVerticalGridView(); 540 } else if (direction == towardEnd) { 541 if (isVerticalScrolling() || !mShowingHeaders) { 542 return focused; 543 } 544 return mRowsFragment.getVerticalGridView(); 545 } else if (focused == searchOrbView && direction == View.FOCUS_DOWN) { 546 return mShowingHeaders ? mHeadersFragment.getVerticalGridView() : 547 mRowsFragment.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 (mHeadersFragment != null && mHeadersFragment.getView() != null && 570 mHeadersFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 571 return true; 572 } 573 } 574 if (mRowsFragment != null && mRowsFragment.getView() != null && 575 mRowsFragment.getView().requestFocus(direction, previouslyFocusedRect)) { 576 return true; 577 } 578 if (mTitleView != null && 579 mTitleView.requestFocus(direction, previouslyFocusedRect)) { 580 return true; 581 } 582 return false; 583 }; 584 585 @Override 586 public void onRequestChildFocus(View child, View focused) { 587 if (getChildFragmentManager().isDestroyed()) { 588 return; 589 } 590 if (!mCanShowHeaders || isInHeadersTransition()) return; 591 int childId = child.getId(); 592 if (childId == R.id.browse_container_dock && mShowingHeaders) { 593 startHeadersTransitionInternal(false); 594 } else if (childId == R.id.browse_headers_dock && !mShowingHeaders) { 595 startHeadersTransitionInternal(true); 596 } 597 } 598 }; 599 600 @Override 601 public void onSaveInstanceState(Bundle outState) { 602 if (mBackStackChangedListener != null) { 603 mBackStackChangedListener.save(outState); 604 } else { 605 outState.putBoolean(HEADER_SHOW, mShowingHeaders); 606 } 607 outState.putBoolean(TITLE_SHOW, mShowingTitle); 608 } 609 610 @Override 611 public void onCreate(Bundle savedInstanceState) { 612 super.onCreate(savedInstanceState); 613 TypedArray ta = getActivity().obtainStyledAttributes(R.styleable.LeanbackTheme); 614 mContainerListMarginStart = (int) ta.getDimension( 615 R.styleable.LeanbackTheme_browseRowsMarginStart, 0); 616 mContainerListAlignTop = (int) ta.getDimension( 617 R.styleable.LeanbackTheme_browseRowsMarginTop, 0); 618 ta.recycle(); 619 620 readArguments(getArguments()); 621 622 if (mCanShowHeaders) { 623 if (mHeadersBackStackEnabled) { 624 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 625 mBackStackChangedListener = new BackStackListener(); 626 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 627 mBackStackChangedListener.load(savedInstanceState); 628 } else { 629 if (savedInstanceState != null) { 630 mShowingHeaders = savedInstanceState.getBoolean(HEADER_SHOW); 631 } 632 } 633 } 634 635 } 636 637 @Override 638 public void onDestroy() { 639 if (mBackStackChangedListener != null) { 640 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 641 } 642 super.onDestroy(); 643 } 644 645 @Override 646 public View onCreateView(LayoutInflater inflater, ViewGroup container, 647 Bundle savedInstanceState) { 648 if (getChildFragmentManager().findFragmentById(R.id.browse_container_dock) == null) { 649 mRowsFragment = new RowsFragment(); 650 mHeadersFragment = new HeadersFragment(); 651 getChildFragmentManager().beginTransaction() 652 .replace(R.id.browse_headers_dock, mHeadersFragment) 653 .replace(R.id.browse_container_dock, mRowsFragment).commit(); 654 } else { 655 mHeadersFragment = (HeadersFragment) getChildFragmentManager() 656 .findFragmentById(R.id.browse_headers_dock); 657 mRowsFragment = (RowsFragment) getChildFragmentManager() 658 .findFragmentById(R.id.browse_container_dock); 659 } 660 661 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 662 663 mRowsFragment.setAdapter(mAdapter); 664 if (mHeaderPresenterSelector != null) { 665 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 666 } 667 mHeadersFragment.setAdapter(mAdapter); 668 669 mRowsFragment.enableRowScaling(mRowScaleEnabled); 670 mRowsFragment.setOnItemSelectedListener(mRowSelectedListener); 671 mRowsFragment.setOnItemViewSelectedListener(mRowViewSelectedListener); 672 mHeadersFragment.setOnItemSelectedListener(mHeaderSelectedListener); 673 mHeadersFragment.setOnHeaderClickedListener(mHeaderClickedListener); 674 mRowsFragment.setOnItemClickedListener(mOnItemClickedListener); 675 mRowsFragment.setOnItemViewClickedListener(mOnItemViewClickedListener); 676 677 View root = inflater.inflate(R.layout.lb_browse_fragment, container, false); 678 679 mBrowseFrame = (BrowseFrameLayout) root.findViewById(R.id.browse_frame); 680 mBrowseFrame.setOnFocusSearchListener(mOnFocusSearchListener); 681 mBrowseFrame.setOnChildFocusListener(mOnChildFocusListener); 682 683 mTitleView = (TitleView) root.findViewById(R.id.browse_title_group); 684 mTitleView.setTitle(mTitle); 685 mTitleView.setBadgeDrawable(mBadgeDrawable); 686 if (mSearchAffordanceColorSet) { 687 mTitleView.setSearchAffordanceColors(mSearchAffordanceColors); 688 } 689 if (mExternalOnSearchClickedListener != null) { 690 mTitleView.setOnSearchClickedListener(mExternalOnSearchClickedListener); 691 } 692 693 if (mBrandColorSet) { 694 mHeadersFragment.setBackgroundColor(mBrandColor); 695 } 696 697 mSceneWithTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 698 @Override 699 public void run() { 700 mTitleView.setVisibility(View.VISIBLE); 701 } 702 }); 703 mSceneWithoutTitle = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 704 @Override 705 public void run() { 706 mTitleView.setVisibility(View.INVISIBLE); 707 } 708 }); 709 mSceneWithHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 710 @Override 711 public void run() { 712 showHeaders(true); 713 } 714 }); 715 mSceneWithoutHeaders = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 716 @Override 717 public void run() { 718 showHeaders(false); 719 } 720 }); 721 mSceneAfterEntranceTransition = sTransitionHelper.createScene(mBrowseFrame, new Runnable() { 722 @Override 723 public void run() { 724 setEntranceTransitionEndState(); 725 } 726 }); 727 Context context = getActivity(); 728 mTitleUpTransition = sTransitionHelper.loadTransition(context, R.transition.lb_title_out); 729 mTitleDownTransition = sTransitionHelper.loadTransition(context, R.transition.lb_title_in); 730 731 if (savedInstanceState != null) { 732 mShowingTitle = savedInstanceState.getBoolean(TITLE_SHOW); 733 } 734 mTitleView.setVisibility(mShowingTitle ? View.VISIBLE: View.INVISIBLE); 735 736 return root; 737 } 738 739 private void createHeadersTransition() { 740 mHeadersTransition = sTransitionHelper.loadTransition(getActivity(), 741 mShowingHeaders ? 742 R.transition.lb_browse_headers_in : R.transition.lb_browse_headers_out); 743 744 sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() { 745 @Override 746 public void onTransitionStart(Object transition) { 747 } 748 @Override 749 public void onTransitionEnd(Object transition) { 750 mHeadersTransition = null; 751 mRowsFragment.onTransitionEnd(); 752 mHeadersFragment.onTransitionEnd(); 753 if (mShowingHeaders) { 754 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 755 if (headerGridView != null && !headerGridView.hasFocus()) { 756 headerGridView.requestFocus(); 757 } 758 } else { 759 VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView(); 760 if (rowsGridView != null && !rowsGridView.hasFocus()) { 761 rowsGridView.requestFocus(); 762 } 763 } 764 if (mBrowseTransitionListener != null) { 765 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 766 } 767 } 768 }); 769 } 770 771 /** 772 * Sets the {@link PresenterSelector} used to render the row headers. 773 * 774 * @param headerPresenterSelector The PresenterSelector that will determine 775 * the Presenter for each row header. 776 */ 777 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 778 mHeaderPresenterSelector = headerPresenterSelector; 779 if (mHeadersFragment != null) { 780 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 781 } 782 } 783 784 private void setRowsAlignedLeft(boolean alignLeft) { 785 MarginLayoutParams lp; 786 View containerList; 787 containerList = mRowsFragment.getView(); 788 lp = (MarginLayoutParams) containerList.getLayoutParams(); 789 lp.setMarginStart(alignLeft ? 0 : mContainerListMarginStart); 790 containerList.setLayoutParams(lp); 791 } 792 793 private void setHeadersOnScreen(boolean onScreen) { 794 MarginLayoutParams lp; 795 View containerList; 796 containerList = mHeadersFragment.getView(); 797 lp = (MarginLayoutParams) containerList.getLayoutParams(); 798 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 799 containerList.setLayoutParams(lp); 800 } 801 802 private void showHeaders(boolean show) { 803 if (DEBUG) Log.v(TAG, "showHeaders " + show); 804 mHeadersFragment.setHeadersEnabled(show); 805 setHeadersOnScreen(show); 806 setRowsAlignedLeft(!show); 807 mRowsFragment.setExpand(!show); 808 } 809 810 private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = 811 new HeadersFragment.OnHeaderClickedListener() { 812 @Override 813 public void onHeaderClicked() { 814 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 815 return; 816 } 817 startHeadersTransitionInternal(false); 818 mRowsFragment.getVerticalGridView().requestFocus(); 819 } 820 }; 821 822 private OnItemViewSelectedListener mRowViewSelectedListener = new OnItemViewSelectedListener() { 823 @Override 824 public void onItemSelected(Presenter.ViewHolder itemViewHolder, Object item, 825 RowPresenter.ViewHolder rowViewHolder, Row row) { 826 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 827 if (DEBUG) Log.v(TAG, "row selected position " + position); 828 onRowSelected(position); 829 if (mExternalOnItemViewSelectedListener != null) { 830 mExternalOnItemViewSelectedListener.onItemSelected(itemViewHolder, item, 831 rowViewHolder, row); 832 } 833 } 834 }; 835 836 private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() { 837 @Override 838 public void onItemSelected(Object item, Row row) { 839 if (mExternalOnItemSelectedListener != null) { 840 mExternalOnItemSelectedListener.onItemSelected(item, row); 841 } 842 } 843 }; 844 845 private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() { 846 @Override 847 public void onItemSelected(Object item, Row row) { 848 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 849 if (DEBUG) Log.v(TAG, "header selected position " + position); 850 onRowSelected(position); 851 } 852 }; 853 854 private void onRowSelected(int position) { 855 if (position != mSelectedPosition) { 856 mSetSelectionRunnable.mPosition = position; 857 mBrowseFrame.getHandler().post(mSetSelectionRunnable); 858 859 if (getAdapter() == null || getAdapter().size() == 0 || position == 0) { 860 if (!mShowingTitle) { 861 sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition); 862 mShowingTitle = true; 863 } 864 } else if (mShowingTitle) { 865 sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition); 866 mShowingTitle = false; 867 } 868 } 869 } 870 871 private class SetSelectionRunnable implements Runnable { 872 int mPosition; 873 boolean mSmooth = true; 874 @Override 875 public void run() { 876 setSelection(mPosition, mSmooth); 877 } 878 } 879 880 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 881 882 private void setSelection(int position, boolean smooth) { 883 if (position != NO_POSITION) { 884 mRowsFragment.setSelectedPosition(position, smooth); 885 mHeadersFragment.setSelectedPosition(position, smooth); 886 } 887 mSelectedPosition = position; 888 } 889 890 /** 891 * Sets the selected row position with smooth animation. 892 */ 893 public void setSelectedPosition(int position) { 894 setSelectedPosition(position, true); 895 } 896 897 /** 898 * Sets the selected row position. 899 */ 900 public void setSelectedPosition(int position, boolean smooth) { 901 mSetSelectionRunnable.mPosition = position; 902 mSetSelectionRunnable.mSmooth = smooth; 903 mBrowseFrame.getHandler().post(mSetSelectionRunnable); 904 } 905 906 @Override 907 public void onStart() { 908 super.onStart(); 909 mHeadersFragment.setWindowAlignmentFromTop(mContainerListAlignTop); 910 mHeadersFragment.setItemAlignment(); 911 mRowsFragment.setWindowAlignmentFromTop(mContainerListAlignTop); 912 mRowsFragment.setItemAlignment(); 913 914 mRowsFragment.setScalePivots(0, mContainerListAlignTop); 915 916 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 917 mHeadersFragment.getView().requestFocus(); 918 } else if ((!mCanShowHeaders || !mShowingHeaders) 919 && mRowsFragment.getView() != null) { 920 mRowsFragment.getView().requestFocus(); 921 } 922 if (mCanShowHeaders) { 923 showHeaders(mShowingHeaders); 924 } 925 if (isEntranceTransitionEnabled()) { 926 setEntranceTransitionStartState(); 927 } 928 } 929 930 @Override 931 public void onPause() { 932 mTitleView.enableAnimation(false); 933 super.onPause(); 934 } 935 936 @Override 937 public void onResume() { 938 super.onResume(); 939 mTitleView.enableAnimation(true); 940 } 941 942 /** 943 * Enable/disable headers transition on back key support. This is enabled by 944 * default. The BrowseFragment will add a back stack entry when headers are 945 * showing. Running a headers transition when the back key is pressed only 946 * works when the headers state is {@link #HEADERS_ENABLED} or 947 * {@link #HEADERS_HIDDEN}. 948 * <p> 949 * NOTE: If an Activity has its own onBackPressed() handling, you must 950 * disable this feature. You may use {@link #startHeadersTransition(boolean)} 951 * and {@link BrowseTransitionListener} in your own back stack handling. 952 */ 953 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 954 mHeadersBackStackEnabled = headersBackStackEnabled; 955 } 956 957 /** 958 * Returns true if headers transition on back key support is enabled. 959 */ 960 public final boolean isHeadersTransitionOnBackEnabled() { 961 return mHeadersBackStackEnabled; 962 } 963 964 private void readArguments(Bundle args) { 965 if (args == null) { 966 return; 967 } 968 if (args.containsKey(ARG_TITLE)) { 969 setTitle(args.getString(ARG_TITLE)); 970 } 971 if (args.containsKey(ARG_HEADERS_STATE)) { 972 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 973 } 974 } 975 976 /** 977 * Sets the drawable displayed in the browse fragment title. 978 * 979 * @param drawable The Drawable to display in the browse fragment title. 980 */ 981 public void setBadgeDrawable(Drawable drawable) { 982 if (mBadgeDrawable != drawable) { 983 mBadgeDrawable = drawable; 984 if (mTitleView != null) { 985 mTitleView.setBadgeDrawable(drawable); 986 } 987 } 988 } 989 990 /** 991 * Returns the badge drawable used in the fragment title. 992 */ 993 public Drawable getBadgeDrawable() { 994 return mBadgeDrawable; 995 } 996 997 /** 998 * Sets a title for the browse fragment. 999 * 1000 * @param title The title of the browse fragment. 1001 */ 1002 public void setTitle(String title) { 1003 mTitle = title; 1004 if (mTitleView != null) { 1005 mTitleView.setTitle(title); 1006 } 1007 } 1008 1009 /** 1010 * Returns the title for the browse fragment. 1011 */ 1012 public String getTitle() { 1013 return mTitle; 1014 } 1015 1016 /** 1017 * Sets the state for the headers column in the browse fragment. Must be one 1018 * of {@link #HEADERS_ENABLED}, {@link #HEADERS_HIDDEN}, or 1019 * {@link #HEADERS_DISABLED}. 1020 * 1021 * @param headersState The state of the headers for the browse fragment. 1022 */ 1023 public void setHeadersState(int headersState) { 1024 if (headersState < HEADERS_ENABLED || headersState > HEADERS_DISABLED) { 1025 throw new IllegalArgumentException("Invalid headers state: " + headersState); 1026 } 1027 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 1028 1029 if (headersState != mHeadersState) { 1030 mHeadersState = headersState; 1031 switch (headersState) { 1032 case HEADERS_ENABLED: 1033 mCanShowHeaders = true; 1034 mShowingHeaders = true; 1035 break; 1036 case HEADERS_HIDDEN: 1037 mCanShowHeaders = true; 1038 mShowingHeaders = false; 1039 break; 1040 case HEADERS_DISABLED: 1041 mCanShowHeaders = false; 1042 mShowingHeaders = false; 1043 break; 1044 default: 1045 Log.w(TAG, "Unknown headers state: " + headersState); 1046 break; 1047 } 1048 if (mHeadersFragment != null) { 1049 mHeadersFragment.setHeadersGone(!mCanShowHeaders); 1050 } 1051 } 1052 } 1053 1054 /** 1055 * Returns the state of the headers column in the browse fragment. 1056 */ 1057 public int getHeadersState() { 1058 return mHeadersState; 1059 } 1060 1061 @Override 1062 protected Object createEntranceTransition() { 1063 return sTransitionHelper.loadTransition(getActivity(), 1064 R.transition.lb_browse_entrance_transition); 1065 } 1066 1067 @Override 1068 protected void runEntranceTransition(Object entranceTransition) { 1069 sTransitionHelper.runTransition(mSceneAfterEntranceTransition, 1070 entranceTransition); 1071 } 1072 1073 @Override 1074 protected void onEntranceTransitionStart() { 1075 mHeadersFragment.onTransitionStart(); 1076 mRowsFragment.onTransitionStart(); 1077 } 1078 1079 @Override 1080 protected void onEntranceTransitionEnd() { 1081 mRowsFragment.onTransitionEnd(); 1082 mHeadersFragment.onTransitionEnd(); 1083 } 1084 1085 void setSearchOrbViewOnScreen(boolean onScreen) { 1086 View searchOrbView = mTitleView.getSearchAffordanceView(); 1087 MarginLayoutParams lp = (MarginLayoutParams) searchOrbView.getLayoutParams(); 1088 lp.setMarginStart(onScreen ? 0 : -mContainerListMarginStart); 1089 searchOrbView.setLayoutParams(lp); 1090 } 1091 1092 void setEntranceTransitionStartState() { 1093 setHeadersOnScreen(false); 1094 setSearchOrbViewOnScreen(false); 1095 mRowsFragment.setEntranceTransitionState(false); 1096 } 1097 1098 void setEntranceTransitionEndState() { 1099 setHeadersOnScreen(mShowingHeaders); 1100 setSearchOrbViewOnScreen(true); 1101 mRowsFragment.setEntranceTransitionState(true); 1102 } 1103 1104} 1105 1106