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