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