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