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