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