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