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