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