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