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