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