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