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.common.model.Contact; 41 import com.android.contacts.util.PhoneCapabilityTester; 42 import com.android.contacts.common.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 289 if (PhoneCapabilityTester.isUsingTwoPanes(mActivity)) { 290 // Tablet: If we already showed data before, we want to cross-fade from screen to screen 291 if (contactWasLoaded && mTransitionAnimationView != null && isDifferentContact) { 292 mTransitionAnimationView.startMaskTransition(mContactData == null, -1); 293 } 294 } else { 295 // Small screen: We are on our own screen. Fade the data in, but only the first time 296 if (!contactWasLoaded) { 297 mViewContainer.setAlpha(0.0f); 298 final ViewPropertyAnimator animator = mViewContainer.animate(); 299 animator.alpha(1.0f); 300 animator.setDuration(SINGLE_PANE_FADE_IN_DURATION); 301 } 302 } 303 304 showContactWithoutUpdates(); 305 } 306 307 public void showEmptyState() { 308 switch (mLayoutMode) { 309 case LayoutMode.FRAGMENT_CAROUSEL: { 310 mFragmentCarousel.setCurrentPage(0); 311 mFragmentCarousel.enableSwipe(false); 312 mDetailFragment.showEmptyState(); 313 break; 314 } 315 case LayoutMode.TWO_COLUMN: { 316 mDetailFragment.setShowStaticPhoto(false); 317 mUpdatesFragmentView.setVisibility(View.GONE); 318 mDetailFragment.showEmptyState(); 319 break; 320 } 321 case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: { 322 mFragmentCarousel.setCurrentPage(0); 323 mFragmentCarousel.enableSwipe(false); 324 mDetailFragment.setShowStaticPhoto(false); 325 mUpdatesFragmentView.setVisibility(View.GONE); 326 mDetailFragment.showEmptyState(); 327 break; 328 } 329 case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: { 330 mDetailFragment.setShowStaticPhoto(false); 331 mDetailFragment.showEmptyState(); 332 mTabCarousel.loadData(null); 333 mTabCarousel.setVisibility(View.GONE); 334 mViewPagerAdapter.enableSwipe(false); 335 mViewPager.setCurrentItem(0); 336 break; 337 } 338 default: 339 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 340 } 341 } 342 343 /** 344 * Setup the layout for the contact with updates. 345 * TODO: Clean up this method so it's easier to understand. 346 */ 347 private void showContactWithUpdates(boolean animateStateChange) { 348 if (mContactData == null) { 349 return; 350 } 351 352 Uri previousContactUri = mContactUri; 353 mContactUri = mContactData.getLookupUri(); 354 boolean isDifferentContact = !UriUtils.areEqual(previousContactUri, mContactUri); 355 356 switch (mLayoutMode) { 357 case LayoutMode.TWO_COLUMN: { 358 if (!isDifferentContact && animateStateChange) { 359 // This is screen is very hard to animate properly, because there is such a hard 360 // cut from the regular version. A proper animation would have to reflow text 361 // and move things around. Doing a simple cross-fade instead. 362 mTransitionAnimationView.startMaskTransition(false, -1); 363 } 364 365 // Set the contact data (hide the static photo because the photo will already be in 366 // the header that scrolls with contact details). 367 mDetailFragment.setShowStaticPhoto(false); 368 // Show the updates fragment 369 mUpdatesFragmentView.setVisibility(View.VISIBLE); 370 break; 371 } 372 case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: { 373 // Update and show the tab carousel (also restore its last saved position) 374 mTabCarousel.loadData(mContactData); 375 mTabCarousel.restoreYCoordinate(); 376 mTabCarousel.setVisibility(View.VISIBLE); 377 // Update ViewPager to allow swipe between all the fragments (to see updates) 378 mViewPagerAdapter.enableSwipe(true); 379 // If this is a different contact than before, then reset some views. 380 if (isDifferentContact) { 381 resetViewPager(); 382 resetTabCarousel(); 383 } 384 if (!isDifferentContact && animateStateChange) { 385 mTabCarousel.animateAppear(mViewContainer.getWidth(), 386 mDetailFragment.getFirstListItemOffset()); 387 } 388 break; 389 } 390 case LayoutMode.FRAGMENT_CAROUSEL: { 391 // Allow swiping between all fragments 392 mFragmentCarousel.enableSwipe(true); 393 if (!isDifferentContact && animateStateChange) { 394 mFragmentCarousel.animateAppear(); 395 } 396 break; 397 } 398 case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: { 399 // Allow swiping between all fragments 400 mFragmentCarousel.enableSwipe(true); 401 if (isDifferentContact) { 402 mFragmentCarousel.reset(); 403 } 404 if (!isDifferentContact && animateStateChange) { 405 mFragmentCarousel.animateAppear(); 406 } 407 mDetailFragment.setShowStaticPhoto(false); 408 break; 409 } 410 default: 411 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 412 } 413 414 if (isDifferentContact) { 415 resetFragments(); 416 } 417 418 mDetailFragment.setData(mContactUri, mContactData); 419 mUpdatesFragment.setData(mContactUri, mContactData); 420 } 421 422 /** 423 * Setup the layout for the contact without updates. 424 * TODO: Clean up this method so it's easier to understand. 425 */ 426 private void showContactWithoutUpdates() { 427 if (mContactData == null) { 428 return; 429 } 430 431 Uri previousContactUri = mContactUri; 432 mContactUri = mContactData.getLookupUri(); 433 boolean isDifferentContact = !UriUtils.areEqual(previousContactUri, mContactUri); 434 435 switch (mLayoutMode) { 436 case LayoutMode.TWO_COLUMN: 437 // Show the static photo which is next to the list of scrolling contact details 438 mDetailFragment.setShowStaticPhoto(true); 439 // Hide the updates fragment 440 mUpdatesFragmentView.setVisibility(View.GONE); 441 break; 442 case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: 443 // Hide the tab carousel 444 mTabCarousel.setVisibility(View.GONE); 445 // Update ViewPager to disable swipe so that it only shows the detail fragment 446 // and switch to the detail fragment 447 mViewPagerAdapter.enableSwipe(false); 448 mViewPager.setCurrentItem(0, false /* smooth transition */); 449 break; 450 case LayoutMode.FRAGMENT_CAROUSEL: 451 // Disable swipe so only the detail fragment shows 452 mFragmentCarousel.setCurrentPage(0); 453 mFragmentCarousel.enableSwipe(false); 454 break; 455 case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: 456 mFragmentCarousel.setCurrentPage(0); 457 mFragmentCarousel.enableSwipe(false); 458 mDetailFragment.setShowStaticPhoto(true); 459 break; 460 default: 461 throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); 462 } 463 464 if (isDifferentContact) { 465 resetFragments(); 466 } 467 468 mDetailFragment.setData(mContactUri, mContactData); 469 } 470 471 private void resetTabCarousel() { 472 mTabCarousel.reset(); 473 } 474 475 private void resetViewPager() { 476 mViewPager.setCurrentItem(0, false /* smooth transition */); 477 } 478 479 private void resetFragments() { 480 mDetailFragment.resetAdapter(); 481 mUpdatesFragment.resetAdapter(); 482 } 483 484 public FragmentKeyListener getCurrentPage() { 485 switch (getCurrentPageIndex()) { 486 case 0: 487 return mDetailFragment; 488 case 1: 489 return mUpdatesFragment; 490 default: 491 throw new IllegalStateException("Invalid current item for ViewPager"); 492 } 493 } 494 495 private int getCurrentPageIndex() { 496 // If the contact has social updates, then retrieve the current page based on the 497 // {@link ViewPager} or fragment carousel. 498 if (mContactHasUpdates) { 499 if (mViewPager != null) { 500 return mViewPager.getCurrentItem(); 501 } else if (mFragmentCarousel != null) { 502 return mFragmentCarousel.getCurrentPage(); 503 } 504 } 505 // Otherwise return the default page (detail fragment). 506 return 0; 507 } 508 509 public void onSaveInstanceState(Bundle outState) { 510 outState.putParcelable(KEY_CONTACT_URI, mContactUri); 511 outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates); 512 outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPageIndex()); 513 } 514 515 private final OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { 516 517 private ObjectAnimator mTabCarouselAnimator; 518 519 @Override 520 public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { 521 // The user is horizontally dragging the {@link ViewPager}, so send 522 // these scroll changes to the tab carousel. Ignore these events though if the carousel 523 // is actually controlling the {@link ViewPager} scrolls because it will already be 524 // in the correct position. 525 if (mViewPager.isFakeDragging()) return; 526 527 int x = (int) ((position + positionOffset) * 528 mTabCarousel.getAllowedHorizontalScrollLength()); 529 mTabCarousel.scrollTo(x, 0); 530 } 531 532 @Override 533 public void onPageSelected(int position) { 534 // Since the {@link ViewPager} has committed to a new page now (but may not have 535 // finished scrolling yet), update the tab selection in the carousel. 536 mTabCarousel.setCurrentTab(position); 537 } 538 539 @Override 540 public void onPageScrollStateChanged(int state) { 541 if (mViewPagerState == ViewPager.SCROLL_STATE_IDLE) { 542 543 // If we are leaving the IDLE state, we are starting a swipe. 544 // First cancel any pending animations on the tab carousel. 545 cancelTabCarouselAnimator(); 546 547 // Sync the two lists because the list on the other page will start to show as 548 // we swipe over more. 549 syncScrollStateBetweenLists(mViewPager.getCurrentItem()); 550 551 } else if (state == ViewPager.SCROLL_STATE_IDLE) { 552 553 // Otherwise if the {@link ViewPager} is idle now, a page has been selected and 554 // scrolled into place. Perform an animation of the tab carousel is needed. 555 int currentPageIndex = mViewPager.getCurrentItem(); 556 int tabCarouselOffset = (int) mTabCarousel.getY(); 557 boolean shouldAnimateTabCarousel; 558 559 // Find the offset position of the first item in the list of the current page. 560 int listOffset = getOffsetOfFirstItemInList(currentPageIndex); 561 562 // If the list was able to successfully offset by the tab carousel amount, then 563 // log this as the new Y coordinate for that page, and no animation is needed. 564 if (listOffset == tabCarouselOffset) { 565 mTabCarousel.storeYCoordinate(currentPageIndex, tabCarouselOffset); 566 shouldAnimateTabCarousel = false; 567 } else if (listOffset == Integer.MIN_VALUE) { 568 // If the offset of the first item in the list is unknown (i.e. the item 569 // is no longer visible on screen) then just animate the tab carousel to the 570 // previously logged position. 571 shouldAnimateTabCarousel = true; 572 } else if (Math.abs(listOffset) < Math.abs(tabCarouselOffset)) { 573 // If the list could not offset the full amount of the tab carousel offset (i.e. 574 // the list can only be scrolled a tiny amount), then animate the carousel down 575 // to compensate. 576 mTabCarousel.storeYCoordinate(currentPageIndex, listOffset); 577 shouldAnimateTabCarousel = true; 578 } else { 579 // By default, animate back to the Y coordinate of the tab carousel the last 580 // time the other page was selected. 581 shouldAnimateTabCarousel = true; 582 } 583 584 if (shouldAnimateTabCarousel) { 585 float desiredOffset = mTabCarousel.getStoredYCoordinateForTab(currentPageIndex); 586 if (desiredOffset != tabCarouselOffset) { 587 createTabCarouselAnimator(desiredOffset); 588 mTabCarouselAnimator.start(); 589 } 590 } 591 } 592 mViewPagerState = state; 593 } 594 595 private void createTabCarouselAnimator(float desiredValue) { 596 mTabCarouselAnimator = ObjectAnimator.ofFloat( 597 mTabCarousel, "y", desiredValue).setDuration(75); 598 mTabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator( 599 mActivity, android.R.anim.accelerate_decelerate_interpolator)); 600 mTabCarouselAnimator.addListener(mTabCarouselAnimatorListener); 601 } 602 603 private void cancelTabCarouselAnimator() { 604 if (mTabCarouselAnimator != null) { 605 mTabCarouselAnimator.cancel(); 606 mTabCarouselAnimator = null; 607 mTabCarouselIsAnimating = false; 608 } 609 } 610 }; 611 612 private void syncScrollStateBetweenLists(int currentPageIndex) { 613 // Since the user interacted with the currently visible page, we need to sync the 614 // list on the other page (i.e. if the updates page is the current page, modify the 615 // list in the details page). 616 if (currentPageIndex == TAB_INDEX_UPDATES) { 617 mDetailFragment.requestToMoveToOffset((int) mTabCarousel.getY()); 618 } else { 619 mUpdatesFragment.requestToMoveToOffset((int) mTabCarousel.getY()); 620 } 621 } 622 623 private int getOffsetOfFirstItemInList(int currentPageIndex) { 624 if (currentPageIndex == TAB_INDEX_DETAIL) { 625 return mDetailFragment.getFirstListItemOffset(); 626 } else { 627 return mUpdatesFragment.getFirstListItemOffset(); 628 } 629 } 630 631 /** 632 * This listener keeps track of whether the tab carousel animation is currently going on or not, 633 * in order to prevent other simultaneous changes to the Y position of the tab carousel which 634 * can cause flicker. 635 */ 636 private final AnimatorListener mTabCarouselAnimatorListener = new AnimatorListener() { 637 638 @Override 639 public void onAnimationCancel(Animator animation) { 640 mTabCarouselIsAnimating = false; 641 } 642 643 @Override 644 public void onAnimationEnd(Animator animation) { 645 mTabCarouselIsAnimating = false; 646 } 647 648 @Override 649 public void onAnimationRepeat(Animator animation) { 650 mTabCarouselIsAnimating = true; 651 } 652 653 @Override 654 public void onAnimationStart(Animator animation) { 655 mTabCarouselIsAnimating = true; 656 } 657 }; 658 659 private final ContactDetailTabCarousel.Listener mTabCarouselListener 660 = new ContactDetailTabCarousel.Listener() { 661 662 @Override 663 public void onTouchDown() { 664 // The user just started scrolling the carousel, so begin 665 // "fake dragging" the {@link ViewPager} if it's not already 666 // doing so. 667 if (!mViewPager.isFakeDragging()) mViewPager.beginFakeDrag(); 668 } 669 670 @Override 671 public void onTouchUp() { 672 // The user just stopped scrolling the carousel, so stop 673 // "fake dragging" the {@link ViewPager} if it was doing so 674 // before. 675 if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); 676 } 677 678 @Override 679 public void onScrollChanged(int l, int t, int oldl, int oldt) { 680 // The user is scrolling the carousel, so send the scroll 681 // deltas to the {@link ViewPager} so it can move in sync. 682 if (mViewPager.isFakeDragging()) { 683 mViewPager.fakeDragBy(oldl - l); 684 } 685 } 686 687 @Override 688 public void onTabSelected(int position) { 689 // The user selected a tab, so update the {@link ViewPager} 690 mViewPager.setCurrentItem(position); 691 } 692 }; 693 694 private final class VerticalScrollListener implements OnScrollListener { 695 696 private final int mPageIndex; 697 698 public VerticalScrollListener(int pageIndex) { 699 mPageIndex = pageIndex; 700 } 701 702 @Override 703 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 704 int totalItemCount) { 705 int currentPageIndex = mViewPager.getCurrentItem(); 706 // Don't move the carousel if: 1) the contact does not have social updates because then 707 // tab carousel must not be visible, 2) if the view pager is still being scrolled, 708 // 3) if the current page being viewed is not this one, or 4) if the tab carousel 709 // is already being animated vertically. 710 if (!mContactHasUpdates || mViewPagerState != ViewPager.SCROLL_STATE_IDLE || 711 mPageIndex != currentPageIndex || mTabCarouselIsAnimating) { 712 return; 713 } 714 // If the FIRST item is not visible on the screen, then the carousel must be pinned 715 // at the top of the screen. 716 if (firstVisibleItem != 0) { 717 mTabCarousel.moveToYCoordinate(mPageIndex, 718 -mTabCarousel.getAllowedVerticalScrollLength()); 719 return; 720 } 721 View topView = view.getChildAt(firstVisibleItem); 722 if (topView == null) { 723 return; 724 } 725 int amtToScroll = Math.max((int) view.getChildAt(firstVisibleItem).getY(), 726 -mTabCarousel.getAllowedVerticalScrollLength()); 727 mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll); 728 } 729 730 @Override 731 public void onScrollStateChanged(AbsListView view, int scrollState) { 732 // Once the list has become IDLE, check if we need to sync the scroll position of 733 // the other list now. This will make swiping faster by doing the re-layout now 734 // (instead of at the start of a swipe). However, there will still be another check 735 // when we start swiping if the scroll positions are correct (to catch the edge case 736 // where the user flings and immediately starts a swipe so we never get the idle state). 737 if (scrollState == SCROLL_STATE_IDLE) { 738 syncScrollStateBetweenLists(mPageIndex); 739 } 740 } 741 } 742 } 743