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