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