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