/* Copyright © 2013-2014, Silent Circle, LLC. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Any redistribution, use, or modification is done solely for personal benefit and not for any commercial purpose or for monetary gain * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Silent Circle nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL SILENT CIRCLE, LLC BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This implementation is edited version of original Android sources. */ /* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.silentcircle.contacts.detail; import android.animation.Animator; import android.animation.ObjectAnimator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager.OnPageChangeListener; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewPropertyAnimator; import android.view.animation.AnimationUtils; import android.widget.AbsListView; import android.widget.AbsListView.OnScrollListener; import com.silentcircle.contacts.NfcHandler; import com.silentcircle.contacts.activities.ScContactDetailActivity; import com.silentcircle.contacts.model.Contact; import com.silentcircle.contacts.widget.TransitionAnimationView; import com.silentcircle.contacts.R; import com.silentcircle.contacts.utils.PhoneCapabilityTester; import com.silentcircle.contacts.utils.UriUtils; import com.silentcircle.contacts.widget.FrameLayoutWithOverlay; /** * Determines the layout of the contact card. */ public class ContactDetailLayoutController { private static final String TAG = "ContactDetailLayoutController"; private static final String KEY_CONTACT_URI = "contactUri"; private static final String KEY_CONTACT_HAS_UPDATES = "contactHasUpdates"; private static final String KEY_CURRENT_PAGE_INDEX = "currentPageIndex"; private static final int TAB_INDEX_DETAIL = 0; private static final int TAB_INDEX_UPDATES = 1; private final int SINGLE_PANE_FADE_IN_DURATION = 275; /** * There are 4 possible layouts for the contact detail screen: TWO_COLUMN, * VIEW_PAGER_AND_TAB_CAROUSEL, FRAGMENT_CAROUSEL, and TWO_COLUMN_FRAGMENT_CAROUSEL. */ private interface LayoutMode { /** * Tall and wide screen with details and updates shown side-by-side. */ static final int TWO_COLUMN = 0; /** * Tall and narrow screen to allow swipe between the details and updates. */ static final int VIEW_PAGER_AND_TAB_CAROUSEL = 1; /** * Short and wide screen to allow part of the other page to show. */ static final int FRAGMENT_CAROUSEL = 2; /** * Same as FRAGMENT_CAROUSEL (allowing part of the other page to show) except the details * layout is similar to the details layout in TWO_COLUMN mode. */ static final int TWO_COLUMN_FRAGMENT_CAROUSEL = 3; } private final Activity mActivity; private final LayoutInflater mLayoutInflater; private final FragmentManager mFragmentManager; private final View mViewContainer; private final TransitionAnimationView mTransitionAnimationView; private ContactDetailFragment mDetailFragment; private ContactDetailUpdatesFragment mUpdatesFragment; private View mDetailFragmentView; private View mUpdatesFragmentView; private final ViewPager mViewPager; private ContactDetailViewPagerAdapter mViewPagerAdapter; private int mViewPagerState; private final ContactDetailTabCarousel mTabCarousel; private final ContactDetailFragmentCarousel mFragmentCarousel; private final ContactDetailFragment.Listener mContactDetailFragmentListener; private Contact mContactData; private Uri mContactUri; private boolean mTabCarouselIsAnimating; private boolean mContactHasUpdates; private int mLayoutMode; public ContactDetailLayoutController(Activity activity, Bundle savedState, FragmentManager fragmentManager, TransitionAnimationView animationView, View viewContainer, ContactDetailFragment.Listener contactDetailFragmentListener) { if (fragmentManager == null) { throw new IllegalStateException("Cannot initialize a ContactDetailLayoutController " + "without a non-null FragmentManager"); } mActivity = activity; mLayoutInflater = (LayoutInflater) activity.getSystemService(Context.LAYOUT_INFLATER_SERVICE); mFragmentManager = fragmentManager; mContactDetailFragmentListener = contactDetailFragmentListener; mTransitionAnimationView = animationView; // Retrieve views in case this is view pager and carousel mode mViewContainer = viewContainer; mViewPager = (ViewPager) viewContainer.findViewById(R.id.pager); mTabCarousel = (ContactDetailTabCarousel) viewContainer.findViewById(R.id.tab_carousel); // Retrieve view in case this is in fragment carousel mode mFragmentCarousel = (ContactDetailFragmentCarousel) viewContainer.findViewById(R.id.fragment_carousel); // Retrieve container views in case they are already in the XML layout mDetailFragmentView = viewContainer.findViewById(R.id.about_fragment_container); mUpdatesFragmentView = viewContainer.findViewById(R.id.updates_fragment_container); // Determine the layout mode based on the presence of certain views in the layout XML. if (mViewPager != null) { mLayoutMode = LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL; } else if (mFragmentCarousel != null) { if (PhoneCapabilityTester.isUsingTwoPanes(mActivity)) { mLayoutMode = LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL; } else { mLayoutMode = LayoutMode.FRAGMENT_CAROUSEL; } } else { mLayoutMode = LayoutMode.TWO_COLUMN; } initialize(savedState); } private void initialize(Bundle savedState) { boolean fragmentsAddedToFragmentManager = true; mDetailFragment = (ContactDetailFragment) mFragmentManager.findFragmentByTag( ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); mUpdatesFragment = (ContactDetailUpdatesFragment) mFragmentManager.findFragmentByTag( ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); // If the detail fragment was found in the {@link FragmentManager} then we don't need to add // it again. Otherwise, create the fragments dynamically and remember to add them to the // {@link FragmentManager}. if (mDetailFragment == null) { mDetailFragment = new ContactDetailFragment(); mUpdatesFragment = new ContactDetailUpdatesFragment(); fragmentsAddedToFragmentManager = false; } mDetailFragment.setListener(mContactDetailFragmentListener); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) NfcHandler.register(mActivity, mDetailFragment); // Read from savedState if possible int currentPageIndex = 0; if (savedState != null) { mContactUri = savedState.getParcelable(KEY_CONTACT_URI); mContactHasUpdates = savedState.getBoolean(KEY_CONTACT_HAS_UPDATES); currentPageIndex = savedState.getInt(KEY_CURRENT_PAGE_INDEX, 0); } switch (mLayoutMode) { case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: { // Inflate 2 view containers to pass in as children to the {@link ViewPager}, // which will in turn be the parents to the mDetailFragment and mUpdatesFragment // since the fragments must have the same parent view IDs in both landscape and // portrait layouts. mDetailFragmentView = mLayoutInflater.inflate( R.layout.contact_detail_about_fragment_container, mViewPager, false); mUpdatesFragmentView = mLayoutInflater.inflate( R.layout.contact_detail_updates_fragment_container, mViewPager, false); mViewPagerAdapter = new ContactDetailViewPagerAdapter(); mViewPagerAdapter.setAboutFragmentView(mDetailFragmentView); mViewPagerAdapter.setUpdatesFragmentView(mUpdatesFragmentView); mViewPager.addView(mDetailFragmentView); mViewPager.addView(mUpdatesFragmentView); mViewPager.setAdapter(mViewPagerAdapter); mViewPager.setOnPageChangeListener(mOnPageChangeListener); if (!fragmentsAddedToFragmentManager) { FragmentTransaction transaction = mFragmentManager.beginTransaction(); transaction.add(R.id.about_fragment_container, mDetailFragment, ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); transaction.add(R.id.updates_fragment_container, mUpdatesFragment, ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); transaction.commitAllowingStateLoss(); mFragmentManager.executePendingTransactions(); } mTabCarousel.setListener(mTabCarouselListener); mTabCarousel.restoreCurrentTab(currentPageIndex); mDetailFragment.setVerticalScrollListener(new VerticalScrollListener(TAB_INDEX_DETAIL)); mUpdatesFragment.setVerticalScrollListener(new VerticalScrollListener(TAB_INDEX_UPDATES)); mViewPager.setCurrentItem(currentPageIndex); break; } case LayoutMode.TWO_COLUMN: { if (!fragmentsAddedToFragmentManager) { FragmentTransaction transaction = mFragmentManager.beginTransaction(); transaction.add(R.id.about_fragment_container, mDetailFragment, ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); transaction.add(R.id.updates_fragment_container, mUpdatesFragment, ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); transaction.commitAllowingStateLoss(); mFragmentManager.executePendingTransactions(); } break; } case LayoutMode.FRAGMENT_CAROUSEL: case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: { // Add the fragments to the fragment containers in the carousel using a // {@link FragmentTransaction} if they haven't already been added to the // {@link FragmentManager}. if (!fragmentsAddedToFragmentManager) { FragmentTransaction transaction = mFragmentManager.beginTransaction(); transaction.add(R.id.about_fragment_container, mDetailFragment, ContactDetailViewPagerAdapter.ABOUT_FRAGMENT_TAG); transaction.add(R.id.updates_fragment_container, mUpdatesFragment, ContactDetailViewPagerAdapter.UPDATES_FRAGMENT_TAG); transaction.commitAllowingStateLoss(); mFragmentManager.executePendingTransactions(); } mFragmentCarousel.setFragmentViews((FrameLayoutWithOverlay) mDetailFragmentView, (FrameLayoutWithOverlay) mUpdatesFragmentView); mFragmentCarousel.setCurrentPage(currentPageIndex); break; } } // Setup the layout if we already have a saved state if (savedState != null) { if (mContactHasUpdates) { showContactWithUpdates(false); } else { showContactWithoutUpdates(); } } } @TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1) public void setContactData(Contact data) { final boolean contactWasLoaded; final boolean contactHadUpdates; final boolean isDifferentContact; if (mContactData == null) { contactHadUpdates = false; contactWasLoaded = false; isDifferentContact = true; } else { contactHadUpdates = mContactHasUpdates; contactWasLoaded = true; isDifferentContact = !UriUtils.areEqual(mContactData.getLookupUri(), data.getLookupUri()); } mContactData = data; mContactHasUpdates = !data.getStreamItems().isEmpty(); if (PhoneCapabilityTester.isUsingTwoPanes(mActivity)) { // Tablet: If we already showed data before, we want to cross-fade from screen to screen if (contactWasLoaded && mTransitionAnimationView != null && isDifferentContact) { mTransitionAnimationView.startMaskTransition(mContactData == null); } } else { // Small screen: We are on our own screen. Fade the data in, but only the first time if (!contactWasLoaded) { mViewContainer.setVisibility(View.VISIBLE); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) { mViewContainer.setAlpha(0.0f); final ViewPropertyAnimator animator = mViewContainer.animate(); animator.alpha(1.0f); animator.setDuration(SINGLE_PANE_FADE_IN_DURATION); } } } if (mContactHasUpdates) { showContactWithUpdates(contactWasLoaded && !contactHadUpdates); } else { showContactWithoutUpdates(); } } public void showEmptyState() { switch (mLayoutMode) { case LayoutMode.FRAGMENT_CAROUSEL: { mFragmentCarousel.setCurrentPage(0); mFragmentCarousel.enableSwipe(false); mDetailFragment.showEmptyState(); break; } case LayoutMode.TWO_COLUMN: { mDetailFragment.setShowStaticPhoto(false); mUpdatesFragmentView.setVisibility(View.GONE); mDetailFragment.showEmptyState(); break; } case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: { mFragmentCarousel.setCurrentPage(0); mFragmentCarousel.enableSwipe(false); mDetailFragment.setShowStaticPhoto(false); mUpdatesFragmentView.setVisibility(View.GONE); mDetailFragment.showEmptyState(); break; } case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: { mDetailFragment.setShowStaticPhoto(false); mDetailFragment.showEmptyState(); mTabCarousel.loadData(null); mTabCarousel.setVisibility(View.GONE); mViewPagerAdapter.enableSwipe(false); mViewPager.setCurrentItem(0); break; } default: throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); } } /** * Setup the layout for the contact with updates. * TODO: Clean up this method so it's easier to understand. */ private void showContactWithUpdates(boolean animateStateChange) { if (mContactData == null) { return; } Uri previousContactUri = mContactUri; mContactUri = mContactData.getLookupUri(); boolean isDifferentContact = !UriUtils.areEqual(previousContactUri, mContactUri); switch (mLayoutMode) { case LayoutMode.TWO_COLUMN: { if (!isDifferentContact && animateStateChange) { // This is screen is very hard to animate properly, because there is such a hard // cut from the regular version. A proper animation would have to reflow text // and move things around. Doing a simple cross-fade instead. mTransitionAnimationView.startMaskTransition(false); } // Set the contact data (hide the static photo because the photo will already be in // the header that scrolls with contact details). mDetailFragment.setShowStaticPhoto(false); // Show the updates fragment mUpdatesFragmentView.setVisibility(View.VISIBLE); break; } case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: { // Update and show the tab carousel (also restore its last saved position) mTabCarousel.loadData(mContactData); mTabCarousel.restoreYCoordinate(); mTabCarousel.setVisibility(View.VISIBLE); // Update ViewPager to allow swipe between all the fragments (to see updates) mViewPagerAdapter.enableSwipe(true); // If this is a different contact than before, then reset some views. if (isDifferentContact) { resetViewPager(); resetTabCarousel(); } if (!isDifferentContact && animateStateChange) { mTabCarousel.animateAppear(mViewContainer.getWidth(), mDetailFragment.getFirstListItemOffset()); } break; } case LayoutMode.FRAGMENT_CAROUSEL: { // Allow swiping between all fragments mFragmentCarousel.enableSwipe(true); if (!isDifferentContact && animateStateChange) { mFragmentCarousel.animateAppear(); } break; } case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: { // Allow swiping between all fragments mFragmentCarousel.enableSwipe(true); if (isDifferentContact) { mFragmentCarousel.reset(); } if (!isDifferentContact && animateStateChange) { mFragmentCarousel.animateAppear(); } mDetailFragment.setShowStaticPhoto(false); break; } default: throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); } if (isDifferentContact) { resetFragments(); } mDetailFragment.setData(mContactUri, mContactData); mUpdatesFragment.setData(mContactUri, mContactData); } /** * Setup the layout for the contact without updates. * TODO: Clean up this method so it's easier to understand. */ private void showContactWithoutUpdates() { if (mContactData == null) { return; } Uri previousContactUri = mContactUri; mContactUri = mContactData.getLookupUri(); boolean isDifferentContact = !UriUtils.areEqual(previousContactUri, mContactUri); switch (mLayoutMode) { case LayoutMode.TWO_COLUMN: // Show the static photo which is next to the list of scrolling contact details mDetailFragment.setShowStaticPhoto(true); // Hide the updates fragment mUpdatesFragmentView.setVisibility(View.GONE); break; case LayoutMode.VIEW_PAGER_AND_TAB_CAROUSEL: // Hide the tab carousel mTabCarousel.setVisibility(View.GONE); // Update ViewPager to disable swipe so that it only shows the detail fragment // and switch to the detail fragment mViewPagerAdapter.enableSwipe(false); mViewPager.setCurrentItem(0, false /* smooth transition */); break; case LayoutMode.FRAGMENT_CAROUSEL: // Disable swipe so only the detail fragment shows mFragmentCarousel.setCurrentPage(0); mFragmentCarousel.enableSwipe(false); break; case LayoutMode.TWO_COLUMN_FRAGMENT_CAROUSEL: mFragmentCarousel.setCurrentPage(0); mFragmentCarousel.enableSwipe(false); mDetailFragment.setShowStaticPhoto(true); break; default: throw new IllegalStateException("Invalid LayoutMode " + mLayoutMode); } if (isDifferentContact) { resetFragments(); } mDetailFragment.setData(mContactUri, mContactData); } private void resetTabCarousel() { mTabCarousel.reset(); } private void resetViewPager() { mViewPager.setCurrentItem(0, false /* smooth transition */); } private void resetFragments() { mDetailFragment.resetAdapter(); mUpdatesFragment.resetAdapter(); } public ScContactDetailActivity.FragmentKeyListener getCurrentPage() { switch (getCurrentPageIndex()) { case 0: return mDetailFragment; case 1: return mUpdatesFragment; default: throw new IllegalStateException("Invalid current item for ViewPager"); } } private int getCurrentPageIndex() { // If the contact has social updates, then retrieve the current page based on the // {@link ViewPager} or fragment carousel. if (mContactHasUpdates) { if (mViewPager != null) { return mViewPager.getCurrentItem(); } else if (mFragmentCarousel != null) { return mFragmentCarousel.getCurrentPage(); } } // Otherwise return the default page (detail fragment). return 0; } public void onSaveInstanceState(Bundle outState) { outState.putParcelable(KEY_CONTACT_URI, mContactUri); outState.putBoolean(KEY_CONTACT_HAS_UPDATES, mContactHasUpdates); outState.putInt(KEY_CURRENT_PAGE_INDEX, getCurrentPageIndex()); } private final OnPageChangeListener mOnPageChangeListener = new OnPageChangeListener() { private ObjectAnimator mTabCarouselAnimator; @Override public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { // The user is horizontally dragging the {@link ViewPager}, so send // these scroll changes to the tab carousel. Ignore these events though if the carousel // is actually controlling the {@link ViewPager} scrolls because it will already be // in the correct position. if (mViewPager.isFakeDragging()) return; int x = (int) ((position + positionOffset) * mTabCarousel.getAllowedHorizontalScrollLength()); mTabCarousel.scrollTo(x, 0); } @Override public void onPageSelected(int position) { // Since the {@link ViewPager} has committed to a new page now (but may not have // finished scrolling yet), update the tab selection in the carousel. mTabCarousel.setCurrentTab(position); } @Override @TargetApi(Build.VERSION_CODES.HONEYCOMB) public void onPageScrollStateChanged(int state) { if (mViewPagerState == ViewPager.SCROLL_STATE_IDLE) { // If we are leaving the IDLE state, we are starting a swipe. // First cancel any pending animations on the tab carousel. cancelTabCarouselAnimator(); // Sync the two lists because the list on the other page will start to show as // we swipe over more. syncScrollStateBetweenLists(mViewPager.getCurrentItem()); } else if (state == ViewPager.SCROLL_STATE_IDLE) { // Otherwise if the {@link ViewPager} is idle now, a page has been selected and // scrolled into place. Perform an animation of the tab carousel is needed. int currentPageIndex = mViewPager.getCurrentItem(); int tabCarouselOffset = (int) mTabCarousel.getY(); boolean shouldAnimateTabCarousel; // Find the offset position of the first item in the list of the current page. int listOffset = getOffsetOfFirstItemInList(currentPageIndex); // If the list was able to successfully offset by the tab carousel amount, then // log this as the new Y coordinate for that page, and no animation is needed. if (listOffset == tabCarouselOffset) { mTabCarousel.storeYCoordinate(currentPageIndex, tabCarouselOffset); shouldAnimateTabCarousel = false; } else if (listOffset == Integer.MIN_VALUE) { // If the offset of the first item in the list is unknown (i.e. the item // is no longer visible on screen) then just animate the tab carousel to the // previously logged position. shouldAnimateTabCarousel = true; } else if (Math.abs(listOffset) < Math.abs(tabCarouselOffset)) { // If the list could not offset the full amount of the tab carousel offset (i.e. // the list can only be scrolled a tiny amount), then animate the carousel down // to compensate. mTabCarousel.storeYCoordinate(currentPageIndex, listOffset); shouldAnimateTabCarousel = true; } else { // By default, animate back to the Y coordinate of the tab carousel the last // time the other page was selected. shouldAnimateTabCarousel = true; } if (shouldAnimateTabCarousel && Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { float desiredOffset = mTabCarousel.getStoredYCoordinateForTab(currentPageIndex); if (desiredOffset != tabCarouselOffset) { createTabCarouselAnimator(desiredOffset); mTabCarouselAnimator.start(); } } } mViewPagerState = state; } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void createTabCarouselAnimator(float desiredValue) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) return; setAnimatorListener(); mTabCarouselAnimator = ObjectAnimator.ofFloat(mTabCarousel, "y", desiredValue).setDuration(75); mTabCarouselAnimator.setInterpolator(AnimationUtils.loadInterpolator( mActivity, android.R.anim.accelerate_decelerate_interpolator)); mTabCarouselAnimator.addListener((Animator.AnimatorListener)mTabCarouselAnimatorListener); } @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void cancelTabCarouselAnimator() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB && mTabCarouselAnimator != null) { mTabCarouselAnimator.cancel(); mTabCarouselAnimator = null; mTabCarouselIsAnimating = false; } } }; private void syncScrollStateBetweenLists(int currentPageIndex) { // Since the user interacted with the currently visible page, we need to sync the // list on the other page (i.e. if the updates page is the current page, modify the // list in the details page). if (currentPageIndex == TAB_INDEX_UPDATES) { mDetailFragment.requestToMoveToOffset((int) mTabCarousel.getY()); } else { mUpdatesFragment.requestToMoveToOffset((int) mTabCarousel.getY()); } } private int getOffsetOfFirstItemInList(int currentPageIndex) { if (currentPageIndex == TAB_INDEX_DETAIL) { return mDetailFragment.getFirstListItemOffset(); } else { return mUpdatesFragment.getFirstListItemOffset(); } } /** * This listener keeps track of whether the tab carousel animation is currently going on or not, * in order to prevent other simultaneous changes to the Y position of the tab carousel which * can cause flicker. */ private Object mTabCarouselAnimatorListener; @TargetApi(Build.VERSION_CODES.HONEYCOMB) private void setAnimatorListener() { if (mTabCarouselAnimatorListener != null || Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) return; mTabCarouselAnimatorListener = new Animator.AnimatorListener() { @Override public void onAnimationCancel(Animator animation) { mTabCarouselIsAnimating = false; } @Override public void onAnimationEnd(Animator animation) { mTabCarouselIsAnimating = false; } @Override public void onAnimationRepeat(Animator animation) { mTabCarouselIsAnimating = true; } @Override public void onAnimationStart(Animator animation) { mTabCarouselIsAnimating = true; } }; } private final ContactDetailTabCarousel.Listener mTabCarouselListener = new ContactDetailTabCarousel.Listener() { @Override public void onTouchDown() { // The user just started scrolling the carousel, so begin // "fake dragging" the {@link ViewPager} if it's not already // doing so. if (!mViewPager.isFakeDragging()) mViewPager.beginFakeDrag(); } @Override public void onTouchUp() { // The user just stopped scrolling the carousel, so stop // "fake dragging" the {@link ViewPager} if it was doing so // before. if (mViewPager.isFakeDragging()) mViewPager.endFakeDrag(); } @Override public void onScrollChanged(int l, int t, int oldl, int oldt) { // The user is scrolling the carousel, so send the scroll // deltas to the {@link ViewPager} so it can move in sync. if (mViewPager.isFakeDragging()) { mViewPager.fakeDragBy(oldl - l); } } @Override public void onTabSelected(int position) { // The user selected a tab, so update the {@link ViewPager} mViewPager.setCurrentItem(position); } }; private final class VerticalScrollListener implements OnScrollListener { private final int mPageIndex; public VerticalScrollListener(int pageIndex) { mPageIndex = pageIndex; } @Override public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { int currentPageIndex = mViewPager.getCurrentItem(); // Don't move the carousel if: 1) the contact does not have social updates because then // tab carousel must not be visible, 2) if the view pager is still being scrolled, // 3) if the current page being viewed is not this one, or 4) if the tab carousel // is already being animated vertically. if (!mContactHasUpdates || mViewPagerState != ViewPager.SCROLL_STATE_IDLE || mPageIndex != currentPageIndex || mTabCarouselIsAnimating) { return; } // If the FIRST item is not visible on the screen, then the carousel must be pinned // at the top of the screen. if (firstVisibleItem != 0) { mTabCarousel.moveToYCoordinate(mPageIndex, -mTabCarousel.getAllowedVerticalScrollLength()); return; } View topView = view.getChildAt(firstVisibleItem); if (topView == null) { return; } int amtToScroll; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) amtToScroll = Math.max((int) topView.getY(), -mTabCarousel.getAllowedVerticalScrollLength()); else amtToScroll = -mTabCarousel.getAllowedVerticalScrollLength(); mTabCarousel.moveToYCoordinate(mPageIndex, amtToScroll); } @Override public void onScrollStateChanged(AbsListView view, int scrollState) { // Once the list has become IDLE, check if we need to sync the scroll position of // the other list now. This will make swiping faster by doing the re-layout now // (instead of at the start of a swipe). However, there will still be another check // when we start swiping if the scroll positions are correct (to catch the edge case // where the user flings and immediately starts a swipe so we never get the idle state). if (scrollState == SCROLL_STATE_IDLE) { syncScrollStateBetweenLists(mPageIndex); } } } }