Custom scrolling ViewGroup for QuickContact

Plus basic entrance animation

As our UX person gets to play with this, the MultiShrinkScroller's
snapToTop and snapToBottom() methods will likely be changed a lot.

Change-Id: I75adde129f5e5224e3519733f214fc638c2512ab
diff --git a/res/layout/quickcontact_activity.xml b/res/layout/quickcontact_activity.xml
index fcf348b..573890f 100644
--- a/res/layout/quickcontact_activity.xml
+++ b/res/layout/quickcontact_activity.xml
@@ -14,35 +14,45 @@
      See the License for the specific language governing permissions and
      limitations under the License.
 -->
-<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:ex="http://schemas.android.com/apk/res-auto"
+<com.android.contacts.widget.MultiShrinkScroller
+    xmlns:android="http://schemas.android.com/apk/res/android"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:orientation="vertical"
+    android:id="@+id/multiscroller"
     android:focusable="true"
     android:focusableInTouchMode="true"
-    android:descendantFocusability="afterDescendants"
-    android:background="@color/card_margin_color" >
+    android:descendantFocusability="afterDescendants" >
 
-    <LinearLayout
-        android:id="@android:id/content"
+    <!-- Will contain ToolBar and image behind ToolBar -->
+    <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical" >
+        android:layout_height="@dimen/quickcontact_maximum_header_height"
+        android:layout_marginTop="@dimen/quickcontact_starting_empty_height"
+        android:background="@color/card_margin_color"
+        android:id="@+id/toolbar_parent">
+        <include layout="@layout/quickcontact_photo_container" />
+    </FrameLayout>
 
-        <view
+    <com.android.contacts.widget.TouchlessScrollView
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fillViewport="true"
+        android:id="@+id/content_scroller"
+        android:background="@color/card_margin_color">
+
+        <!-- All the cards should be inserted into this LinearLayout -->
+        <LinearLayout
             android:layout_width="match_parent"
-            android:layout_height="wrap_content"
-            class="com.android.contacts.common.widget.ProportionalLayout"
-            ex:direction="widthToHeight"
-            ex:ratio="0.5" >
+            android:layout_height="match_parent"
+            android:orientation="vertical"
+            android:id="@+id/card_container">
+            <com.android.contacts.quickcontact.ExpandingEntryCardView
+                style="@style/ExpandingEntryCardStyle"
+                android:id="@+id/communication_card"
+                android:layout_marginTop="@dimen/communication_card_marginTop" />
+        </LinearLayout>
 
-            <include layout="@layout/quickcontact_photo_container" />
-        </view>
+    </com.android.contacts.widget.TouchlessScrollView>
 
-        <com.android.contacts.quickcontact.ExpandingEntryCardView
-            style="@style/ExpandingEntryCardStyle"
-            android:id="@+id/communication_card"
-            android:layout_marginTop="@dimen/communication_card_marginTop" />
-    </LinearLayout>
-</ScrollView>
\ No newline at end of file
+</com.android.contacts.widget.MultiShrinkScroller>
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index e72a0ee..d975093 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -17,7 +17,5 @@
     <dimen name="dialpad_digits_height">66dip</dimen>
     <dimen name="dialpad_digits_text_size">28sp</dimen>
     <dimen name="dialpad_digits_margin_bottom">50dip</dimen>
-    <!-- Center vertically -->
-    <dimen name="quick_contact_top_position">-1px</dimen>
     <dimen name="editor_type_label_width">120dip</dimen>
 </resources>
diff --git a/res/values-sw600dp/dimens.xml b/res/values-sw600dp/dimens.xml
index 8661ddc..e99dccf 100644
--- a/res/values-sw600dp/dimens.xml
+++ b/res/values-sw600dp/dimens.xml
@@ -26,8 +26,6 @@
     <dimen name="group_detail_border_padding">16dip</dimen>
     <dimen name="search_view_width">400dip</dimen>
     <dimen name="contact_tile_list_padding_top">16dip</dimen>
