Home | History | Annotate | Download | only in util
      1 /*
      2  * Copyright (C) 2015 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.launcher3.util;
     18 
     19 import android.graphics.Bitmap;
     20 import android.graphics.Canvas;
     21 import android.graphics.Color;
     22 import android.graphics.drawable.Drawable;
     23 
     24 import com.android.launcher3.LauncherAppState;
     25 
     26 import java.nio.ByteBuffer;
     27 
     28 public class IconNormalizer {
     29 
     30     // Ratio of icon visible area to full icon size for a square shaped icon
     31     private static final float MAX_SQUARE_AREA_FACTOR = 359.0f / 576;
     32     // Ratio of icon visible area to full icon size for a circular shaped icon
     33     private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
     34 
     35     private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
     36 
     37     // Slope used to calculate icon visible area to full icon size for any generic shaped icon.
     38     private static final float LINEAR_SCALE_SLOPE =
     39             (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
     40 
     41     private static final int MIN_VISIBLE_ALPHA = 40;
     42 
     43     private static final Object LOCK = new Object();
     44     private static IconNormalizer sIconNormalizer;
     45 
     46     private final int mMaxSize;
     47     private final Bitmap mBitmap;
     48     private final Canvas mCanvas;
     49     private final byte[] mPixels;
     50 
     51     // for each y, stores the position of the leftmost x and the rightmost x
     52     private final float[] mLeftBorder;
     53     private final float[] mRightBorder;
     54 
     55     private IconNormalizer() {
     56         // Use twice the icon size as maximum size to avoid scaling down twice.
     57         mMaxSize = LauncherAppState.getInstance().getInvariantDeviceProfile().iconBitmapSize * 2;
     58         mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
     59         mCanvas = new Canvas(mBitmap);
     60         mPixels = new byte[mMaxSize * mMaxSize];
     61 
     62         mLeftBorder = new float[mMaxSize];
     63         mRightBorder = new float[mMaxSize];
     64     }
     65 
     66     /**
     67      * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
     68      * matches the design guidelines for a launcher icon.
     69      *
     70      * We first calculate the convex hull of the visible portion of the icon.
     71      * This hull then compared with the bounding rectangle of the hull to find how closely it
     72      * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an
     73      * ideal solution but it gives satisfactory result without affecting the performance.
     74      *
     75      * This closeness is used to determine the ratio of hull area to the full icon size.
     76      * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
     77      */
     78     public synchronized float getScale(Drawable d) {
     79         int width = d.getIntrinsicWidth();
     80         int height = d.getIntrinsicHeight();
     81         if (width <= 0 || height <= 0) {
     82             width = width <= 0 || width > mMaxSize ? mMaxSize : width;
     83             height = height <= 0 || height > mMaxSize ? mMaxSize : height;
     84         } else if (width > mMaxSize || height > mMaxSize) {
     85             int max = Math.max(width, height);
     86             width = mMaxSize * width / max;
     87             height = mMaxSize * height / max;
     88         }
     89 
     90         mBitmap.eraseColor(Color.TRANSPARENT);
     91         d.setBounds(0, 0, width, height);
     92         d.draw(mCanvas);
     93 
     94         ByteBuffer buffer = ByteBuffer.wrap(mPixels);
     95         buffer.rewind();
     96         mBitmap.copyPixelsToBuffer(buffer);
     97 
     98         // Overall bounds of the visible icon.
     99         int topY = -1;
    100         int bottomY = -1;
    101         int leftX = mMaxSize + 1;
    102         int rightX = -1;
    103 
    104         // Create border by going through all pixels one row at a time and for each row find
    105         // the first and the last non-transparent pixel. Set those values to mLeftBorder and
    106         // mRightBorder and use -1 if there are no visible pixel in the row.
    107 
    108         // buffer position
    109         int index = 0;
    110         // buffer shift after every row, width of buffer = mMaxSize
    111         int rowSizeDiff = mMaxSize - width;
    112         // first and last position for any row.
    113         int firstX, lastX;
    114 
    115         for (int y = 0; y < height; y++) {
    116             firstX = lastX = -1;
    117             for (int x = 0; x < width; x++) {
    118                 if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
    119                     if (firstX == -1) {
    120                         firstX = x;
    121                     }
    122                     lastX = x;
    123                 }
    124                 index++;
    125             }
    126             index += rowSizeDiff;
    127 
    128             mLeftBorder[y] = firstX;
    129             mRightBorder[y] = lastX;
    130 
    131             // If there is at least one visible pixel, update the overall bounds.
    132             if (firstX != -1) {
    133                 bottomY = y;
    134                 if (topY == -1) {
    135                     topY = y;
    136                 }
    137 
    138                 leftX = Math.min(leftX, firstX);
    139                 rightX = Math.max(rightX, lastX);
    140             }
    141         }
    142 
    143         if (topY == -1 || rightX == -1) {
    144             // No valid pixels found. Do not scale.
    145             return 1;
    146         }
    147 
    148         convertToConvexArray(mLeftBorder, 1, topY, bottomY);
    149         convertToConvexArray(mRightBorder, -1, topY, bottomY);
    150 
    151         // Area of the convex hull
    152         float area = 0;
    153         for (int y = 0; y < height; y++) {
    154             if (mLeftBorder[y] <= -1) {
    155                 continue;
    156             }
    157             area += mRightBorder[y] - mLeftBorder[y] + 1;
    158         }
    159 
    160         // Area of the rectangle required to fit the convex hull
    161         float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
    162         float hullByRect = area / rectArea;
    163 
    164         float scaleRequired;
    165         if (hullByRect < CIRCLE_AREA_BY_RECT) {
    166             scaleRequired = MAX_CIRCLE_AREA_FACTOR;
    167         } else {
    168             scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1  - hullByRect);
    169         }
    170 
    171         float areaScale = area / (width * height);
    172         // Use sqrt of the final ratio as the images is scaled across both width and height.
    173         float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
    174         return scale;
    175     }
    176 
    177     /**
    178      * Modifies {@param xCordinates} to represent a convex border. Fills in all missing values
    179      * (except on either ends) with appropriate values.
    180      * @param xCordinates map of x coordinate per y.
    181      * @param direction 1 for left border and -1 for right border.
    182      * @param topY the first Y position (inclusive) with a valid value.
    183      * @param bottomY the last Y position (inclusive) with a valid value.
    184      */
    185     private static void convertToConvexArray(
    186             float[] xCordinates, int direction, int topY, int bottomY) {
    187         int total = xCordinates.length;
    188         // The tangent at each pixel.
    189         float[] angles = new float[total - 1];
    190 
    191         int first = topY; // First valid y coordinate
    192         int last = -1;    // Last valid y coordinate which didn't have a missing value
    193 
    194         float lastAngle = Float.MAX_VALUE;
    195 
    196         for (int i = topY + 1; i <= bottomY; i++) {
    197             if (xCordinates[i] <= -1) {
    198                 continue;
    199             }
    200             int start;
    201 
    202             if (lastAngle == Float.MAX_VALUE) {
    203                 start = first;
    204             } else {
    205                 float currentAngle = (xCordinates[i] - xCordinates[last]) / (i - last);
    206                 start = last;
    207                 // If this position creates a concave angle, keep moving up until we find a
    208                 // position which creates a convex angle.
    209                 if ((currentAngle - lastAngle) * direction < 0) {
    210                     while (start > first) {
    211                         start --;
    212                         currentAngle = (xCordinates[i] - xCordinates[start]) / (i - start);
    213                         if ((currentAngle - angles[start]) * direction >= 0) {
    214                             break;
    215                         }
    216                     }
    217                 }
    218             }
    219 
    220             // Reset from last check
    221             lastAngle = (xCordinates[i] - xCordinates[start]) / (i - start);
    222             // Update all the points from start.
    223             for (int j = start; j < i; j++) {
    224                 angles[j] = lastAngle;
    225                 xCordinates[j] = xCordinates[start] + lastAngle * (j - start);
    226             }
    227             last = i;
    228         }
    229     }
    230 
    231     public static IconNormalizer getInstance() {
    232         synchronized (LOCK) {
    233             if (sIconNormalizer == null) {
    234                 sIconNormalizer = new IconNormalizer();
    235             }
    236         }
    237         return sIconNormalizer;
    238     }
    239 }
    240