Make color tinting less offensive

-don't tint colors when opening QuickContacts in its
 semi-collapsed state. Ie, the state it starts in when
 opened from Dialer
-the transition from untinted to solid color is quick
-always apply the black gradients to QuickContacts. Using
 less tinting makes the absence of gradients a bigger
 problem. We could have tweaked WhitenessUtils to apply
 the gradient in all problematic cases. But this turns
 out to be the majority of cases. So we should just
 apply the gradient all the time.
-made the gradient more attractive

Bug: 17944406
Change-Id: I46c8a7a3fccc0f7a7e8bb7470dee621edf92b8cb
diff --git a/res/layout/quickcontact_header.xml b/res/layout/quickcontact_header.xml
index 010146b..88a80f7 100644
--- a/res/layout/quickcontact_header.xml
+++ b/res/layout/quickcontact_header.xml
@@ -34,13 +34,11 @@
         android:id="@+id/title_gradient"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:visibility="gone"
         android:layout_gravity="bottom" />
     <View
         android:id="@+id/action_bar_gradient"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        android:visibility="gone"
         android:layout_gravity="top" />
 
     <!-- Need to set a non null background on Toolbar in order for MenuItem ripples to be drawn on
diff --git a/src/com/android/contacts/quickcontact/QuickContactActivity.java b/src/com/android/contacts/quickcontact/QuickContactActivity.java
index 70d3281..1da010c 100644
--- a/src/com/android/contacts/quickcontact/QuickContactActivity.java
+++ b/src/com/android/contacts/quickcontact/QuickContactActivity.java
@@ -874,7 +874,6 @@
         mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization());
         mPhotoSetter.setupContactPhoto(data, mPhotoView);
         extractAndApplyTintFromPhotoViewAsynchronously();
-        analyzeWhitenessOfPhotoAsynchronously();
         setHeaderNameText(ContactDisplayUtils.getDisplayName(this, data).toString());
 
         Trace.endSection();
@@ -1824,30 +1823,6 @@
         }.execute();
     }
 