-    <!-- Center vertically -->
-    <dimen name="quick_contact_top_position">-1px</dimen>
     <!-- Contact list (vertical scroll bar comes left) -->
     <dimen name="list_visible_scrollbar_padding">32dip</dimen>
     <dimen name="list_header_extra_top_padding">@dimen/contact_browser_list_top_margin</dimen>
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index e60fb84..f20ff91 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -16,9 +16,17 @@
 <resources>
     <dimen name="account_selector_popup_width">400dip</dimen>
 
-    <!-- Top position of quick contact. If this is -1, the vertical position is determined
-    based on the source of the request -->
-    <dimen name="quick_contact_top_position">48dip</dimen>
+    <!-- Initial height of transparent space above QuickContacts -->
+    <dimen name="quickcontact_starting_empty_height">150dp</dimen>
+    <!-- Initial/maximum height of QuickContact's header/avatar-photo -->
+    <dimen name="quickcontact_maximum_header_height">200dp</dimen>
+    <!-- Minimum height of QuickContact's header/avatar-photo -->
+    <dimen name="quickcontact_minimum_header_height">64dp</dimen>
+    <!-- If you scroll the QuickContact by this amount over the top of viewport,
+         the MultiShrinkScroller will smoothScroll the QuickContact to the top of the
+         viewport. This is used to give a sense of elasticity surrounding
+         the top of the viewport. -->
+    <dimen name="quickcontact_elastic_scroll_over_top_region">50dp</dimen>
 
     <!-- Top padding of the entire contact editor  -->
     <dimen name="editor_padding_top">0dip</dimen>
diff --git a/res/values/styles.xml b/res/values/styles.xml
index d282466..3584722 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -41,7 +41,14 @@
     <style name="Theme">
     </style>
 
-    <style name="Theme.QuickContact" parent="@android:style/Theme.Quantum.Light">
+    <style name="Theme.QuickContact" parent="@style/PeopleTheme">
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowFrame">@null</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowAnimationStyle">@null</item>
+        <item name="android:windowIsFloating">false</item>
+        <item name="android:windowIsTranslucent">true</item>
         <item name="android:windowNoTitle">true</item>
         <item name="android:listViewStyle">@style/ListViewStyle</item>
     </style>
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 286026e..11b274b 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -16,21 +16,16 @@
 
 package com.android.contacts.quickcontact;
 
+import android.animation.ObjectAnimator;
 import android.app.Activity;
-import android.app.Fragment;
-import android.app.FragmentManager;
 import android.app.LoaderManager.LoaderCallbacks;
-import android.content.ActivityNotFoundException;
 import android.content.ContentUris;
 import android.content.Context;
 import android.content.Intent;
 import android.content.Loader;
 import android.content.pm.PackageManager;
-import android.graphics.Rect;
-import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.Bundle;
-import android.os.Handler;
 import android.provider.ContactsContract.CommonDataKinds.Email;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
@@ -42,21 +37,14 @@
 import android.provider.ContactsContract.Directory;
 import android.provider.ContactsContract.QuickContact;
 import android.provider.ContactsContract.RawContacts;
-import android.support.v13.app.FragmentPagerAdapter;
-import android.support.v4.view.PagerAdapter;
-import android.support.v4.view.ViewPager;
-import android.support.v4.view.ViewPager.SimpleOnPageChangeListener;
 import android.text.TextUtils;
 import android.util.Log;
-import android.view.MotionEvent;
 import android.view.View;
 import android.view.View.OnClickListener;
-import android.view.ViewGroup;
 import android.view.WindowManager;
-import android.widget.HorizontalScrollView;
+import android.view.animation.AccelerateDecelerateInterpolator;
+import android.view.animation.Interpolator;
 import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
 import android.widget.TextView;
 import android.widget.Toast;
 
