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