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