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