@@ -77,8 +65,10 @@
 import com.android.contacts.common.util.UriUtils;
 import com.android.contacts.quickcontact.ExpandingEntryCardView.Entry;
 import com.android.contacts.util.ImageViewDrawableSetter;
-import com.android.contacts.util.SchedulingUtils;
 import com.android.contacts.common.util.StopWatch;
+import com.android.contacts.util.SchedulingUtils;
+import com.android.contacts.widget.MultiShrinkScroller;
+import com.android.contacts.widget.MultiShrinkScroller.MultiShrinkScrollerListener;
 
 import com.google.common.base.Preconditions;
 import com.google.common.collect.Lists;
@@ -89,8 +79,6 @@
 import java.util.List;
 import java.util.Set;
 
-// TODO: Save selected tab index during rotation
-
 /**
  * Mostly translucent {@link Activity} that shows QuickContact dialog. It loads
  * data asynchronously, and then shows a popup with details centered around
@@ -101,7 +89,7 @@
 
     private static final boolean TRACE_LAUNCH = false;
     private static final String TRACE_TAG = "quickcontact";
-    private static final int POST_DRAW_WAIT_DURATION = 60;
+    private static final int ANIMATION_DURATION = 250;
     private static final boolean ENABLE_STOPWATCH = false;
 
 
@@ -118,6 +106,7 @@
     private ImageView mEditOrAddContactImage;
     private ImageView mStarImage;
     private ExpandingEntryCardView mCommunicationCard;
+    private MultiShrinkScroller mScroller;
 
     private Contact mContactData;
     private ContactLoader mContactLoader;
@@ -202,6 +191,14 @@
         }
     };
 
+    final MultiShrinkScrollerListener mMultiShrinkScrollerListener
+            = new MultiShrinkScrollerListener() {
+        @Override
+        public void onScrolledOffBottom() {
+            onBackPressed();
+        }
+    };
+
     @Override
     protected void onCreate(Bundle icicle) {
         mStopWatch.lap("c"); // create start
@@ -245,8 +242,13 @@
         mEditOrAddContactImage = (ImageView) findViewById(R.id.contact_edit_image);
         mStarImage = (ImageView) findViewById(R.id.quickcontact_star_button);
         mCommunicationCard = (ExpandingEntryCardView) findViewById(R.id.communication_card);
+        mScroller = (MultiShrinkScroller) findViewById(R.id.multiscroller);
         mCommunicationCard.setTitle(getResources().getString(R.string.communication_card_title));
 
+        if (mScroller != null) {
+            mScroller.initialize(mMultiShrinkScrollerListener);
+        }
+
         mEditOrAddContactImage.setOnClickListener(mEditContactClickHandler);
         mCommunicationCard.setOnClickListener(mEntryClickHandler);
 
@@ -260,17 +262,21 @@
 
         mStopWatch.lap("v"); // view initialized
 
-        // TODO: Use some sort of fading in for the layout and content during animation
-        /*SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
-            @Override
-            public void run() {
-                mFloatingLayout.fadeInBackground();
-            }
-        });*/
+        if (mScroller != null) {
+            mScroller.setVisibility(View.GONE);
+        }
 
         mStopWatch.lap("cf"); // onCreate finished
     }
 
+    private void runEntranceAnimation() {
+        final int bottomScroll = mScroller.getScrollUntilOffBottom() - 1;
+        final ObjectAnimator scrollAnimation
+                = ObjectAnimator.ofInt(mScroller, "scroll", -bottomScroll, 0);
+        scrollAnimation.setDuration(ANIMATION_DURATION);
+        scrollAnimation.start();
+    }
+
     /** Assign this string to the view if it is not empty. */
     private void setHeaderNameText(int id, int resId) {
         setHeaderNameText(id, getText(resId));
@@ -541,22 +547,20 @@
                 Log.d(Constants.PERFORMANCE_TAG, "QuickContact shown");
             }
 
