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