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