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