Clicking on QC header expands the header

* The maximum expansion size of the image is equal to its width.
* For the minimum header size. We now rely on android.R.attr.actionBarSize
  instead of using a value in dimens.

Change-Id: I9e2438a457bd2afc50f07d028c810efc8455678e
diff --git a/res/layout/quickcontact_activity.xml b/res/layout/quickcontact_activity.xml
index a7c12dd..8f78811 100644
--- a/res/layout/quickcontact_activity.xml
+++ b/res/layout/quickcontact_activity.xml
@@ -26,7 +26,7 @@
 
     <FrameLayout
         android:layout_width="match_parent"
-        android:layout_height="@dimen/quickcontact_maximum_header_height"
+        android:layout_height="match_parent"
         android:layout_marginTop="@dimen/quickcontact_starting_empty_height"
         android:background="@color/card_margin_color"
         android:id="@+id/toolbar_parent">
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index cb9f101..99bc345 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -18,15 +18,8 @@
 
     <!-- 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>
+    <!-- Initial height of QuickContact's header/avatar-photo -->
+    <dimen name="quickcontact_starting_header_height">200dp</dimen>
 
     <!-- Top padding of the entire contact editor  -->
     <dimen name="editor_padding_top">0dip</dimen>
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index dcdeb1c..4d38f33 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -322,7 +322,9 @@
                 mScroller.setVisibility(View.VISIBLE);
                 mScroller.setScroll(mScroller.getScrollNeededToBeFullScreen());
             } else {
-                mScroller.setVisibility(View.GONE);
+                // mScroller needs to perform asynchronous measurements after initalize(), therefore
+                // we can't mark this as GONE.
+                mScroller.setVisibility(View.INVISIBLE);
             }
         }
 
diff --git a/src/com/android/contacts/widget/MultiShrinkScroller.java b/src/com/android/contacts/widget/MultiShrinkScroller.java
index 01aeb4e..4a3aff4 100644
--- a/src/com/android/contacts/widget/MultiShrinkScroller.java
+++ b/src/com/android/contacts/widget/MultiShrinkScroller.java
@@ -2,9 +2,13 @@
 
 import com.android.contacts.R;
 import com.android.contacts.test.NeededForReflection;
+import com.android.contacts.util.SchedulingUtils;
 
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
 import android.animation.ObjectAnimator;
 import android.content.Context;
+import android.content.res.TypedArray;
 import android.graphics.Canvas;
 import android.graphics.PorterDuff;
 import android.graphics.PorterDuffColorFilter;
@@ -54,16 +58,16 @@
     private View mPhotoViewContainer;
     private MultiShrinkScrollerListener mListener;
     private int mHeaderTintColor;
+    private int mMaximumHeaderHeight;
 
     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 mIntermediateHeaderHeight;
     private final int mMinimumHeaderHeight;
     private final int mTransparentStartHeight;
-    private final int mElasticScrollOverTopRegion;
     private final float mToolbarElevation;
     private final PorterDuffColorFilter mColorFilter
             = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP);
@@ -76,7 +80,26 @@
         void onExitFullscreen();
     }
 
-    // Interpolator from android.support.v4.view.ViewPager
+    private final AnimatorListener mHeaderExpandAnimationListener = new AnimatorListener() {
+        @Override
+        public void onAnimationStart(Animator animation) {}
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mPhotoView.setClickable(true);
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {}
+
+        @Override
+        public void onAnimationRepeat(Animator animation) {}
+    };
+
+    /**
+     * Interpolator from android.support.v4.view.ViewPager. Snappier and more elastic feeling
+     * than the default interpolator.
+     */
     private static final Interpolator sInterpolator = new Interpolator() {
 
         /**
@@ -110,18 +133,19 @@
         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);
+        mIntermediateHeaderHeight = (int) getResources().getDimension(
+                R.dimen.quickcontact_starting_header_height);
         mTransparentStartHeight = (int) getResources().getDimension(
                 R.dimen.quickcontact_starting_empty_height);
-        mElasticScrollOverTopRegion = (int) getResources().getDimension(
-                R.dimen.quickcontact_elastic_scroll_over_top_region);
         mHeaderTintColor = mContext.getResources().getColor(
                 R.color.actionbar_background_color);
-        mToolbarElevation = (float) mContext.getResources().getDimension(
+        mToolbarElevation = mContext.getResources().getDimension(
                 R.dimen.quick_contact_toolbar_elevation);
+
+        final TypedArray attributeArray = context.obtainStyledAttributes(
+                new int[]{android.R.attr.actionBarSize});
+        mMinimumHeaderHeight = attributeArray.getDimensionPixelSize(0, 0);
+        attributeArray.recycle();
     }
 
     /**
@@ -131,9 +155,25 @@
         mScrollView = (ScrollView) findViewById(R.id.content_scroller);
         mScrollViewChild = findViewById(R.id.card_container);
         mToolbar = findViewById(R.id.toolbar_parent);
-        mPhotoView = (ImageView) findViewById(R.id.photo);
         mPhotoViewContainer = findViewById(R.id.toolbar_parent);
         mListener = listener;
+
+        mPhotoView = (ImageView) findViewById(R.id.photo);
+        setHeaderHeight(mIntermediateHeaderHeight);
+        mPhotoView.setOnClickListener(new OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                expandCollapseHeader();
+            }
+        });
+
+        SchedulingUtils.doOnPreDraw(this, /* drawNextFrame = */ true, new Runnable() {
+            @Override
+            public void run() {
+                // We never want the height of the photo view to exceed its width.
+                mMaximumHeaderHeight = mToolbar.getWidth();
+            }
+        });
     }
 
     @Override
