Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 9ecfd22d authored by Brian Attwell's avatar Brian Attwell Committed by Android Git Automerger
Browse files

am 7609b7dc: Make color tinting less offensive

* commit '7609b7dc':
  Make color tinting less offensive
parents 66964ca7 7609b7dc
Loading
Loading
Loading
Loading
+0 −2
Original line number Diff line number Diff line
@@ -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
+0 −25
Original line number Diff line number Diff line
@@ -874,7 +874,6 @@ public class QuickContactActivity extends ContactsActivity {
        mPhotoView.setIsBusiness(mContactData.isDisplayNameFromOrganization());
        mPhotoSetter.setupContactPhoto(data, mPhotoView);
        extractAndApplyTintFromPhotoViewAsynchronously();
        analyzeWhitenessOfPhotoAsynchronously();
        setHeaderNameText(ContactDisplayUtils.getDisplayName(this, data).toString());

        Trace.endSection();
@@ -1824,30 +1823,6 @@ public class QuickContactActivity extends ContactsActivity {
        }.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;
+0 −131
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+64 −66
Original line number Diff line number Diff line
@@ -76,14 +76,21 @@ public class MultiShrinkScroller extends FrameLayout {
    private static final int EXIT_FLING_ANIMATION_DURATION_MS = 300;

    /**
     * Length of the entrance animation.
     * In portrait mode, the height:width ratio of the photo's starting height.
     */
    private static final int ENTRANCE_ANIMATION_SLIDE_OPEN_DURATION_MS = 250;
    private static final float INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.6f;

    /**
     * In portrait mode, the height:width ratio of the photo's starting height.
     * 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 INTERMEDIATE_HEADER_HEIGHT_RATIO = 0.5f;
    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 @@ public class MultiShrinkScroller extends FrameLayout {

    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 @@ public class MultiShrinkScroller extends FrameLayout {
    }

    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 @@ public class MultiShrinkScroller extends FrameLayout {
        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 @@ public class MultiShrinkScroller extends FrameLayout {
    }

    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 @@ public class MultiShrinkScroller extends FrameLayout {

        // Reuse an existing mColorFilter (to avoid GC pauses) to change the photo's tint.
        mPhotoView.clearColorFilter();
        mColorMatrix.reset();

        // 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);
        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.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;
            mColorMatrix.postConcat(alphaMatrix(DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA,
                    mHeaderTintColor));
            gradientAlpha = 0;
        } else {
            // 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_ALPHA, 1/TILE_EXPONENT)));
            float linearBeforeMiddleish = Math.max(1 - (1 - ratio) / intermediateRatio
                    / (1 - Math.pow(1 - DESIRED_INTERMEDIATE_LETTER_TILE_ALPHA, 1/TILE_EXPONENT)));
            float linearBeforeIntermediate = Math.max(1 - (1 - ratio) / intermediateRatio
                    / slowingFactor, 0);
            colorAlpha = 1 - (float) Math.pow(linearBeforeMiddleish, TILE_EXPONENT);
            float colorAlpha = 1 - (float) Math.pow(linearBeforeIntermediate, TILE_EXPONENT);
            mColorMatrix.postConcat(alphaMatrix(colorAlpha, mHeaderTintColor));
        } else {
            colorAlpha = 1 - semiLinearBeforeMiddle;
            mColorMatrix.postConcat(multiplyBlendMatrix(mHeaderTintColor, colorAlpha));
            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}.
     */