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