-            // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
-            // that the layout passes are completed
-            // TODO: Add animation here
-            /*SchedulingUtils.doAfterLayout(mFloatingLayout, new Runnable() {
-                @Override
-                public void run() {
-                    mFloatingLayout.showContent(new Runnable() {
-                        @Override
-                        public void run() {
-                            mContactLoader.upgradeToFullContact();
-                        }
-                    });
-                }
-            });*/
+            if (mScroller != null) {
+                // Data bound and ready, pull curtain to show. Put this on the Handler to ensure
+                // that the layout passes are completed
+                mScroller.setVisibility(View.VISIBLE);
+                SchedulingUtils.doOnPreDraw(mScroller, /* drawNextFrame = */ false,
+                        new Runnable() {
+                    @Override
+                    public void run() {
+                        runEntranceAnimation();
+                    }
+                });
+            }
             mStopWatch.stopAndLog(TAG, 0);
-            mStopWatch = StopWatch.getNullStopWatch(); // We're done with it.
+            mStopWatch = StopWatch.getNullStopWatch();
         }
 
         @Override
@@ -569,4 +573,13 @@
                     false /*postViewNotification*/, true /*computeFormattedPhoneNumber*/);
         }
     };
+    @Override
+    public void onBackPressed() {
+        if (mScroller != null) {
+            // TODO: implement exit animation if the scroller isn't already off the screen
+            finish();
+        } else {
+            super.onBackPressed();
+        }
+    }
 }
diff --git a/src/com/android/contacts/util/SchedulingUtils.java b/src/com/android/contacts/util/SchedulingUtils.java
index 94294fa..fbb2458 100644
--- a/src/com/android/contacts/util/SchedulingUtils.java
+++ b/src/com/android/contacts/util/SchedulingUtils.java
@@ -19,6 +19,7 @@
 import android.view.View;
 import android.view.ViewTreeObserver.OnDrawListener;
 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.ViewTreeObserver.OnPreDrawListener;
 
 /** Static methods that are useful for scheduling actions to occur at a later time. */
 public class SchedulingUtils {
@@ -37,15 +38,17 @@
         view.getViewTreeObserver().addOnGlobalLayoutListener(listener);
     }
 
