ContactDetailLayoutController.java revision 187c8167d77687fbc04237c9ac1e87564b2d5f33
1/* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.contacts.detail; 18 19import com.android.contacts.ContactLoader; 20 21import com.android.contacts.NfcHandler; 22import com.android.contacts.R; 23import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener; 24import com.android.contacts.util.PhoneCapabilityTester; 25import com.android.contacts.util.UriUtils; 26import com.android.contacts.widget.TransitionAnimationView; 27 28import android.animation.Animator; 29import android.animation.Animator.AnimatorListener; 30import android.animation.ObjectAnimator; 31import android.app.Activity; 32import android.app.FragmentManager; 33import android.app.FragmentTransaction; 34import android.content.Context; 35import android.net.Uri; 36import android.os.Bundle; 37import android.support.v4.view.ViewPager; 38import android.support.v4.view.ViewPager.OnPageChangeListener; 39import android.util.Log; 40import android.view.LayoutInflater; 41import android.view.View; 42import android.view.ViewPropertyAnimator; 43import android.view.animation.AnimationUtils; 44import android.widget.AbsListView; 45import android.widget.AbsListView.OnScrollListener; 46 47/** 48 * Determines the layout of the contact card. 49 */ 50public class ContactDetailLayoutController { 51 52 private static final String KEY_CONTACT_URI = "contactUri"; 53 private static final String KEY_CONTACT_HAS_UPDATES = "contactHasUpdates"; 54 private static final String KEY_CURRENT_PAGE_INDEX = "currentPageIndex"; 55 56 private static final int TAB_INDEX_DETAIL = 0; 57 private static final int TAB_INDEX_UPDATES = 1; 58 59 private final int SINGLE_PANE_FADE_IN_DURATION = 275; 60 61 private static final String TAG = "ContactDetailLayoutController"; 62 private static final boolean DEBUG = false; 63 64 /** 65 * There are 3 possible layouts for the contact detail screen: 66 * 1. TWO_COLUMN - Tall and wide screen so the 2 pages can be shown side-by-side 67 * 2. VIEW_PAGER_AND_TAB_CAROUSEL - Tall and narrow screen to allow swipe between the 2 pages 68 * 3. FRAGMENT_CAROUSEL- Short and wide screen to allow half of the other page to show at a time 69 */ 70 private enum LayoutMode { 71 TWO_COLUMN, VIEW_PAGER_AND_TAB_CAROUSEL, FRAGMENT_CAROUSEL, 72 } 73 74 private final Activity mActivity; 75 private final LayoutInflater mLayoutInflater; 76 private final FragmentManager mFragmentManager; 77 78 private final View mViewContainer; 79 private final TransitionAnimationView mTransitionAnimationView; 80 private ContactDetailFragment mDetailFragment; 81 private ContactDetailUpdatesFragment mUpdatesFragment; 82 83 private View mDetailFragmentView; 84 private View mUpdatesFragmentView; 85 86 private final ViewPager mViewPager; 87 private ContactDetailViewPagerAdapter mViewPagerAdapter; 88 private int mViewPagerState; 89 90 private final ContactDetailTabCarousel mTabCarousel; 91 private final ContactDetailFragmentCarousel mFragmentCarousel; 92 93 private final ContactDetailFragment.Listener mContactDetailFragmentListener; 94 95 private ContactLoader.Result mContactData; 96 private Uri mContactUri; 97 98 private boolean mTabCarouselIsAnimating; 99 100 private boolean mContactHasUpdates; 101 102 private LayoutMode mLayoutMode; 103 104 public ContactDetailLayoutController(Activity activity, Bundle savedState, 105 FragmentManager fragmentManager, TransitionAnimationView animationView, 106 View viewContainer, ContactDetailFragment.Listener contactDetailFragmentListener) { 107 108 if (fragmentManager == null) { 109 throw new IllegalStateException("Cannot initialize a ContactDetailLayoutController " 110 + "without a non-null FragmentManager"); 111 } 112 113 mActivity = activity; 114 mLayoutInflater = (LayoutInflater) activity.getSystemService( 115 Context.LAYOUT_INFLATER_SERVICE); 116 mFragmentManager = fragmentManager; 117 mContactDetailFragmentListener = contactDetailFragmentListener; 118 119 mTransitionAnimationView = animationView; 120 121 // Retrieve views in case this is view pager and carousel mode 122 mViewContainer = viewContainer; 123 124 mViewPager = (ViewPager) viewContainer.findViewById(R.id.pager); 125 mTabCarousel = (ContactDetailTabCarousel) viewContainer.findViewById(R.id.tab_carousel); 126 127 // Retrieve view in case this is in fragment carousel mode 128 mFragmentCarousel = (ContactDetailFragmentCarousel) viewContainer.findViewById( 129 R.id.fragment_carousel); 130 131 // Retrieve container views in case they are already in the XML layout 132 mDetailFragmentView = viewContainer.findViewById(R.id.about_fragment_container); 133 mUpdatesFragmentView = viewContainer.findViewById(R.id.updates_fragment_container); 134 135 // Determine the layout mode based on the presence of certain views in the layout XML. 136 if (mViewPager != null) { 137 mLayoutMode = LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL; 138 if (DEBUG) Log.d(TAG, "set layout mode to VIEW_PAGER_AND_TAB_CAROUSEL"); 139 } else if (mFragmentCarousel != null) { 140 mLayoutMode = LayoutMode.FRAGMENT_CAROUSEL; 141 if (DEBUG) Log.d(TAG, "set layout mode to FRAGMENT_CAROUSEL"); 142 } else { 143 mLayoutMode = LayoutMode.TWO_COLUMN; 144 if (DEBUG) Log.d(TAG, "set layout mode to TWO_COLUMN"); 145 } 146 147 initialize(savedState); 148 } 149 150 private void initialize(Bundle savedState) { 151 boolean fragmentsAddedToFragmentManager = true; 152 mDetailFragment = (ContactDetailFragment) mFragmentManager.findFragmentByTag( 153 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 154 mUpdatesFragment = (ContactDetailUpdatesFragment) mFragmentManager.findFragmentByTag( 155 ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); 156 157 // If the detail fragment was found in the {@link FragmentManager} then we don't need to add 158 // it again. Otherwise, create the fragments dynamically and remember to add them to the 159 // {@link FragmentManager}. 160 if (mDetailFragment == null) { 161 mDetailFragment = new ContactDetailFragment(); 162 mUpdatesFragment = new ContactDetailUpdatesFragment(); 163 fragmentsAddedToFragmentManager = false; 164 } 165 166 mDetailFragment.setListener(mContactDetailFragmentListener); 167 NfcHandler.register(mActivity, mDetailFragment); 168 169 // Read from savedState if possible 170 int currentPageIndex = 0; 171 if (savedState != null) { 172 mContactUri = savedState.getParcelable(KEY_CONTACT_URI); 173 mContactHasUpdates = savedState.getBoolean(KEY_CONTACT_HAS_UPDATES); 174 currentPageIndex = savedState.getInt(KEY_CURRENT_PAGE_INDEX, 0); 175 } 176 177 switch (mLayoutMode) { 178 case VIEW_PAGER_AND_TAB_CAROUSEL: { 179 // Inflate 2 view containers to pass in as children to the {@link ViewPager}, 180 // which will in turn be the parents to the mDetailFragment and mUpdatesFragment 181 // since the fragments must have the same parent view IDs in both landscape and 182 // portrait layouts. 183 mDetailFragmentView = mLayoutInflater.inflate( 184 R.layout.contact_detail_about_fragment_container, mViewPager, false); 185 mUpdatesFragmentView = mLayoutInflater.inflate( 186 R.layout.contact_detail_updates_fragment_container, mViewPager, false); 187 188 mViewPagerAdapter = new ContactDetailViewPagerAdapter(); 189 mViewPagerAdapter.setAboutFragmentView(mDetailFragmentView); 190 mViewPagerAdapter.setUpdatesFragmentView(mUpdatesFragmentView); 191 192 mViewPager.addView(mDetailFragmentView); 193 mViewPager.addView(mUpdatesFragmentView); 194 mViewPager.setAdapter(mViewPagerAdapter); 195 mViewPager.setOnPageChangeListener(mOnPageChangeListener); 196 197 if (!fragmentsAddedToFragmentManager) { 198 FragmentTransaction transaction = mFragmentManager.beginTransaction(); 199 transaction.add(R.id.about_fragment_container, mDetailFragment, 200 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 201 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 202 ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); 203 transaction.commitAllowingStateLoss(); 204 mFragmentManager.executePendingTransactions(); 205 } 206 207 mTabCarousel.setListener(mTabCarouselListener); 208 mTabCarousel.restoreCurrentTab(currentPageIndex); 209 mDetailFragment.setVerticalScrollListener( 210 new VerticalScrollListener(TAB_INDEX_DETAIL)); 211 mUpdatesFragment.setVerticalScrollListener( 212 new VerticalScrollListener(TAB_INDEX_UPDATES)); 213 mViewPager.setCurrentItem(currentPageIndex); 214 break; 215 } 216 case TWO_COLUMN: { 217 if (!fragmentsAddedToFragmentManager) { 218 FragmentTransaction transaction = mFragmentManager.beginTransaction(); 219 transaction.add(R.id.about_fragment_container, mDetailFragment, 220 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 221 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 222 ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); 223 transaction.commitAllowingStateLoss(); 224 mFragmentManager.executePendingTransactions(); 225 } 226 break; 227 } 228 case FRAGMENT_CAROUSEL: { 229 // Add the fragments to the fragment containers in the carousel using a 230 // {@link FragmentTransaction} if they haven't already been added to the 231 // {@link FragmentManager}. 232 if (!fragmentsAddedToFragmentManager) { 233 FragmentTransaction transaction = mFragmentManager.beginTransaction(); 234 transaction.add(R.id.about_fragment_container, mDetailFragment, 235 ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); 236 transaction.add(R.id.updates_fragment_container, mUpdatesFragment, 237 ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); 238 transaction.commitAllowingStateLoss(); 239 mFragmentManager.executePendingTransactions(); 240 } 241 242 mFragmentCarousel.setFragmentViews(mDetailFragmentView, mUpdatesFragmentView); 243 mFragmentCarousel.setFragments(mDetailFragment, mUpdatesFragment); 244 mFragmentCarousel.setCurrentPage(currentPageIndex); 245 break; 246 } 247 } 248 249 // Setup the layout if we already have a saved state 250 if (savedState != null) { 251 if (mContactHasUpdates) { 252 showContactWithUpdates(false); 253 } else { 254 showContactWithoutUpdates(); 255 } 256 } 257 } 258 259 public void setContactData(ContactLoader.Result data) { 260 final boolean contactWasLoaded; 261 final boolean contactHadUpdates; 262 final boolean isDifferentContact; 263 if (mContactData == null) { 264 contactHadUpdates = false; 265 contactWasLoaded = false; 266 isDifferentContact = true; 267 } else { 268 contactHadUpdates = mContactHasUpdates; 269 contactWasLoaded = true; 270 isDifferentContact = 271 !UriUtils.areEqual(mContactData.getLookupUri(), data.getLookupUri()); 272 } 273 mContactData = data; 274 mContactHasUpdates = !data.getStreamItems().isEmpty(); 275 276 if (PhoneCapabilityTester.isUsingTwoPanes(mActivity)) { 277 // Tablet: If we already showed data before, we want to cross-fade from screen to screen 278 if (contactWasLoaded && mTransitionAnimationView != null && isDifferentContact) { 279 mTransitionAnimationView.startTransition( 280 mViewContainer, mContactData == null); 281 } 282 } else { 283 // Small screen: We are on our own screen. Fade the data in, but only the first time 284 if (!contactWasLoaded) { 285 mViewContainer.setAlpha(0.0f); 286 final ViewPropertyAnimator animator = mViewContainer.animate(); 287 animator.alpha(1.0f); 288 animator.setDuration(SINGLE_PANE_FADE_IN_DURATION); 289 } 290 } 291 292 if (mContactHasUpdates) { 293 showContactWithUpdates( 294 contactWasLoaded && contactHadUpdates == false); 295 } else { 296 showContactWithoutUpdates(); 297 } 298 } 299 300 public void showEmptyState() { 301 switch (mLayoutMode) { 302 case FRAGMENT_CAROUSEL: { 303 mFragmentCarousel.setCurrentPage(0); 304 mFragmentCarousel.enableSwipe(false); 305 mDetailFragment.showEmptyState(); 306 break; 307 } 308 case TWO_COLUMN: { 309 mDetailFragment.setShowStaticPhoto(false); 310 mUpdatesFragmentView.setVisibility(View.GONE); 311 mDetailFragment.showEmptyState(); 312 break; 313 } 314 case VIEW_PAGER_AND_TAB_CAROUSEL: { 315 mDetailFragment.setShowStaticPhoto(false); 316 mDetailFragment.showEmptyState(); 317 mTabCarousel.loadData(null); 318 mTabCarousel.setVisibility(View.GONE); 319 mViewPagerAdapter.enableSwipe(false); 320 mViewPager.setCurrentItem(0); 321 break; 322 } 323 default: 324 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 325 } 326 } 327 328 /** 329 * Setup the layout for the contact with updates. 330 * TODO: Clean up this method so it's easier to understand. 331 */ 332 private void showContactWithUpdates(boolean animateStateChange) { 333 if (mContactData == null) { 334 return; 335 } 336 337 Uri previousContactUri = mContactUri; 338 mContactUri = mContactData.getLookupUri(); 339 boolean isDifferentContact = !UriUtils.areEqual(previousContactUri, mContactUri); 340 341 switch (mLayoutMode) { 342 case TWO_COLUMN: { 343 if (!isDifferentContact && animateStateChange) { 344 // This is screen is very hard to animate properly, because there is such a hard 345 // cut from the regular version. A proper animation would have to reflow text 346 // and move things around. Doing a simple cross-fade instead. 347 mTransitionAnimationView.startTransition(mViewContainer, false); 348 } 349 350 // Set the contact data (hide the static photo because the photo will already be in 351 // the header that scrolls with contact details). 352 mDetailFragment.setShowStaticPhoto(false); 353 // Show the updates fragment 354 mUpdatesFragmentView.setVisibility(View.VISIBLE); 355 break; 356 } 357 case VIEW_PAGER_AND_TAB_CAROUSEL: { 358 // Update and show the tab carousel (also restore its last saved position) 359 mTabCarousel.loadData(mContactData); 360 mTabCarousel.restoreYCoordinate(); 361 mTabCarousel.setVisibility(View.VISIBLE); 362 // Update ViewPager to allow swipe between all the fragments (to see updates) 363 mViewPagerAdapter.enableSwipe(true); 364 // If this is a different contact than before, then reset some views. 365 if (isDifferentContact) { 366 resetViewPager(); 367 resetTabCarousel(); 368 } 369 if (!isDifferentContact && animateStateChange) { 370 mTabCarousel.animateAppear(mViewContainer.getWidth(), 371 mDetailFragment.getFirstListItemOffset()); 372 } 373 break; 374 } 375 case FRAGMENT_CAROUSEL: { 376 // Allow swiping between all fragments 377 mFragmentCarousel.enableSwipe(true); 378 if (!isDifferentContact && animateStateChange) { 379 mFragmentCarousel.animateAppear(); 380 } 381 break; 382 } 383 default: 384 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 385 } 386 387 if (isDifferentContact) { 388 resetFragments(); 389 } 390 391 mDetailFragment.setData(mContactUri, mContactData); 392 mUpdatesFragment.setData(mContactUri, mContactData); 393 } 394 395 /** 396 * Setup the layout for the contact without updates. 397 * TODO: Clean up this method so it's easier to understand. 398 */ 399 private void showContactWithoutUpdates() { 400 if (mContactData == null) { 401 return; 402 } 403 404 Uri previousContactUri = mContactUri; 405 mContactUri = mContactData.getLookupUri(); 406 boolean isDifferentContact = !UriUtils.areEqual(previousContactUri, mContactUri); 407 408 switch (mLayoutMode) { 409 case TWO_COLUMN: 410 // Show the static photo which is next to the list of scrolling contact details 411 mDetailFragment.setShowStaticPhoto(true); 412 // Hide the updates fragment 413 mUpdatesFragmentView.setVisibility(View.GONE); 414 break; 415 case VIEW_PAGER_AND_TAB_CAROUSEL: 416 // Hide the tab carousel 417 mTabCarousel.setVisibility(View.GONE); 418 // Update ViewPager to disable swipe so that it only shows the detail fragment 419 // and switch to the detail fragment 420 mViewPagerAdapter.enableSwipe(false); 421 mViewPager.setCurrentItem(0, false /* smooth transition */); 422 break; 423 case FRAGMENT_CAROUSEL: { 424 // Disable swipe so only the detail fragment shows 425 mFragmentCarousel.setCurrentPage(0); 426 mFragmentCarousel.enableSwipe(false); 427 break; 428 } 429 default: 430 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 431 } 432 433 if (isDifferentContact) { 434 resetFragments(); 435 } 436 437 mDetailFragment.setData(mContactUri, mContactData); 438 } 439 440 private void resetTabCarousel() { 441 mTabCarousel.reset(); 442 } 443 444 private void resetViewPager() { 445 mViewPager.setCurrentItem(0, false /* smooth transition */); 446 } 447 448 private void resetFragments() { 449 mDetailFragment.resetAdapter(); 450 mUpdatesFragment.resetAdapter(); 451 } 452 453 public FragmentKeyListener getCurrentPage() { 454 switch (getCurrentPageIndex()) { 455 case 0: 456 return mDetailFragment; 457 case 1: 458 return mUpdatesFragment; 459 default: 460 throw new IllegalStateException("Invalid current item for ViewPager"); 461 } 462 } 463 464 private int getCurrentPageIndex() { 465 // If the contact has social updates, then retrieve the current page based on the 466 // {@link ViewPager} or fragment carousel. 467 if (mContactHasUpdates) { 468 if (mViewPager != null) { 469 return mViewPager.getCurrentItem(); 470 } else if (mFragmentCarousel != null) { 471 return mFragmentCarousel.getCurrentPage(); 472 } 473 } 474 // Otherwise return the default page (detail fragment). 475 return 0; 476 } 477 478 public void onSaveInstanceState(Bundle outState) { 479 outState.putParcelable(KEY_CONTACT_URI, mContactUri); 480 outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates); 481 outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPageIndex()); 482 } 483 484 private final OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { 485 486 private ObjectAnimator mTabCarouselAnimator; 487 488 @Override 489 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 490 // The user is horizontally dragging the {@link ViewPager}, so send 491 // these scroll changes to the tab carousel. Ignore these events though if the carousel 492 // is actually controlling the {@link ViewPager} scrolls because it will already be 493 // in the correct position. 494 if (mViewPager.isFakeDragging()) { 495 return; 496 } 497 int x = (int) ((position + positionOffset) * 498 mTabCarousel.getAllowedHorizontalScrollLength()); 499 mTabCarousel.scrollTo(x, 0); 500 } 501 502 @Override 503 public void onPageSelected(int position) { 504 // Since the {@link ViewPager} has committed to a new page now (but may not have 505 // finished scrolling yet), update the tab selection in the carousel. 506 mTabCarousel.setCurrentTab(position); 507 } 508 509 @Override 510 public void onPageScrollStateChanged(int state) { 511 if (mViewPagerState == ViewPager.SCROLL_STATE_IDLE) { 512 513 // If we are leaving the IDLE state, we are starting a swipe. 514 // First cancel any pending animations on the tab carousel. 515 cancelTabCarouselAnimator(); 516 517 // Sync the two lists because the list on the other page will start to show as 518 // we swipe over more. 519 syncScrollStateBetweenLists(mViewPager.getCurrentItem()); 520 521 } else if (state == ViewPager.SCROLL_STATE_IDLE) { 522 523 // Otherwise if the {@link ViewPager} is idle now, a page has been selected and 524 // scrolled into place. Perform an animation of the tab carousel is needed. 525 int currentPageIndex = mViewPager.getCurrentItem(); 526 int tabCarouselOffset = (int) mTabCarousel.getY(); 527 boolean shouldAnimateTabCarousel; 528 529 // Find the offset position of the first item in the list of the current page. 530 int listOffset = getOffsetOfFirstItemInList(currentPageIndex); 531 532 // If the list was able to successfully offset by the tab carousel amount, then 533 // log this as the new Y coordinate for that page, and no animation is needed. 534 if (listOffset == tabCarouselOffset) { 535 mTabCarousel.storeYCoordinate(currentPageIndex, tabCarouselOffset); 536 shouldAnimateTabCarousel = false; 537 } else if (listOffset == Integer.MIN_VALUE) { 538 // If the offset of the first item in the list is unknown (i.e. the item 539 // is no longer visible on screen) then just animate the tab carousel to the 540 // previously logged position. 541 shouldAnimateTabCarousel = true; 542 } else if (Math.abs(listOffset) < Math.abs(tabCarouselOffset)) { 543 // If the list could not offset the full amount of the tab carousel offset (i.e. 544 // the list can only be scrolled a tiny amount), then animate the carousel down 545 // to compensate. 546 mTabCarousel.storeYCoordinate(currentPageIndex, listOffset); 547 shouldAnimateTabCarousel = true; 548 } else { 549 // By default, animate back to the Y coordinate of the tab carousel the last 550 // time the other page was selected. 551 shouldAnimateTabCarousel = true; 552 } 553 554 if (shouldAnimateTabCarousel) { 555 float desiredOffset = mTabCarousel.getStoredYCoordinateForTab(currentPageIndex); 556 if (desiredOffset != tabCarouselOffset) { 557 createTabCarouselAnimator(desiredOffset); 558 mTabCarouselAnimator.start(); 559 } 560 } 561 } 562 mViewPagerState = state; 563 } 564 565 private void createTabCarouselAnimator(float desiredValue) { 566 mTabCarouselAnimator = ObjectAnimator.ofFloat( 567 mTabCarousel, "y", desiredValue).setDuration(75); 568 mTabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator( 569 mActivity, android.R.anim.accelerate_decelerate_interpolator)); 570 mTabCarouselAnimator.addListener(mTabCarouselAnimatorListener); 571 } 572 573 private void cancelTabCarouselAnimator() { 574 if (mTabCarouselAnimator != null) { 575 mTabCarouselAnimator.cancel(); 576 mTabCarouselAnimator = null; 577 mTabCarouselIsAnimating = false; 578 } 579 } 580 }; 581 582 private void syncScrollStateBetweenLists(int currentPageIndex) { 583 // Since the user interacted with the currently visible page, we need to sync the 584 // list on the other page (i.e. if the updates page is the current page, modify the 585 // list in the details page). 586 if (currentPageIndex == TAB_INDEX_UPDATES) { 587 mDetailFragment.requestToMoveToOffset((int) mTabCarousel.getY()); 588 } else { 589 mUpdatesFragment.requestToMoveToOffset((int) mTabCarousel.getY()); 590 } 591 } 592 593 private int getOffsetOfFirstItemInList(int currentPageIndex) { 594 if (currentPageIndex == TAB_INDEX_DETAIL) { 595 return mDetailFragment.getFirstListItemOffset(); 596 } else { 597 return mUpdatesFragment.getFirstListItemOffset(); 598 } 599 } 600 601 /** 602 * This listener keeps track of whether the tab carousel animation is currently going on or not, 603 * in order to prevent other simultaneous changes to the Y position of the tab carousel which 604 * can cause flicker. 605 */ 606 private final AnimatorListener mTabCarouselAnimatorListener = new AnimatorListener() { 607 608 @Override 609 public void onAnimationCancel(Animator animation) { 610 mTabCarouselIsAnimating = false; 611 } 612 613 @Override 614 public void onAnimationEnd(Animator animation) { 615 mTabCarouselIsAnimating = false; 616 } 617 618 @Override 619 public void onAnimationRepeat(Animator animation) { 620 mTabCarouselIsAnimating = true; 621 } 622 623 @Override 624 public void onAnimationStart(Animator animation) { 625 mTabCarouselIsAnimating = true; 626 } 627 }; 628 629 private final ContactDetailTabCarousel.Listener mTabCarouselListener = 630 new ContactDetailTabCarousel.Listener() { 631 632 @Override 633 public void onTouchDown() { 634 // The user just started scrolling the carousel, so begin "fake dragging" the 635 // {@link ViewPager} if it's not already doing so. 636 if (mViewPager.isFakeDragging()) { 637 return; 638 } 639 mViewPager.beginFakeDrag(); 640 } 641 642 @Override 643 public void onTouchUp() { 644 // The user just stopped scrolling the carousel, so stop "fake dragging" the 645 // {@link ViewPager} if was doing so before. 646 if (mViewPager.isFakeDragging()) { 647 mViewPager.endFakeDrag(); 648 } 649 } 650 651 @Override 652 public void onScrollChanged(int l, int t, int oldl, int oldt) { 653 // The user is scrolling the carousel, so send the scroll deltas to the 654 // {@link ViewPager} so it can move in sync. 655 if (mViewPager.isFakeDragging()) { 656 mViewPager.fakeDragBy(oldl-l); 657 } 658 } 659 660 @Override 661 public void onTabSelected(int position) { 662 // The user selected a tab, so update the {@link ViewPager} 663 mViewPager.setCurrentItem(position); 664 } 665 }; 666 667 private final class VerticalScrollListener implements OnScrollListener { 668 669 private final int mPageIndex; 670 671 public VerticalScrollListener(int pageIndex) { 672 mPageIndex = pageIndex; 673 } 674 675 @Override 676 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 677 int totalItemCount) { 678 int currentPageIndex = mViewPager.getCurrentItem(); 679 // Don't move the carousel if: 1) the contact does not have social updates because then 680 // tab carousel must not be visible, 2) if the view pager is still being scrolled, 681 // 3) if the current page being viewed is not this one, or 4) if the tab carousel 682 // is already being animated vertically. 683 if (!mContactHasUpdates || mViewPagerState != ViewPager.SCROLL_STATE_IDLE || 684 mPageIndex != currentPageIndex || mTabCarouselIsAnimating) { 685 return; 686 } 687 // If the FIRST item is not visible on the screen, then the carousel must be pinned 688 // at the top of the screen. 689 if (firstVisibleItem != 0) { 690 mTabCarousel.moveToYCoordinate(mPageIndex, 691 -mTabCarousel.getAllowedVerticalScrollLength()); 692 return; 693 } 694 View topView = view.getChildAt(firstVisibleItem); 695 if (topView == null) { 696 return; 697 } 698 int amtToScroll = Math.max((int) view.getChildAt(firstVisibleItem).getY(), 699 -mTabCarousel.getAllowedVerticalScrollLength()); 700 mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll); 701 } 702 703 @Override 704 public void onScrollStateChanged(AbsListView view, int scrollState) { 705 // Once the list has become IDLE, check if we need to sync the scroll position of 706 // the other list now. This will make swiping faster by doing the re-layout now 707 // (instead of at the start of a swipe). However, there will still be another check 708 // when we start swiping if the scroll positions are correct (to catch the edge case 709 // where the user flings and immediately starts a swipe so we never get the idle state). 710 if (scrollState == SCROLL_STATE_IDLE) { 711 syncScrollStateBetweenLists(mPageIndex); 712 } 713 } 714 } 715} 716