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