-    /**
-     * Examine how many white pixels are in the bitmap in order to determine whether or not
-     * we need gradient overlays on top of the image.
-     */
-    private void analyzeWhitenessOfPhotoAsynchronously() {
-        final Drawable imageViewDrawable = mPhotoView.getDrawable();
-        new AsyncTask<Void, Void, Boolean>() {
-            @Override
-            protected Boolean doInBackground(Void... params) {
-                if (imageViewDrawable instanceof BitmapDrawable) {
-                    final Bitmap bitmap = ((BitmapDrawable) imageViewDrawable).getBitmap();
-                    return WhitenessUtils.isBitmapWhiteAtTopOrBottom(bitmap);
-                }
-                return !(imageViewDrawable instanceof LetterTileDrawable);
-            }
-
-            @Override
-            protected void onPostExecute(Boolean isWhite) {
-                super.onPostExecute(isWhite);
-                mScroller.setUseGradient(isWhite);
-            }
-        }.execute();
-    }
-
     private void setThemeColor(MaterialPalette palette) {
         // If the color is invalid, use the predefined default
         final int primaryColor = palette.mPrimaryColor;
diff --git a/src/com/android/contacts/quickcontact/WhitenessUtils.java b/src/com/android/contacts/quickcontact/WhitenessUtils.java
deleted file mode 100644
index 0d2f2e2..0000000
--- a/src/com/android/contacts/quickcontact/WhitenessUtils.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2014 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.android.contacts.quickcontact;
-
-import android.graphics.Bitmap;
-import android.graphics.Color;
-import android.os.Trace;
-
-/**
- * Utility class for determining whether Bitmaps contain a lot of white pixels in locations
- * where QuickContactActivity will want to place white text or buttons.
- *
- * This class liberally considers bitmaps white. All constants are chosen with a small amount of
- * experimentation. Despite a lack of rigour, this class successfully allows QuickContactsActivity
- * to detect when Bitmap are obviously *not* white. Therefore, it is better than nothing.
- */
-public class WhitenessUtils {
-
-    /**
-     * Analyze this amount of the top and bottom of the bitmap.
-     */
-    private static final float HEIGHT_PERCENT_ANALYZED = 0.2f;
-
-    /**
-     * An image with more than this amount white, is considered to be a whitish image.
-     */
-    private static final float PROPORTION_WHITE_CUTOFF = 0.1f;
-
-    private static final float THIRD = 0.33f;
-
-    /**
-     * Colors with luma greater than this are considered close to white. This value is lower than
-     * the value used in Palette's ColorUtils, since we want to liberally declare images white.
-     */
-    private static final float LUMINANCE_OF_WHITE =  0.90f;
-
-    /**
-     * Returns true if 20% of the image's top right corner is white, or 20% of the bottom
-     * of the image is white.
-     */
-    public static boolean isBitmapWhiteAtTopOrBottom(Bitmap largeBitmap) {
-        Trace.beginSection("isBitmapWhiteAtTopOrBottom");
-        try {
-            final Bitmap smallBitmap = scaleBitmapDown(largeBitmap);
-
-            final int[] rgbPixels = new int[smallBitmap.getWidth() * smallBitmap.getHeight()];
-            smallBitmap.getPixels(rgbPixels, 0, smallBitmap.getWidth(), 0, 0,
-                    smallBitmap.getWidth(), smallBitmap.getHeight());
-
-            // look at top right corner of the bitmap
-            int whiteCount = 0;
-            for (int y = 0; y < smallBitmap.getHeight() * HEIGHT_PERCENT_ANALYZED; y++) {
-                for (int x = (int) (smallBitmap.getWidth() * (1 - THIRD));
-                        x < smallBitmap.getWidth(); x++) {
-                    final int rgb = rgbPixels[y * smallBitmap.getWidth() + x];
-                    if (isWhite(rgb)) {
-                        whiteCount ++;
-                    }
-                }
-            }
-            int totalPixels = (int) (smallBitmap.getHeight() * smallBitmap.getWidth()
-                    * THIRD * HEIGHT_PERCENT_ANALYZED);
-            if (whiteCount / (float) totalPixels > PROPORTION_WHITE_CUTOFF) {
-                return true;
-            }
-
-            // look at bottom portion of bitmap
-            whiteCount = 0;
-            for (int y = (int) (smallBitmap.getHeight() * (1 - HEIGHT_PERCENT_ANALYZED));
-                    y <  smallBitmap.getHeight(); y++) {
-                for (int x = 0; x < smallBitmap.getWidth(); x++) {
-                    final int rgb = rgbPixels[y * smallBitmap.getWidth() + x];
-                    if (isWhite(rgb)) {
-                        whiteCount ++;
-                    }
-                }
-            }
-
-            totalPixels = (int) (smallBitmap.getHeight()
-                    * smallBitmap.getWidth() * HEIGHT_PERCENT_ANALYZED);
-
-            return whiteCount / (float) totalPixels > PROPORTION_WHITE_CUTOFF;
-        } finally {
-            Trace.endSection();
-        }
-    }
-
-    private static boolean isWhite(int rgb) {
-        return calculateXyzLuma(rgb) > LUMINANCE_OF_WHITE;
-    }
-
-    private static float calculateXyzLuma(int rgb) {
-        return (0.2126f * Color.red(rgb) +
-                0.7152f * Color.green(rgb) +
-                0.0722f * Color.blue(rgb)) / 255f;
-    }
-
-    /**
-     * Scale down the bitmap in order to make color analysis faster. Taken from Palette.
-     */
-    private static Bitmap scaleBitmapDown(Bitmap bitmap) {
-        final int CALCULATE_BITMAP_MIN_DIMENSION = 100;
-        final int minDimension = Math.min(bitmap.getWidth(), bitmap.getHeight());
-
-        if (minDimension <= CALCULATE_BITMAP_MIN_DIMENSION) {
-            // If the bitmap is small enough already, just return it
-            return bitmap;
-        }
-
-        final float scaleRatio = CALCULATE_BITMAP_MIN_DIMENSION / (float) minDimension;
-        return Bitmap.createScaledBitmap(bitmap,
-                Math.round(bitmap.getWidth() * scaleRatio),
-                Math.round(bitmap.getHeight() * scaleRatio),
-                false);
-    }
-}
diff --git a/src/com/android/contacts/widget/MultiShrinkScroller.java b/src/com/android/contacts/widget/MultiShrinkScroller.java
index 5106978..f7f0c7b 100644
--- a/src/com/android/contacts/widget/MultiShrinkScroller.java
+++ b/src/com/android/contacts/widget/MultiShrinkScroller.java
@@ -76,14 +76,21 @@
     private static final int EXIT_FLING_ANIMATION_DURATION_MS = 300;
 
     /**
-     * Length of the entrance animation.
-     */
-    private static final int ENTRANCE_ANIMATION_SLIDE_OPEN_DURATION_MS = 250;
-
-    /**
      * In portrait mode, the height:width ratio of the photo's starting height.
      */
-    private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.5f;
+    private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.6f;
+
+    /**
+     * Color blending will only be performed on the contact photo once the toolbar is compressed
+     * to this ratio of its full height.
+     */
+    private static final float COLOR_BLENDING_START_RATIO = 0.5f;
+
+    /**
+     * When displaying a letter tile drawable, this alpha value should be used at the intermediate
+     * toolbar height.
+     */
+    private static final float DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA = 0.8f;
 
     /**
      * Maximum velocity for flings in dips per second. Picked via non-rigorous experimentation.
@@ -166,14 +173,8 @@
 
     private final PathInterpolator mTextSizePathInterpolator
             = new PathInterpolator(0.16f, 0.4f, 0.2f, 1);
-    /**
-     * Interpolator that starts and ends with nearly straight segments. At x=0 it has a y of
-     * approximately 0.25. We only want the contact photo 25% faded when half collapsed.
-     */
-    private final PathInterpolator mWhiteBlendingPathInterpolator
-            = new PathInterpolator(1.0f, 0.4f, 0.9f, 0.8f);
 
-    private final int[] mGradientColors = new int[] {0,0xAA000000};
+    private final int[] mGradientColors = new int[] {0,0x88000000};
     private GradientDrawable mTitleGradientDrawable = new GradientDrawable(
             GradientDrawable.Orientation.TOP_BOTTOM, mGradientColors);
     private GradientDrawable mActionBarGradientDrawable = new GradientDrawable(
@@ -363,18 +364,17 @@
     }
 
     private void configureGradientViewHeights() {
-        final float GRADIENT_SIZE_COEFFICIENT = 1.25f;
         final FrameLayout.LayoutParams actionBarGradientLayoutParams
                 = (FrameLayout.LayoutParams) mActionBarGradientView.getLayoutParams();
-        actionBarGradientLayoutParams.height
-                = (int) (mActionBarSize * GRADIENT_SIZE_COEFFICIENT);
+        actionBarGradientLayoutParams.height = mActionBarSize;
         mActionBarGradientView.setLayoutParams(actionBarGradientLayoutParams);
         final FrameLayout.LayoutParams titleGradientLayoutParams
                 = (FrameLayout.LayoutParams) mTitleGradientView.getLayoutParams();
+        final float TITLE_GRADIENT_SIZE_COEFFICIENT = 1.25f;
         final FrameLayout.LayoutParams largeTextLayoutParms
                 = (FrameLayout.LayoutParams) mLargeTextView.getLayoutParams();
         titleGradientLayoutParams.height = (int) ((mLargeTextView.getHeight()
-                + largeTextLayoutParms.bottomMargin) * GRADIENT_SIZE_COEFFICIENT);
+                + largeTextLayoutParms.bottomMargin) * TITLE_GRADIENT_SIZE_COEFFICIENT);
         mTitleGradientView.setLayoutParams(titleGradientLayoutParams);
     }
 
