Home | History | Annotate | Download | only in detail
      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 
     17 package com.android.contacts.detail;
     18 
     19 import com.android.contacts.ContactLoader;
     20 
     21 import com.android.contacts.NfcHandler;
     22 import com.android.contacts.R;
     23 import com.android.contacts.activities.ContactDetailActivity.FragmentKeyListener;
     24 import com.android.contacts.util.PhoneCapabilityTester;
     25 import com.android.contacts.util.UriUtils;
     26 import com.android.contacts.widget.FrameLayoutWithOverlay;
     27 import com.android.contacts.widget.TransitionAnimationView;
     28 
     29 import android.animation.Animator;
     30 import android.animation.Animator.AnimatorListener;
     31 import android.animation.ObjectAnimator;
     32 import android.app.Activity;
     33 import android.app.FragmentManager;
     34 import android.app.FragmentTransaction;
     35 import android.content.Context;
     36 import android.net.Uri;
     37 import android.os.Bundle;
     38 import android.support.v4.view.ViewPager;
     39 import android.support.v4.view.ViewPager.OnPageChangeListener;
     40 import android.view.LayoutInflater;
     41 import android.view.View;
     42 import android.view.ViewPropertyAnimator;
     43 import android.view.animation.AnimationUtils;
     44 import android.widget.AbsListView;
     45 import android.widget.AbsListView.OnScrollListener;
     46 
     47 /**
     48  * Determines the layout of the contact card.
     49  */
     50 public 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