• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.graphics;
18 
19 import android.content.Context;
20 import android.graphics.Bitmap;
21 import android.graphics.Canvas;
22 import android.graphics.Color;
23 import android.graphics.Matrix;
24 import android.graphics.Paint;
25 import android.graphics.Path;
26 import android.graphics.PorterDuff;
27 import android.graphics.PorterDuff.Mode;
28 import android.graphics.PorterDuffXfermode;
29 import android.graphics.Rect;
30 import android.graphics.RectF;
31 import android.graphics.drawable.AdaptiveIconDrawable;
32 import android.graphics.drawable.ColorDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.support.annotation.NonNull;
35 import android.support.annotation.Nullable;
36 import android.util.Log;
37 import com.android.launcher3.LauncherAppState;
38 import com.android.launcher3.Utilities;
39 import com.android.launcher3.dragndrop.FolderAdaptiveIcon;
40 
41 import java.nio.ByteBuffer;
42 
43 public class IconNormalizer {
44 
45     private static final String TAG = "IconNormalizer";
46     private static final boolean DEBUG = false;
47     // Ratio of icon visible area to full icon size for a square shaped icon
48     private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
49     // Ratio of icon visible area to full icon size for a circular shaped icon
50     private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
51 
52     private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
53 
54     // Slope used to calculate icon visible area to full icon size for any generic shaped icon.
55     private static final float LINEAR_SCALE_SLOPE =
56             (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
57 
58     private static final int MIN_VISIBLE_ALPHA = 40;
59 
60     // Shape detection related constants
61     private static final float BOUND_RATIO_MARGIN = .05f;
62     private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f;
63     private static final float SCALE_NOT_INITIALIZED = 0;
64 
65     // Ratio of the diameter of an normalized circular icon to the actual icon size.
66     public static final float ICON_VISIBLE_AREA_FACTOR = 0.92f;
67 
68     private final int mMaxSize;
69     private final Bitmap mBitmap;
70     private final Canvas mCanvas;
71     private final Paint mPaintMaskShape;
72     private final Paint mPaintMaskShapeOutline;
73     private final byte[] mPixels;
74 
75     private final Rect mAdaptiveIconBounds;
76     private float mAdaptiveIconScale;
77 
78     // for each y, stores the position of the leftmost x and the rightmost x
79     private final float[] mLeftBorder;
80     private final float[] mRightBorder;
81     private final Rect mBounds;
82     private final Path mShapePath;
83     private final Matrix mMatrix;
84 
85     /** package private **/
IconNormalizer(Context context)86     IconNormalizer(Context context) {
87         // Use twice the icon size as maximum size to avoid scaling down twice.
88         mMaxSize = LauncherAppState.getIDP(context).iconBitmapSize * 2;
89         mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
90         mCanvas = new Canvas(mBitmap);
91         mPixels = new byte[mMaxSize * mMaxSize];
92         mLeftBorder = new float[mMaxSize];
93         mRightBorder = new float[mMaxSize];
94         mBounds = new Rect();
95         mAdaptiveIconBounds = new Rect();
96 
97         mPaintMaskShape = new Paint();
98         mPaintMaskShape.setColor(Color.RED);
99         mPaintMaskShape.setStyle(Paint.Style.FILL);
100         mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
101 
102         mPaintMaskShapeOutline = new Paint();
103         mPaintMaskShapeOutline.setStrokeWidth(2 * context.getResources().getDisplayMetrics().density);
104         mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
105         mPaintMaskShapeOutline.setColor(Color.BLACK);
106         mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
107 
108         mShapePath = new Path();
109         mMatrix = new Matrix();
110         mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
111     }
112 
113     /**
114      * Returns if the shape of the icon is same as the path.
115      * For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds.
116      */
isShape(Path maskPath)117     private boolean isShape(Path maskPath) {
118         // Condition1:
119         // If width and height of the path not close to a square, then the icon shape is
120         // not same as the mask shape.
121         float iconRatio = ((float) mBounds.width()) / mBounds.height();
122         if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) {
123             if (DEBUG) {
124                 Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio);
125             }
126             return false;
127         }
128 
129         // Condition 2:
130         // Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
131         // should generate transparent image, if the actual icon is equivalent to the shape.
132 
133         // Fit the shape within the icon's bounding box
134         mMatrix.reset();
135         mMatrix.setScale(mBounds.width(), mBounds.height());
136         mMatrix.postTranslate(mBounds.left, mBounds.top);
137         maskPath.transform(mMatrix, mShapePath);
138 
139         // XOR operation
140         mCanvas.drawPath(mShapePath, mPaintMaskShape);
141 
142         // DST_OUT operation around the mask path outline
143         mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
144 
145         // Check if the result is almost transparent
146         return isTransparentBitmap();
147     }
148 
149     /**
150      * Used to determine if certain the bitmap is transparent.
151      */
isTransparentBitmap()152     private boolean isTransparentBitmap() {
153         ByteBuffer buffer = ByteBuffer.wrap(mPixels);
154         buffer.rewind();
155         mBitmap.copyPixelsToBuffer(buffer);
156 
157         int y = mBounds.top;
158         // buffer position
159         int index = y * mMaxSize;
160         // buffer shift after every row, width of buffer = mMaxSize
161         int rowSizeDiff = mMaxSize - mBounds.right;
162 
163         int sum = 0;
164         for (; y < mBounds.bottom; y++) {
165             index += mBounds.left;
166             for (int x = mBounds.left; x < mBounds.right; x++) {
167                 if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
168                     sum++;
169                 }
170                 index++;
171             }
172             index += rowSizeDiff;
173         }
174 
175         float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
176         return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
177     }
178 
179     /**
180      * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
181      * matches the design guidelines for a launcher icon.
182      *
183      * We first calculate the convex hull of the visible portion of the icon.
184      * This hull then compared with the bounding rectangle of the hull to find how closely it
185      * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an
186      * ideal solution but it gives satisfactory result without affecting the performance.
187      *
188      * This closeness is used to determine the ratio of hull area to the full icon size.
189      * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
190      *
191      * @param outBounds optional rect to receive the fraction distance from each edge.
192      */
getScale(@onNull Drawable d, @Nullable RectF outBounds, @Nullable Path path, @Nullable boolean[] outMaskShape)193     public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
194             @Nullable Path path, @Nullable boolean[] outMaskShape) {
195         if (Utilities.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
196             if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) {
197                 if (outBounds != null) {
198                     outBounds.set(mAdaptiveIconBounds);
199                 }
200                 return mAdaptiveIconScale;
201             }
202             if (d instanceof FolderAdaptiveIcon) {
203                 // Since we just want the scale, avoid heavy drawing operations
204                 d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK), null);
205             }
206         }
207         int width = d.getIntrinsicWidth();
208         int height = d.getIntrinsicHeight();
209         if (width <= 0 || height <= 0) {
210             width = width <= 0 || width > mMaxSize ? mMaxSize : width;
211             height = height <= 0 || height > mMaxSize ? mMaxSize : height;
212         } else if (width > mMaxSize || height > mMaxSize) {
213             int max = Math.max(width, height);
214             width = mMaxSize * width / max;
215             height = mMaxSize * height / max;
216         }
217 
218         mBitmap.eraseColor(Color.TRANSPARENT);
219         d.setBounds(0, 0, width, height);
220         d.draw(mCanvas);
221 
222         ByteBuffer buffer = ByteBuffer.wrap(mPixels);
223         buffer.rewind();
224         mBitmap.copyPixelsToBuffer(buffer);
225 
226         // Overall bounds of the visible icon.
227         int topY = -1;
228         int bottomY = -1;
229         int leftX = mMaxSize + 1;
230         int rightX = -1;
231 
232         // Create border by going through all pixels one row at a time and for each row find
233         // the first and the last non-transparent pixel. Set those values to mLeftBorder and
234         // mRightBorder and use -1 if there are no visible pixel in the row.
235 
236         // buffer position
237         int index = 0;
238         // buffer shift after every row, width of buffer = mMaxSize
239         int rowSizeDiff = mMaxSize - width;
240         // first and last position for any row.
241         int firstX, lastX;
242 
243         for (int y = 0; y < height; y++) {
244             firstX = lastX = -1;
245             for (int x = 0; x < width; x++) {
246                 if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
247                     if (firstX == -1) {
248                         firstX = x;
249                     }
250                     lastX = x;
251                 }
252                 index++;
253             }
254             index += rowSizeDiff;
255 
256             mLeftBorder[y] = firstX;
257             mRightBorder[y] = lastX;
258 
259             // If there is at least one visible pixel, update the overall bounds.
260             if (firstX != -1) {
261                 bottomY = y;
262                 if (topY == -1) {
263                     topY = y;
264                 }
265 
266                 leftX = Math.min(leftX, firstX);
267                 rightX = Math.max(rightX, lastX);
268             }
269         }
270 
271         if (topY == -1 || rightX == -1) {
272             // No valid pixels found. Do not scale.
273             return 1;
274         }
275 
276         convertToConvexArray(mLeftBorder, 1, topY, bottomY);
277         convertToConvexArray(mRightBorder, -1, topY, bottomY);
278 
279         // Area of the convex hull
280         float area = 0;
281         for (int y = 0; y < height; y++) {
282             if (mLeftBorder[y] <= -1) {
283                 continue;
284             }
285             area += mRightBorder[y] - mLeftBorder[y] + 1;
286         }
287 
288         // Area of the rectangle required to fit the convex hull
289         float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
290         float hullByRect = area / rectArea;
291 
292         float scaleRequired;
293         if (hullByRect < CIRCLE_AREA_BY_RECT) {
294             scaleRequired = MAX_CIRCLE_AREA_FACTOR;
295         } else {
296             scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
297         }
298         mBounds.left = leftX;
299         mBounds.right = rightX;
300 
301         mBounds.top = topY;
302         mBounds.bottom = bottomY;
303 
304         if (outBounds != null) {
305             outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
306                     1 - ((float) mBounds.right) / width,
307                     1 - ((float) mBounds.bottom) / height);
308         }
309 
310         if (outMaskShape != null && outMaskShape.length > 0) {
311             outMaskShape[0] = isShape(path);
312         }
313         float areaScale = area / (width * height);
314         // Use sqrt of the final ratio as the images is scaled across both width and height.
315         float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
316         if (Utilities.ATLEAST_OREO && d instanceof AdaptiveIconDrawable &&
317                 mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
318             mAdaptiveIconScale = scale;
319             mAdaptiveIconBounds.set(mBounds);
320         }
321         return scale;
322     }
323 
324     /**
325      * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
326      * (except on either ends) with appropriate values.
327      * @param xCoordinates map of x coordinate per y.
328      * @param direction 1 for left border and -1 for right border.
329      * @param topY the first Y position (inclusive) with a valid value.
330      * @param bottomY the last Y position (inclusive) with a valid value.
331      */
convertToConvexArray( float[] xCoordinates, int direction, int topY, int bottomY)332     private static void convertToConvexArray(
333             float[] xCoordinates, int direction, int topY, int bottomY) {
334         int total = xCoordinates.length;
335         // The tangent at each pixel.
336         float[] angles = new float[total - 1];
337 
338         int first = topY; // First valid y coordinate
339         int last = -1;    // Last valid y coordinate which didn't have a missing value
340 
341         float lastAngle = Float.MAX_VALUE;
342 
343         for (int i = topY + 1; i <= bottomY; i++) {
344             if (xCoordinates[i] <= -1) {
345                 continue;
346             }
347             int start;
348 
349             if (lastAngle == Float.MAX_VALUE) {
350                 start = first;
351             } else {
352                 float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
353                 start = last;
354                 // If this position creates a concave angle, keep moving up until we find a
355                 // position which creates a convex angle.
356                 if ((currentAngle - lastAngle) * direction < 0) {
357                     while (start > first) {
358                         start --;
359                         currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
360                         if ((currentAngle - angles[start]) * direction >= 0) {
361                             break;
362                         }
363                     }
364                 }
365             }
366 
367             // Reset from last check
368             lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
369             // Update all the points from start.
370             for (int j = start; j < i; j++) {
371                 angles[j] = lastAngle;
372                 xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
373             }
374             last = i;
375         }
376     }
377 
378     /**
379      * @return The diameter of the normalized circle that fits inside of the square (size x size).
380      */
getNormalizedCircleSize(int size)381     public static int getNormalizedCircleSize(int size) {
382         float area = size * size * MAX_CIRCLE_AREA_FACTOR;
383         return (int) Math.round(Math.sqrt((4 * area) / Math.PI));
384     }
385 }
386