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