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