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