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