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