@@ -233,6 +273,25 @@
         updatePhotoTintAndDropShadow();
     }
 
+    /**
+     * Expand to maximum size or starting size. Disable clicks on the photo until the animation is
+     * complete.
+     */
+    private void expandCollapseHeader() {
+        mPhotoView.setClickable(false);
+        if (getHeaderHeight() != mMaximumHeaderHeight) {
+            final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
+                    mMaximumHeaderHeight);
+            animator.addListener(mHeaderExpandAnimationListener);
+            animator.start();
+        } else if (getHeaderHeight() != mMinimumHeaderHeight) {
+            final ObjectAnimator animator = ObjectAnimator.ofInt(this, "headerHeight",
+                    mIntermediateHeaderHeight);
+            animator.addListener(mHeaderExpandAnimationListener);
+            animator.start();
+        }
+    }
+
     private void startDrag() {
         mIsBeingDragged = true;
         mScroller.abortAnimation();
@@ -272,14 +331,12 @@
      * If needed, snap the subviews to the top of the Window.
      */
     private boolean snapToTop(int flingDelta) {
-        if (-getScroll() - flingDelta < 0
-                && -getScroll() - flingDelta > -mTransparentStartHeight
-                - mElasticScrollOverTopRegion) {
+        if (-getScroll_ignoreOversizedHeader() - flingDelta < 0
+                && -getScroll_ignoreOversizedHeader() - flingDelta > -mTransparentStartHeight) {
             // 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.
+            // to fling past the top of the Window, so elastically snap the empty space shut.
             mScroller.forceFinished(true);
-            smoothScrollBy(-getScroll() + mTransparentStartHeight);
+            smoothScrollBy(-getScroll_ignoreOversizedHeader() + mTransparentStartHeight);
             return true;
         }
         return false;
@@ -289,7 +346,7 @@
      * If needed, scroll all the subviews off the bottom of the Window.
      */
     private void snapToBottom(int flingDelta) {
-        if (-getScroll() - flingDelta > 0) {
+        if (-getScroll_ignoreOversizedHeader() - flingDelta > 0) {
             mScroller.forceFinished(true);
             ObjectAnimator translateAnimation = ObjectAnimator.ofInt(this, "scroll",
                     getScroll() - getScrollUntilOffBottom());
@@ -319,6 +376,23 @@
         }
     }
 
+    /**
+     * Set the height of the toolbar and update its tint accordingly.
+     */
+    @NeededForReflection
+    public void setHeaderHeight(int height) {
+        final LinearLayout.LayoutParams toolbarLayoutParams
+                = (LayoutParams) mToolbar.getLayoutParams();
+        toolbarLayoutParams.height = height;
+        mToolbar.setLayoutParams(toolbarLayoutParams);
+        updatePhotoTintAndDropShadow();
+    }
+
+    @NeededForReflection
+    public int getHeaderHeight() {
+        return mToolbar.getLayoutParams().height;
+    }
+
     @NeededForReflection
     public void setScroll(int scroll) {
         scrollTo(0, scroll);
@@ -326,13 +400,27 @@
 
     /**
      * Returns the total amount scrolled inside the nested ScrollView + the amount of shrinking
-     * performed on the ToolBar.
+     * performed on the ToolBar. This is the value inspected by animators.
      */
+    @NeededForReflection
     public int getScroll() {
         final LinearLayout.LayoutParams toolbarLayoutParams
                 = (LayoutParams) mToolbar.getLayoutParams();
         return mTransparentStartHeight - toolbarLayoutParams.topMargin
-                + mMaximumHeaderHeight - toolbarLayoutParams.height + mScrollView.getScrollY();
+                + mIntermediateHeaderHeight - toolbarLayoutParams.height + mScrollView.getScrollY();
+    }
+
+    /**
+     * A variant of {@link #getScroll} that pretends the header is never larger than
+     * than mIntermediateHeaderHeight. This function is sometimes needed when making scrolling
+     * decisions that will not change the header size (ie, snapping to the bottom or top).
+     */
+    public int getScroll_ignoreOversizedHeader() {
+        final LinearLayout.LayoutParams toolbarLayoutParams
+                = (LayoutParams) mToolbar.getLayoutParams();
+        return mTransparentStartHeight - toolbarLayoutParams.topMargin
+                + Math.max(mIntermediateHeaderHeight - toolbarLayoutParams.height, 0)
+                + mScrollView.getScrollY();
     }
 
     /**
@@ -349,7 +437,7 @@
      * bottom.
      */
     public int getScrollUntilOffBottom() {
-        return getHeight() + getScroll() - mTransparentStartHeight;
+        return getHeight() + getScroll_ignoreOversizedHeader() - mTransparentStartHeight;
     }
 
     @Override
@@ -415,7 +503,7 @@
     private int getMaximumScrollUpwards() {
         return mTransparentStartHeight
                 // How much the Header view can compress
-                + mMaximumHeaderHeight - mMinimumHeaderHeight
+                + mIntermediateHeaderHeight - mMinimumHeaderHeight
                 // How much the ScrollView can scroll. 0, if child is smaller than ScrollView.
                 + Math.max(0, mScrollViewChild.getHeight() - getHeight() + mMinimumHeaderHeight);
     }
@@ -446,10 +534,11 @@
             mScrollView.scrollBy(0, delta);
             delta -= mScrollView.getScrollY() - originalValue;
         }
-        if (toolbarLayoutParams.height != mMaximumHeaderHeight) {
+        if (toolbarLayoutParams.height < mIntermediateHeaderHeight) {
             final int originalValue = toolbarLayoutParams.height;
             toolbarLayoutParams.height -= delta;
-            toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height, mMaximumHeaderHeight);
+            toolbarLayoutParams.height = Math.min(toolbarLayoutParams.height,
+                    mIntermediateHeaderHeight);
             mToolbar.setLayoutParams(toolbarLayoutParams);
             delta -= originalValue - toolbarLayoutParams.height;
         }
@@ -472,7 +561,7 @@
         final int toolbarHeight = mToolbar.getLayoutParams().height;
         // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
         mPhotoView.clearColorFilter();
-        if (toolbarHeight >= mMaximumHeaderHeight) {
+        if (toolbarHeight >= mIntermediateHeaderHeight) {
             mPhotoViewContainer.setElevation(0);
             return;
         }
@@ -480,10 +569,10 @@
             mColorFilter.setColor(mHeaderTintColor);
             mPhotoView.setColorFilter(mColorFilter);
             mPhotoViewContainer.setElevation(mToolbarElevation);
-        } else {
+        } else if (toolbarHeight <= mIntermediateHeaderHeight) {
             mPhotoViewContainer.setElevation(0);
-            final int alphaBits = 0xff - 0xff * (mToolbar.getHeight()  - mMinimumHeaderHeight)
-                    / (mMaximumHeaderHeight - mMinimumHeaderHeight);
+            final int alphaBits = 0xff - 0xff * (toolbarHeight  - mMinimumHeaderHeight)
+                    / (mIntermediateHeaderHeight - mMinimumHeaderHeight);
             final int color = alphaBits << 24 | (mHeaderTintColor & 0xffffff);
             mColorFilter.setColor(color);
             mPhotoView.setColorFilter(mColorFilter);
@@ -511,6 +600,12 @@
     }
 
     private void smoothScrollBy(int delta) {
+        if (delta == 0) {
+            // Delta=0 implies the code calling smoothScrollBy is sloppy. We should avoid doing
+            // this, since it prevents Views from being able to register any clicks for 250ms.
+            throw new IllegalArgumentException("Smooth scrolling by delta=0 is "
+                    + "pointless and harmful");
+        }
         mScroller.startScroll(0, getScroll(), 0, delta);
         invalidate();
     }