-    /** Runs a piece of code just before the next draw. */
-    public static void doAfterDraw(final View view, final Runnable runnable) {
-        final OnDrawListener listener = new OnDrawListener() {
+    /** Runs a piece of code just before the next draw, after layout and measurement */
+    public static void doOnPreDraw(final View view, final boolean drawNextFrame,
+            final Runnable runnable) {
+        final OnPreDrawListener listener = new OnPreDrawListener() {
             @Override
-            public void onDraw() {
-                view.getViewTreeObserver().removeOnDrawListener(this);
+            public boolean onPreDraw() {
+                view.getViewTreeObserver().removeOnPreDrawListener(this);
                 runnable.run();
+                return drawNextFrame;
             }
         };
-        view.getViewTreeObserver().addOnDrawListener(listener);
+        view.getViewTreeObserver().addOnPreDrawListener(listener);
     }
 }
diff --git a/src/com/android/contacts/widget/MultiShrinkScroller.java b/src/com/android/contacts/widget/MultiShrinkScroller.java
new file mode 100644
index 0000000..206c721
--- /dev/null
+++ b/src/com/android/contacts/widget/MultiShrinkScroller.java
@@ -0,0 +1,450 @@
+package com.android.contacts.widget;
+
+import com.android.contacts.R;
+import com.android.contacts.test.NeededForReflection;
+
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewConfiguration;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.widget.EdgeEffect;
+import android.widget.LinearLayout;
+import android.widget.Scroller;
+import android.widget.ScrollView;
+
+/**
+ * A custom {@link ViewGroup} that operates similarly to a {@link ScrollView}, except with multiple
+ * subviews. These subviews are scrolled or shrinked one at a time, until each reaches their
+ * minimum or maximum value.
+ *
+ * MultiShrinkScroller is designed for a specific problem. As such, this class is designed to be
+ * used with a specific layout file: quickcontact_activity.xml. MultiShrinkScroller expects subviews
+ * with specific ID values.
+ *
+ * MultiShrinkScroller's code is heavily influenced by ScrollView. Nonetheless, several ScrollView
+ * features are missing. For example: handling of KEYCODES, OverScroll bounce and saving
+ * scroll state in savedInstanceState bundles.
+ */
+public class MultiShrinkScroller extends LinearLayout {
+
+    /**
+     * 1000 pixels per millisecond. Ie, 1 pixel per second.
+     */
+    private static final int PIXELS_PER_SECOND = 1000;
+
+    private float[] mLastEventPosition = { 0, 0 };
+    private VelocityTracker mVelocityTracker;
+    private boolean mIsBeingDragged = false;
+    private boolean mReceivedDown = false;
+
+    private ScrollView mScrollView;
+    private View mScrollViewChild;
+    private View mToolbar;
+    private MultiShrinkScrollerListener mListener;
+
+    private final Scroller mScroller;
+    private final EdgeEffect mEdgeGlowBottom;
+    private final int mTouchSlop;
+    private final int mMaximumVelocity;
+    private final int mMinimumVelocity;
+    private final int mMaximumHeaderHeight;
+    private final int mMinimumHeaderHeight;
+    private final int mTransparentStartHeight;
+    private final int mElasticScrollOverTopRegion;
+
+    public interface MultiShrinkScrollerListener {
+        void onScrolledOffBottom();
+    }
+
+    // Interpolator from android.support.v4.view.ViewPager
+    private static final Interpolator sInterpolator = new Interpolator() {
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public float getInterpolation(float t) {
+            t -= 1.0f;
+            return t * t * t * t * t + 1.0f;
+        }
+    };
+
+    public MultiShrinkScroller(Context context) {
+        this(context, null);
+    }
+
+    public MultiShrinkScroller(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public MultiShrinkScroller(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        final ViewConfiguration configuration = ViewConfiguration.get(context);
+        setFocusable(false);
+        // Drawing must be enabled in order to support EdgeEffect
+        setWillNotDraw(/* willNotDraw = */ false);
+
+        mEdgeGlowBottom = new EdgeEffect(context);
+        mScroller = new Scroller(context, sInterpolator);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mMaximumHeaderHeight = (int) getResources().getDimension(
+                R.dimen.quickcontact_maximum_header_height);
+        mMinimumHeaderHeight = (int) getResources().getDimension(
+                R.dimen.quickcontact_minimum_header_height);
+        mTransparentStartHeight = (int) getResources().getDimension(
+                R.dimen.quickcontact_starting_empty_height);
+        mElasticScrollOverTopRegion = (int) getResources().getDimension(
+                R.dimen.quickcontact_elastic_scroll_over_top_region);
+    }
+
+    /**
+     * This method must be called inside the Activity's OnCreate.
+     */
+    public void initialize(MultiShrinkScrollerListener listener) {
+        mScrollView = (ScrollView) findViewById(R.id.content_scroller);
+        mScrollViewChild = findViewById(R.id.card_container);
+        mToolbar = findViewById(R.id.toolbar_parent);
+        mListener = listener;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        // The only time we want to intercept touch events is when we are being dragged.
+        return shouldStartDrag(event);
+    }
+
+    private boolean shouldStartDrag(MotionEvent event) {
+        if (mIsBeingDragged) {
+            mIsBeingDragged = false;
+            return false;
+        }
+
+        switch (event.getAction()) {
+            // If we are in the middle of a fling and there is a down event, we'll steal it and
+            // start a drag.
+            case MotionEvent.ACTION_DOWN:
+                updateLastEventPosition(event);
+                if (!mScroller.isFinished()) {
+                    startDrag();
+                    return true;
+                } else {
+                    mReceivedDown = true;
+                }
+                break;
+
+            // Otherwise, we will start a drag if there is enough motion in the direction we are
+            // capable of scrolling.
+            case MotionEvent.ACTION_MOVE:
+                if (motionShouldStartDrag(event)) {
+                    updateLastEventPosition(event);
+                    startDrag();
+                    return true;
+                }
+                break;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final int action = event.getAction();
+
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
+
+        if (!mIsBeingDragged) {
+            if (shouldStartDrag(event)) {
+                return true;
+            }
+
+            if (action == MotionEvent.ACTION_UP && mReceivedDown) {
+                mReceivedDown = false;
+                return performClick();
+            }
+            return true;
+        }
+
+        switch (action) {
+            case MotionEvent.ACTION_MOVE:
+                final float delta = updatePositionAndComputeDelta(event);
+                scrollTo(0, getScroll() + (int) delta);
+                mReceivedDown = false;
+
+                if (mIsBeingDragged) {
+                    final int heightScrollViewChild = mScrollViewChild.getHeight();
+                    final int pulledToY = mScrollView.getScrollY() + (int) delta;
+                    if (pulledToY > heightScrollViewChild - mScrollView.getHeight()
+                            && mToolbar.getHeight() == mMinimumHeaderHeight) {
+                        // The ScrollView is being pulled upwards while there is no more
+                        // content offscreen, and the view port is already fully expanded.
+                        mEdgeGlowBottom.onPull(delta / getHeight(), 1 - event.getX() / getWidth());
+                    }
+                    if (!mEdgeGlowBottom.isFinished()) {
+                        postInvalidateOnAnimation();
+                    }
+
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                stopDrag(action == MotionEvent.ACTION_CANCEL);
+                mReceivedDown = false;
+                break;
+        }
+
+        return true;
+    }
+
+    private void startDrag() {
+        mIsBeingDragged = true;
+        mScroller.abortAnimation();
+    }
+
+    private void stopDrag(boolean cancelled) {
+        mIsBeingDragged = false;
+        if (!cancelled && getChildCount() > 0) {
+            final float velocity = getCurrentVelocity();
+            if (velocity > mMinimumVelocity || velocity < -mMinimumVelocity) {
+                fling(-velocity);
+                onDragFinished(mScroller.getFinalY() - mScroller.getStartY());
+            } else {
+                onDragFinished(/* flingDelta = */ 0);
+            }
+        } else {
+            onDragFinished(/* flingDelta = */ 0);
+        }
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+
+        mEdgeGlowBottom.onRelease();
+    }
+
+    private void onDragFinished(int flingDelta) {
+        if (!snapToTop(flingDelta)) {
+            // The drag/fling won't result in the content at the top of the Window. Consider
+            // snapping the content to the bottom of the window.
+            snapToBottom(flingDelta);
+        }
+    }
+
+    /**
+     * If needed, snap the subviews to the top of the Window.
+     */
+    private boolean snapToTop(int flingDelta) {
+        if (-getScroll() - flingDelta < 0
+                && -getScroll() - flingDelta > -mTransparentStartHeight
+                - mElasticScrollOverTopRegion) {
+            // We finish scrolling above the empty starting height, and aren't projected
+            // to fling past the top of the Window by mElasticScrollOverTopRegion worth of
+            // pixels, so elastically snap the empty space shut.
+            mScroller.forceFinished(true);
+            smoothScrollBy(-getScroll() + mTransparentStartHeight);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * If needed, scroll all the subviews off the bottom of the Window.
+     */
+    private void snapToBottom(int flingDelta) {
+        if (-getScroll() - flingDelta > 0) {
+            mScroller.forceFinished(true);
+            ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
+                    getScroll() - getScrollUntilOffBottom());
+            translateAnimation.setRepeatCount(0);
+            translateAnimation.setInterpolator(new AccelerateInterpolator());
+            translateAnimation.start();
+        }
+    }
+
+    @Override
+    public void scrollTo(int x, int y) {
+        int delta = y - getScroll();
+        if (delta > 0) {
+            scrollUp(delta);
+        } else {
+            scrollDown(delta);
+        }
+    }
+
+    @NeededForReflection
+    public void setScroll(int scroll) {
+        scrollTo(0, scroll);
+    }
+
+    /**
+     * Returns the total amount scrolled inside the nested ScrollView + the amount of shrinking
+     * performed on the ToolBar.
+     */
+    public int getScroll() {
+        final LinearLayout.LayoutParams toolbarLayoutParams
+                = (LayoutParams) mToolbar.getLayoutParams();
+        return mTransparentStartHeight - toolbarLayoutParams.topMargin
+                + mMaximumHeaderHeight - toolbarLayoutParams.height + mScrollView.getScrollY();
+    }
+
+    /**
+     * Return amount of scrolling needed in order for all the visible subviews to scroll off the
+     * bottom.
+     */
+    public int getScrollUntilOffBottom() {
+        return getHeight() + getScroll() - mTransparentStartHeight;
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            // Examine the fling results in order to activate EdgeEffect when we fling to the end.
+            final int oldScroll = getScroll();
+            scrollTo(0, mScroller.getCurrY());
+            final int delta = mScroller.getCurrY() - oldScroll;
+            final int distanceFromMaxScrolling = getMaximumScrollUpwards() - getScroll();
+            if (delta > distanceFromMaxScrolling && distanceFromMaxScrolling > 0) {
+                mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
+            }
+
+            if (!awakenScrollBars()) {
+                // Keep on drawing until the animation has finished.
+                postInvalidateOnAnimation();
+            }
+            if (mScroller.getCurrY() >= getMaximumScrollUpwards()) {
+                mScroller.abortAnimation();
+            }
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        if (!mEdgeGlowBottom.isFinished()) {
+            final int restoreCount = canvas.save();
+            final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+            final int height = getHeight();
+
+            // Draw the EdgeEffect on the bottom of the Window (Or a little bit below the bottom
+            // of the Window if we start to scroll upwards while EdgeEffect is visible). This
+            // does not need to consider the case where this MultiShrinkScroller doesn't fill
+            // the Window, since the nested ScrollView should be set to fillViewport.
+            canvas.translate(-width + getPaddingLeft(),
+                    height + getMaximumScrollUpwards() - getScroll());
+
+            canvas.rotate(180, width, 0);
+            mEdgeGlowBottom.setSize(width, height);
+            if (mEdgeGlowBottom.draw(canvas)) {
+                postInvalidateOnAnimation();
+            }
+            canvas.restoreToCount(restoreCount);
+        }
+    }
+
+    private float getCurrentVelocity() {
+        mVelocityTracker.computeCurrentVelocity(PIXELS_PER_SECOND, mMaximumVelocity);
+        return mVelocityTracker.getYVelocity();
+    }
+
+    private void fling(float velocity) {
+        // For reasons I do not understand, scrolling is less janky when maxY=Integer.MAX_VALUE
+        // then when maxY is set to an actual value.
+        mScroller.fling(0, getScroll(), 0, (int) velocity, 0, 0, -Integer.MAX_VALUE,
+                Integer.MAX_VALUE);
+        invalidate();
+    }
+
+    private int getMaximumScrollUpwards() {
+        return mTransparentStartHeight
+                // How much the Header view can compress
+                + mMaximumHeaderHeight - mMinimumHeaderHeight
+                // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
+                + Math.max(0, mScrollViewChild.getHeight() - getHeight() + mMinimumHeaderHeight);
+    }
+
+    private void scrollUp(int delta) {
+        LinearLayout.LayoutParams toolbarLayoutParams = (LayoutParams) mToolbar.getLayoutParams();
+        if (toolbarLayoutParams.topMargin != 0) {
+            final int originalValue = toolbarLayoutParams.topMargin;
+            toolbarLayoutParams.topMargin -= delta;
+            toolbarLayoutParams.topMargin = Math.max(toolbarLayoutParams.topMargin, 0);
+            mToolbar.setLayoutParams(toolbarLayoutParams);
+            delta -= originalValue - toolbarLayoutParams.topMargin;
+        }
+        if (toolbarLayoutParams.height != mMinimumHeaderHeight) {
+            final int originalValue = toolbarLayoutParams.height;
+            toolbarLayoutParams.height -= delta;
+            toolbarLayoutParams.height = Math.max(toolbarLayoutParams.height, mMinimumHeaderHeight);
+            mToolbar.setLayoutParams(toolbarLayoutParams);
+            delta -= originalValue - toolbarLayoutParams.height;
+        }
+        mScrollView.scrollBy(0, delta);
+    }
+
+    private void scrollDown(int delta) {
+        LinearLayout.LayoutParams toolbarLayoutParams = (LayoutParams) mToolbar.getLayoutParams();
+        if (mScrollView.getScrollY() > 0) {
+            final int originalValue = mScrollView.getScrollY();
+            mScrollView.scrollBy(0, delta);
+            delta -= mScrollView.getScrollY() - originalValue;
+        }
+        if (toolbarLayoutParams.height != mMaximumHeaderHeight) {
+            final int originalValue = toolbarLayoutParams.height;
+            toolbarLayoutParams.height -= delta;
+            toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height, mMaximumHeaderHeight);
+            mToolbar.setLayoutParams(toolbarLayoutParams);
+            delta -= originalValue - toolbarLayoutParams.height;
+        }
+        toolbarLayoutParams.topMargin -= delta;
+        mToolbar.setLayoutParams(toolbarLayoutParams);
+
+        if (mListener != null && getScrollUntilOffBottom() <= 0) {
+            post(new Runnable() {
+                @Override
+                public void run() {
+                    mListener.onScrolledOffBottom();
+                }
+            });
+        }
+    }
+
+    private void updateLastEventPosition(MotionEvent event) {
+        mLastEventPosition[0] = event.getX();
+        mLastEventPosition[1] = event.getY();
+    }
+
+    private boolean motionShouldStartDrag(MotionEvent event) {
+        final float deltaX = event.getX() - mLastEventPosition[0];
+        final float deltaY = event.getY() - mLastEventPosition[1];
+        final boolean draggedX = (deltaX > mTouchSlop || deltaX < -mTouchSlop);
+        final boolean draggedY = (deltaY > mTouchSlop || deltaY < -mTouchSlop);
+        return draggedY && !draggedX;
+    }
+
+    private float updatePositionAndComputeDelta(MotionEvent event) {
+        final int VERTICAL = 1;
+        final float position = mLastEventPosition[VERTICAL];
+        updateLastEventPosition(event);
+        return position - mLastEventPosition[VERTICAL];
+    }
+
+    private void smoothScrollBy(int delta) {
+        mScroller.startScroll(0, getScroll(), 0, delta);
+        invalidate();
+    }
+}
diff --git a/src/com/android/contacts/widget/TouchlessScrollView.java b/src/com/android/contacts/widget/TouchlessScrollView.java
new file mode 100644
index 0000000..693dcbc
--- /dev/null
+++ b/src/com/android/contacts/widget/TouchlessScrollView.java
@@ -0,0 +1,43 @@
+package com.android.contacts.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.widget.ScrollView;
+
+/**
+ * A {@link ScrollView} that doesn't respond or intercept touch events.
+ *
+ * This is used in combination with {@link com.android.contacts.widget.MultiShrinkScroller} so
+ * that MultiShrinkScroller can handle all scrolling.
+ */
+public class TouchlessScrollView extends ScrollView {
+
+    public TouchlessScrollView(Context context) {
+        this(context, null);
+    }
+
+    public TouchlessScrollView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TouchlessScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        return false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        return false;
+    }
+}
\ No newline at end of file