@@ -383,13 +383,6 @@
         mPhotoTouchInterceptOverlay.setContentDescription(title);
     }
 
-    public void setUseGradient(boolean useGradient) {
-        if (mTitleGradientView != null) {
-            mTitleGradientView.setVisibility(useGradient ? View.VISIBLE : View.GONE);
-            mActionBarGradientView.setVisibility(useGradient ? View.VISIBLE : View.GONE);
-        }
-    }
-
     @Override
     public boolean onInterceptTouchEvent(MotionEvent event) {
         if (mVelocityTracker == null) {
@@ -1033,9 +1026,7 @@
     }
 
     private void updatePhotoTintAndDropShadow() {
-        // Let's keep an eye on how long this method takes to complete. Right now, it takes ~0.2ms
-        // on a Nexus 5. If it starts to get much slower, there are a number of easy optimizations
-        // available.
+        // Let's keep an eye on how long this method takes to complete.
         Trace.beginSection("updatePhotoTintAndDropShadow");
 
         if (mIsTwoPanel && !mPhotoView.isBasedOffLetterTile()) {
@@ -1058,68 +1049,75 @@
 
         // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
         mPhotoView.clearColorFilter();
-
-        // Ratio of current size to maximum size of the header.
-        final float ratio;
-        // The value that "ratio" will have when the header is at its starting/intermediate size.
-        final float intermediateRatio = calculateHeightRatio((int)
-                (mMaximumPortraitHeaderHeight * INTERMEDIATE_HEADER_HEIGHT_RATIO));
-        if (!mIsTwoPanel) {
-            ratio = calculateHeightRatio(toolbarHeight);
-        } else {
-            // We want the ratio and intermediateRatio to have the *approximate* values
-            // they would have in portrait mode when at the intermediate position.
-            ratio = intermediateRatio;
-        }
-
-        final float linearBeforeMiddle = Math.max(1 - (1 - ratio) / intermediateRatio, 0);
-
-        // Want a function with a derivative of 0 at x=0. I don't want it to grow too
-        // slowly before x=0.5. x^1.1 satisfies both requirements.
-        final float EXPONENT_ALMOST_ONE = 1.1f;
-        final float semiLinearBeforeMiddle = (float) Math.pow(linearBeforeMiddle,
-                EXPONENT_ALMOST_ONE);
         mColorMatrix.reset();
-        mColorMatrix.setSaturation(semiLinearBeforeMiddle);
-        mColorMatrix.postConcat(alphaMatrix(
-                1 - mWhiteBlendingPathInterpolator.getInterpolation(1 - ratio), Color.WHITE));
 
-        final float colorAlpha;
-        if (mPhotoView.isBasedOffLetterTile()) {
-            // Since the letter tile only has white and grey, tint it more slowly. Otherwise
-            // it will be completely invisible before we reach the intermediate point. The values
-            // for TILE_EXPONENT and slowingFactor are chosen to achieve DESIRED_INTERMEDIATE_ALPHA
-            // at the intermediate/starting position.
-            final float DESIRED_INTERMEDIATE_ALPHA = 0.9f;
-            final float TILE_EXPONENT = 1.5f;
-            final float slowingFactor = (float) ((1 - intermediateRatio) / intermediateRatio
-                    / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_ALPHA, 1/TILE_EXPONENT)));
-            float linearBeforeMiddleish = Math.max(1 - (1 - ratio) / intermediateRatio
-                    / slowingFactor, 0);
-            colorAlpha = 1 - (float) Math.pow(linearBeforeMiddleish, TILE_EXPONENT);
-            mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
+        final int gradientAlpha;
+        if (!mPhotoView.isBasedOffLetterTile()) {
+            // Constants and equations were arbitrarily picked to choose values for saturation,
+            // whiteness, tint and gradient alpha. There were four main objectives:
+            // 1) The transition period between the unmodified image and fully colored image should
+            //    be very short.
+            // 2) The tinting should be fully applied even before the background image is fully
+            //    faded out and desaturated. Why? A half tinted photo looks bad and results in
+            //    unappealing colors.
+            // 3) The function should have a derivative of 0 at ratio = 1 to avoid discontinuities.
+            // 4) The entire process should look awesome.
+            final float ratio = calculateHeightRatioToBlendingStartHeight(toolbarHeight);
+            final float alpha = 1.0f - (float) Math.min(Math.pow(ratio, 1.5f) * 2f, 1f);
+            final float tint = (float) Math.min(Math.pow(ratio, 1.5f) * 3f, 1f);
+            mColorMatrix.setSaturation(alpha);
+            mColorMatrix.postConcat(alphaMatrix(alpha, Color.WHITE));
+            mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, tint));
+            gradientAlpha = (int) (255 * alpha);
+        } else if (mIsTwoPanel) {
+            mColorMatrix.reset();
+            mColorMatrix.postConcat(alphaMatrix(DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA,
+                    mHeaderTintColor));
+            gradientAlpha = 0;
         } else {
-            colorAlpha = 1 - semiLinearBeforeMiddle;
-            mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, colorAlpha));
+            // We want a function that has DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA value
+            // at the intermediate position and uses TILE_EXPONENT. Finding an equation
+            // that satisfies this condition requires the following arithmetic.
+            final float ratio = calculateHeightRatioToFullyOpen(toolbarHeight);
+            final float intermediateRatio = calculateHeightRatioToFullyOpen((int)
+                    (mMaximumPortraitHeaderHeight * INTERMEDIATE_HEADER_HEIGHT_RATIO));
+            final float TILE_EXPONENT = 3f;
+            final float slowingFactor = (float) ((1 - intermediateRatio) / intermediateRatio
+                    / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1/TILE_EXPONENT)));
+            float linearBeforeIntermediate = Math.max(1 - (1 - ratio) / intermediateRatio
+                    / slowingFactor, 0);
+            float colorAlpha = 1 - (float) Math.pow(linearBeforeIntermediate, TILE_EXPONENT);
+            mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
+            gradientAlpha = 0;
         }
 
