Home | History | Annotate | Download | only in detail
      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