BrowseFragment.java revision 74c29896d6e2a520e00605cceeef64669bab02b3
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.setAdapter(mAdapter); 529 if (mHeaderPresenterSelector != null) { 530 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 531 } 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 mTitleUpTransition = sTransitionHelper.createChangeBounds(false); 589 sTransitionHelper.setInterpolator(mTitleUpTransition, new DecelerateInterpolator(4)); 590 mTitleDownTransition = sTransitionHelper.createChangeBounds(false); 591 sTransitionHelper.setInterpolator(mTitleDownTransition, new DecelerateInterpolator()); 592 593 sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.browse_headers, true); 594 sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.browse_headers, true); 595 sTransitionHelper.excludeChildren(mTitleUpTransition, R.id.container_list, true); 596 sTransitionHelper.excludeChildren(mTitleDownTransition, R.id.container_list, true); 597 598 return root; 599 } 600 601 private void createHeadersTransition() { 602 mHeadersTransition = sTransitionHelper.createTransitionSet(false); 603 sTransitionHelper.excludeChildren(mHeadersTransition, R.id.browse_title_group, true); 604 Object changeBounds = sTransitionHelper.createChangeBounds(false); 605 Object fadeIn = sTransitionHelper.createFadeTransition(TransitionHelper.FADE_IN); 606 Object fadeOut = sTransitionHelper.createFadeTransition(TransitionHelper.FADE_OUT); 607 608 sTransitionHelper.setDuration(fadeOut, mHeadersTransitionDuration); 609 sTransitionHelper.addTransition(mHeadersTransition, fadeOut); 610 if (mShowingHeaders) { 611 sTransitionHelper.setStartDelay(changeBounds, mHeadersTransitionStartDelay); 612 } 613 sTransitionHelper.setDuration(changeBounds, mHeadersTransitionDuration); 614 sTransitionHelper.addTransition(mHeadersTransition, changeBounds); 615 sTransitionHelper.setDuration(fadeIn, mHeadersTransitionDuration); 616 sTransitionHelper.setStartDelay(fadeIn, mHeadersTransitionStartDelay); 617 sTransitionHelper.addTransition(mHeadersTransition, fadeIn); 618 619 sTransitionHelper.setTransitionListener(mHeadersTransition, new TransitionListener() { 620 @Override 621 public void onTransitionStart(Object transition) { 622 } 623 @Override 624 public void onTransitionEnd(Object transition) { 625 mHeadersTransition = null; 626 mRowsFragment.onTransitionEnd(); 627 mHeadersFragment.onTransitionEnd(); 628 if (mShowingHeaders) { 629 VerticalGridView headerGridView = mHeadersFragment.getVerticalGridView(); 630 if (headerGridView != null && !headerGridView.hasFocus()) { 631 headerGridView.requestFocus(); 632 } 633 } else { 634 VerticalGridView rowsGridView = mRowsFragment.getVerticalGridView(); 635 if (rowsGridView != null && !rowsGridView.hasFocus()) { 636 rowsGridView.requestFocus(); 637 } 638 } 639 if (mBrowseTransitionListener != null) { 640 mBrowseTransitionListener.onHeadersTransitionStop(mShowingHeaders); 641 } 642 } 643 }); 644 } 645 646 public void setHeaderPresenterSelector(PresenterSelector headerPresenterSelector) { 647 mHeaderPresenterSelector = headerPresenterSelector; 648 if (mHeadersFragment != null) { 649 mHeadersFragment.setPresenterSelector(mHeaderPresenterSelector); 650 } 651 } 652 653 private void showTitle(boolean show) { 654 MarginLayoutParams lp = (MarginLayoutParams) mBrowseTitle.getLayoutParams(); 655 lp.topMargin = show ? 0 : -mBrowseTitle.getHeight(); 656 mBrowseTitle.setLayoutParams(lp); 657 } 658 659 private void showHeaders(boolean show) { 660 if (DEBUG) Log.v(TAG, "showHeaders " + show); 661 mHeadersFragment.setHeadersVisiblity(show); 662 MarginLayoutParams lp; 663 View containerList; 664 665 containerList = mRowsFragment.getView(); 666 lp = (MarginLayoutParams) containerList.getLayoutParams(); 667 lp.leftMargin = show ? mContainerListMarginLeft : 0; 668 containerList.setLayoutParams(lp); 669 670 containerList = mHeadersFragment.getView(); 671 lp = (MarginLayoutParams) containerList.getLayoutParams(); 672 lp.leftMargin = show ? 0 : -mContainerListMarginLeft; 673 containerList.setLayoutParams(lp); 674 675 mRowsFragment.setExpand(!show); 676 } 677 678 private HeadersFragment.OnHeaderClickedListener mHeaderClickedListener = 679 new HeadersFragment.OnHeaderClickedListener() { 680 @Override 681 public void onHeaderClicked() { 682 if (!mCanShowHeaders || !mShowingHeaders || isInHeadersTransition()) { 683 return; 684 } 685 startHeadersTransitionInternal(false); 686 mRowsFragment.getVerticalGridView().requestFocus(); 687 } 688 }; 689 690 private OnItemSelectedListener mRowSelectedListener = new OnItemSelectedListener() { 691 @Override 692 public void onItemSelected(Object item, Row row) { 693 int position = mRowsFragment.getVerticalGridView().getSelectedPosition(); 694 if (DEBUG) Log.v(TAG, "row selected position " + position); 695 onRowSelected(position); 696 if (mExternalOnItemSelectedListener != null) { 697 mExternalOnItemSelectedListener.onItemSelected(item, row); 698 } 699 } 700 }; 701 702 private OnItemSelectedListener mHeaderSelectedListener = new OnItemSelectedListener() { 703 @Override 704 public void onItemSelected(Object item, Row row) { 705 int position = mHeadersFragment.getVerticalGridView().getSelectedPosition(); 706 if (DEBUG) Log.v(TAG, "header selected position " + position); 707 onRowSelected(position); 708 } 709 }; 710 711 private void onRowSelected(int position) { 712 if (position != mSelectedPosition) { 713 mSetSelectionRunnable.mPosition = position; 714 mBrowseFrame.getHandler().post(mSetSelectionRunnable); 715 716 if (position == 0) { 717 if (!mShowingTitle) { 718 sTransitionHelper.runTransition(mSceneWithTitle, mTitleDownTransition); 719 mShowingTitle = true; 720 } 721 } else if (mShowingTitle) { 722 sTransitionHelper.runTransition(mSceneWithoutTitle, mTitleUpTransition); 723 mShowingTitle = false; 724 } 725 } 726 } 727 728 private class SetSelectionRunnable implements Runnable { 729 int mPosition; 730 @Override 731 public void run() { 732 setSelection(mPosition); 733 } 734 } 735 736 private final SetSelectionRunnable mSetSelectionRunnable = new SetSelectionRunnable(); 737 738 private void setSelection(int position) { 739 if (position != NO_POSITION) { 740 mRowsFragment.setSelectedPosition(position); 741 mHeadersFragment.setSelectedPosition(position); 742 } 743 mSelectedPosition = position; 744 } 745 746 private void setVerticalGridViewLayout(VerticalGridView listview, int extraOffset) { 747 // align the top edge of item to a fixed position 748 listview.setItemAlignmentOffset(0); 749 listview.setItemAlignmentOffsetPercent(VerticalGridView.ITEM_ALIGN_OFFSET_PERCENT_DISABLED); 750 listview.setWindowAlignmentOffset(mContainerListAlignTop + extraOffset); 751 listview.setWindowAlignmentOffsetPercent(VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); 752 listview.setWindowAlignment(VerticalGridView.WINDOW_ALIGN_NO_EDGE); 753 } 754 755 /** 756 * Setup dimensions that are only meaningful when the child Fragments are inside 757 * BrowseFragment. 758 */ 759 private void setupChildFragmentsLayout() { 760 VerticalGridView headerList = mHeadersFragment.getVerticalGridView(); 761 VerticalGridView containerList = mRowsFragment.getVerticalGridView(); 762 763 // Both fragments list view has the same alignment 764 setVerticalGridViewLayout(headerList, 16); 765 setVerticalGridViewLayout(containerList, 0); 766 } 767 768 @Override 769 public void onStart() { 770 super.onStart(); 771 setupChildFragmentsLayout(); 772 if (mCanShowHeaders && mShowingHeaders && mHeadersFragment.getView() != null) { 773 mHeadersFragment.getView().requestFocus(); 774 } else if ((!mCanShowHeaders || !mShowingHeaders) 775 && mRowsFragment.getView() != null) { 776 mRowsFragment.getView().requestFocus(); 777 } 778 showHeaders(mCanShowHeaders && mShowingHeaders); 779 if (mCanShowHeaders && mHeadersBackStackEnabled) { 780 mWithHeadersBackStackName = LB_HEADERS_BACKSTACK + this; 781 if (mBackStackChangedListener == null) { 782 mBackStackChangedListener = new BackStackListener(); 783 } else { 784 mBackStackChangedListener.reset(); 785 } 786 getFragmentManager().addOnBackStackChangedListener(mBackStackChangedListener); 787 if (!mShowingHeaders) { 788 getFragmentManager().beginTransaction() 789 .addToBackStack(mWithHeadersBackStackName).commit(); 790 } 791 } 792 } 793 794 @Override 795 public void onStop() { 796 if (mBackStackChangedListener != null) { 797 getFragmentManager().removeOnBackStackChangedListener(mBackStackChangedListener); 798 } 799 super.onStop(); 800 } 801 802 /** 803 * Enable/disable headers transition on back key support. This is enabled by default. 804 * BrowseFragment will add a back stack entry when headers are showing. 805 * Headers transition on back key only works for {@link #HEADERS_ENABLED} 806 * or {@link #HEADERS_HIDDEN}. 807 * <p> 808 * NOTE: If app has its own onBackPressed() handling, 809 * app must disable this feature, app may use {@link #startHeadersTransition(boolean)} 810 * and {@link BrowseTransitionListener} in its own back stack handling. 811 */ 812 public final void setHeadersTransitionOnBackEnabled(boolean headersBackStackEnabled) { 813 mHeadersBackStackEnabled = headersBackStackEnabled; 814 } 815 816 /** 817 * Returns true if headers transition on back key support is enabled. 818 */ 819 public final boolean isHeadersTransitionOnBackEnabled() { 820 return mHeadersBackStackEnabled; 821 } 822 823 private void readArguments(Bundle args) { 824 if (args == null) { 825 return; 826 } 827 if (args.containsKey(ARG_TITLE)) { 828 setTitle(args.getString(ARG_TITLE)); 829 } 830 831 if (args.containsKey(ARG_BADGE_URI)) { 832 setBadgeUri(args.getString(ARG_BADGE_URI)); 833 } 834 835 if (args.containsKey(ARG_HEADERS_STATE)) { 836 setHeadersState(args.getInt(ARG_HEADERS_STATE)); 837 } 838 } 839 840 private void setBadgeUri(String badgeUri) { 841 // TODO - need a drawable downloader 842 } 843 844 private void setBadgeDrawable(Drawable drawable) { 845 if (mBadgeView == null) { 846 return; 847 } 848 mBadgeView.setImageDrawable(drawable); 849 if (drawable != null) { 850 mBadgeView.setVisibility(View.VISIBLE); 851 mTitleView.setVisibility(View.GONE); 852 } else { 853 mBadgeView.setVisibility(View.GONE); 854 mTitleView.setVisibility(View.VISIBLE); 855 } 856 } 857 858 private void setTitle(String title) { 859 if (mTitleView != null) { 860 mTitleView.setText(title); 861 } 862 } 863 864 private void setHeadersState(int headersState) { 865 if (DEBUG) Log.v(TAG, "setHeadersState " + headersState); 866 switch (headersState) { 867 case HEADERS_ENABLED: 868 mCanShowHeaders = true; 869 mShowingHeaders = true; 870 break; 871 case HEADERS_HIDDEN: 872 mCanShowHeaders = true; 873 mShowingHeaders = false; 874 break; 875 case HEADERS_DISABLED: 876 mCanShowHeaders = false; 877 mShowingHeaders = false; 878 break; 879 default: 880 Log.w(TAG, "Unknown headers state: " + headersState); 881 break; 882 } 883 } 884} 885