+        // TODO: remove re-allocation of ColorMatrixColorFilter objects (b/17627000)
         mPhotoView.setColorFilter(new ColorMatrixColorFilter(mColorMatrix));
+
         // Tell the photo view what tint we are trying to achieve. Depending on the type of
         // drawable used, the photo view may or may not use this tint.
         mPhotoView.setTint(mHeaderTintColor);
-
-        final int gradientAlpha = (int) (255 * linearBeforeMiddle);
         mTitleGradientDrawable.setAlpha(gradientAlpha);
         mActionBarGradientDrawable.setAlpha(gradientAlpha);
 
         Trace.endSection();
     }
 
-    private float calculateHeightRatio(int height) {
+    private float calculateHeightRatioToFullyOpen(int height) {
         return (height - mMinimumPortraitHeaderHeight)
                 / (float) (mMaximumPortraitHeaderHeight - mMinimumPortraitHeaderHeight);
     }
 
+    private float calculateHeightRatioToBlendingStartHeight(int height) {
+        final float intermediateHeight = mMaximumPortraitHeaderHeight
+                * COLOR_BLENDING_START_RATIO;
+        final float interpolatingHeightRange = intermediateHeight - mMinimumPortraitHeaderHeight;
+        if (height > intermediateHeight) {
+            return 0;
+        }
+        return (intermediateHeight - height) / interpolatingHeightRange;
+    }
+
     /**
      * Simulates alpha blending an image with {@param color}.
      */