Commit d810366d authored by Amit Kumar's avatar Amit Kumar 💻

Add AdaptiveIconGenerator with ColorExtractor to match icon looks

parent 5bf90d15
Pipeline #53543 passed with stage
in 5 minutes and 48 seconds
package foundation.e.blisslauncher;
import android.annotation.TargetApi;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.DrawableWrapper;
import android.os.Build;
import android.util.AttributeSet;
import org.xmlpull.v1.XmlPullParser;
/**
* Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
*/
@TargetApi(Build.VERSION_CODES.N)
public class FixedScaleDrawable extends DrawableWrapper {
// TODO b/33553066 use the constant defined in MaskableIconDrawable
public static final float LEGACY_ICON_SCALE = .7f * .6667f;
private float mScaleX, mScaleY;
public FixedScaleDrawable() {
super(new ColorDrawable());
mScaleX = LEGACY_ICON_SCALE;
mScaleY = LEGACY_ICON_SCALE;
}
@Override
public void draw(Canvas canvas) {
int saveCount = canvas.save();
canvas.scale(mScaleX, mScaleY,
getBounds().exactCenterX(), getBounds().exactCenterY());
super.draw(canvas);
canvas.restoreToCount(saveCount);
}
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
@Override
public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Resources.Theme theme) { }
public void setScale(float scale) {
float h = getIntrinsicHeight();
float w = getIntrinsicWidth();
mScaleX = scale * LEGACY_ICON_SCALE;
mScaleY = scale * LEGACY_ICON_SCALE;
if (h > w && w > 0) {
mScaleX *= w / h;
} else if (w > h && h > 0) {
mScaleY *= h / w;
}
}
}
package foundation.e.blisslauncher.core;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.RectF;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import androidx.core.graphics.ColorUtils;
import foundation.e.blisslauncher.FixedScaleDrawable;
import foundation.e.blisslauncher.core.customviews.AdaptiveIconDrawableCompat;
import static java.lang.Math.max;
import static java.lang.Math.min;
import static java.lang.Math.round;
public class AdaptiveIconGenerator {
// Average number of derived colors (based on averages with ~100 icons and performance testing)
private static final int NUMBER_OF_COLORS_GUESSTIMATE = 45;
// Found after some experimenting, might be improved with some more testing
private static final float FULL_BLEED_ICON_SCALE = 1.44f;
// Found after some experimenting, might be improved with some more testing
private static final float NO_MIXIN_ICON_SCALE = 1.40f;
// Icons with less than 5 colors are considered as "single color"
private static final int SINGLE_COLOR_LIMIT = 5;
// Minimal alpha to be considered opaque
private static final int MIN_VISIBLE_ALPHA = 0xEF;
private Context context;
private Drawable icon;
private boolean ranLoop;
private boolean shouldWrap;
private int backgroundColor = Color.WHITE;
private boolean isFullBleed;
private boolean noMixinNeeded;
private boolean fullBleedChecked;
private boolean matchesMaskShape;
private boolean isBackgroundWhite;
private float scale;
private int height;
private float aHeight;
private int width;
private float aWidth;
private Drawable result;
public AdaptiveIconGenerator(Context context, @NonNull Drawable icon) {
this.context = context;
this.icon = icon;
}
private void loop() {
Drawable extractee = icon;
if (extractee == null) {
Log.e("AdaptiveIconGenerator", "extractee is null, skipping.");
onExitLoop();
return;
}
RectF bounds = new RectF();
scale = 1.0f;
if (extractee instanceof ColorDrawable) {
isFullBleed = true;
fullBleedChecked = true;
}
width = extractee.getIntrinsicWidth();
height = extractee.getIntrinsicHeight();
aWidth = width * (1 - (bounds.left + bounds.right));
aHeight = height * (1 - (bounds.top + bounds.bottom));
// Check if the icon is squarish
final float ratio = aHeight / aWidth;
boolean isSquarish = 0.999 < ratio && ratio < 1.0001;
boolean almostSquarish = isSquarish || (0.97 < ratio && ratio < 1.005);
if (!isSquarish) {
isFullBleed = false;
fullBleedChecked = true;
}
final Bitmap bitmap = Utilities.drawableToBitmap(extractee);
if (bitmap == null) {
onExitLoop();
return;
}
if (!bitmap.hasAlpha()) {
isFullBleed = true;
fullBleedChecked = true;
}
final int size = height * width;
SparseIntArray rgbScoreHistogram = new SparseIntArray(NUMBER_OF_COLORS_GUESSTIMATE);
final int[] pixels = new int[size];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
/*
* Calculate the number of padding pixels around the actual icon (i)
* +----------------+
* | top |
* +---+--------+---+
* | | | |
* | l | i | r |
* | | | |
* +---+--------+---+
* | bottom |
* +----------------+
*/
float adjHeight = height - bounds.top - bounds.bottom;
float l = bounds.left * width * adjHeight;
float top = bounds.top * height * width;
float r = bounds.right * width * adjHeight;
float bottom = bounds.bottom * height * width;
int addPixels = round(l + top + r + bottom);
// Any icon with less than 10% transparent pixels (padding excluded) is considered "full-bleed-ish"
final int maxTransparent = (int) (round(size * .10) + addPixels);
// Any icon with less than 27% transparent pixels (padding excluded) doesn't need a color mix-in
final int noMixinScore = (int) (round(size * .27) + addPixels);
int highScore = 0;
int bestRGB = 0;
int transparentScore = 0;
for (int pixel : pixels) {
int alpha = 0xFF & (pixel >> 24);
if (alpha < MIN_VISIBLE_ALPHA) {
// Drop mostly-transparent pixels.
transparentScore++;
if (transparentScore > maxTransparent) {
isFullBleed = false;
fullBleedChecked = true;
}
continue;
}
// Reduce color complexity.
int rgb = ColorExtractor.posterize(pixel);
if (rgb < 0) {
// Defensively avoid array bounds violations.
continue;
}
int currentScore = rgbScoreHistogram.get(rgb) + 1;
rgbScoreHistogram.append(rgb, currentScore);
if (currentScore > highScore) {
highScore = currentScore;
bestRGB = rgb;
}
}
// add back the alpha channel
bestRGB |= 0xff << 24;
// not yet checked = not set to false = has to be full bleed, isBackgroundWhite = true = is adaptive
isFullBleed |= !fullBleedChecked && !isBackgroundWhite;
// return early if a mix-in isnt needed
noMixinNeeded = !isFullBleed && !isBackgroundWhite && almostSquarish && transparentScore <= noMixinScore;
if (isFullBleed || noMixinNeeded) {
backgroundColor = bestRGB;
onExitLoop();
return;
}
// "single color"
final int numColors = rgbScoreHistogram.size();
boolean singleColor = numColors <= SINGLE_COLOR_LIMIT;
// Convert to HSL to get the lightness and adjust the color
final float[] hsl = new float[3];
ColorUtils.colorToHSL(bestRGB, hsl);
float lightness = hsl[2];
boolean light = lightness > .5;
// Apply dark background to mostly white icons
boolean veryLight = lightness > .75 && singleColor;
// Apply light background to mostly dark icons
boolean veryDark = lightness < .35 && singleColor;
// Adjust color to reach suitable contrast depending on the relationship between the colors
final int opaqueSize = size - transparentScore;
final float pxPerColor = opaqueSize / (float) numColors;
float mixRatio = min(max(pxPerColor / highScore, .15f), .7f);
// Vary color mix-in based on lightness and amount of colors
int fill = (light && !veryLight) || veryDark ? 0xFFFFFFFF : 0xFF333333;
backgroundColor = ColorUtils.blendARGB(bestRGB, fill, mixRatio);
onExitLoop();
}
private void onExitLoop() {
ranLoop = true;
result = genResult();
}
private Drawable genResult() {
AdaptiveIconDrawableCompat tmp = new AdaptiveIconDrawableCompat(
new ColorDrawable(),
new FixedScaleDrawable()
);
((FixedScaleDrawable) tmp.getForeground()).setDrawable(icon);
if (isFullBleed || noMixinNeeded) {
float scale;
if (noMixinNeeded) {
float upScale = min(width / aWidth, height / aHeight);
scale = NO_MIXIN_ICON_SCALE * upScale;
} else {
float upScale = max(width / aWidth, height / aHeight);
scale = FULL_BLEED_ICON_SCALE * upScale;
}
((FixedScaleDrawable) tmp.getForeground()).setScale(scale);
} else {
((FixedScaleDrawable) tmp.getForeground()).setScale(scale);
}
((ColorDrawable) tmp.getBackground()).setColor(backgroundColor);
return tmp;
}
public Drawable getResult() {
if (!ranLoop) {
loop();
}
return result;
}
}
......@@ -32,6 +32,7 @@ public class AdaptiveIconProvider {
"Loader.with(Context) must be called before loading an icon.");
}
PackageManager packageManager = context.getPackageManager();
Drawable background = null, foreground = null;
......@@ -53,15 +54,20 @@ public class AdaptiveIconProvider {
while ((eventType = manifestParser.nextToken()) != XmlPullParser.END_DOCUMENT) {
if (eventType == XmlPullParser.START_TAG && manifestParser.getName().equals(
matcher)) {
Log.d(TAG, "Manifest Parser Count: " + manifestParser.getAttributeCount());
for (int i = 0; i < manifestParser.getAttributeCount(); i++) {
Log.d(TAG, "Icon parser: " + manifestParser.getAttributeName(i));
if (manifestParser.getAttributeName(i).equalsIgnoreCase("icon")) {
iconId = Integer.parseInt(
manifestParser.getAttributeValue(i).substring(1));
Log.d(TAG, "Iconid:" + iconId);
break;
}
}
if (iconId != 0) {
iconName = resources.getResourceName(iconId);
Log.d("AdaptiveIcon", "Iconname: " + iconName);
if (iconName.contains("/")) {
iconName = iconName.split("/")[1];
}
......@@ -77,13 +83,18 @@ public class AdaptiveIconProvider {
XmlResourceParser parser = null;
if (iconId != 0) {
parser = resources.getXml(iconId);
try {
parser = resources.getXml(iconId);
} catch (Resources.NotFoundException e) {
e.printStackTrace();
parser = null;
}
}
for (int dir = 0; dir < IC_DIRS.length && parser == null; dir++) {
/*for (int dir = 0; dir < IC_DIRS.length && parser == null; dir++) {
for (int config = 0; config < IC_CONFIGS.length && parser == null; config++) {
for (String name : iconName != null && !iconName.equals("ic_launcher")
? new String[]{iconName, "ic_launcher"} : new String[]{"ic_launcher"}) {
for (String name : (iconName != null && !iconName.equals("ic_launcher"))
? new String[]{iconName, "ic_launcher", "ic_launcher_round"} : new String[]{"ic_launcher", "ic_launcher_round"}) {
try {
String path = "res/" + IC_DIRS[dir] + IC_CONFIGS[config] + "/" + name
+ ".xml";
......@@ -91,7 +102,6 @@ public class AdaptiveIconProvider {
parser = assetManager.openXmlResourceParser(path);
} catch (Exception e) {
e.printStackTrace();
continue;
}
if (parser != null) {
......@@ -99,7 +109,7 @@ public class AdaptiveIconProvider {
}
}
}
}
}*/
int backgroundRes = -1, foregroundRes = -1;
if (parser != null) {
......@@ -146,10 +156,13 @@ public class AdaptiveIconProvider {
}
if (backgroundRes != -1) {
Log.d(TAG, "BackgroundRes: " + backgroundRes);
Log.d(TAG, "BackgroundResName: " + resources.getResourceName(backgroundRes));
try {
background = ResourcesCompat.getDrawable(resources, backgroundRes, theme);
} catch (Resources.NotFoundException e) {
try {
e.printStackTrace();
/*try {
background = ResourcesCompat.getDrawable(resources,
resources.getIdentifier("ic_launcher_background", "mipmap",
packageName), theme);
......@@ -160,7 +173,7 @@ public class AdaptiveIconProvider {
packageName), theme);
} catch (Resources.NotFoundException ignored) {
}
}
}*/
}
}
......@@ -168,7 +181,8 @@ public class AdaptiveIconProvider {
try {
foreground = ResourcesCompat.getDrawable(resources, foregroundRes, theme);
} catch (Resources.NotFoundException e) {
try {
e.printStackTrace();
/*try {
foreground = ResourcesCompat.getDrawable(resources,
resources.getIdentifier("ic_launcher_foreground", "mipmap",
packageName), theme);
......@@ -179,7 +193,7 @@ public class AdaptiveIconProvider {
packageName), theme);
} catch (Resources.NotFoundException ignored) {
}
}
}*/
}
}
} catch (Exception e) {
......
package foundation.e.blisslauncher.core;
/*
* Copyright (C) 2017 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.
*/
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.util.SparseArray;
import java.util.HashSet;
import java.util.Set;
import kotlin.collections.ArraysKt;
/**
* Utility class for extracting colors from a bitmap.
*/
public class ColorExtractor {
private static final String TAG = "ColorExtractor";
public static int findDominantColorByHue(Bitmap bitmap) {
return findDominantColorByHue(bitmap, 20);
}
/**
* This picks a dominant color, looking for high-saturation, high-value, repeated hues.
*
* @param bitmap The bitmap to scan
* @param samples The approximate max number of samples to use.
*/
public static int findDominantColorByHue(Bitmap bitmap, int samples) {
final int height = bitmap.getHeight();
final int width = bitmap.getWidth();
int sampleStride = (int) Math.sqrt((height * width) / samples);
if (sampleStride < 1) {
sampleStride = 1;
}
// This is an out-param, for getting the hsv values for an rgb
float[] hsv = new float[3];
// First get the best hue, by creating a histogram over 360 hue buckets,
// where each pixel contributes a score weighted by saturation, value, and alpha.
float[] hueScoreHistogram = new float[360];
float highScore = -1;
int bestHue = -1;
int[] pixels = new int[samples];
int pixelCount = 0;
for (int y = 0; y < height; y += sampleStride) {
for (int x = 0; x < width; x += sampleStride) {
int argb = bitmap.getPixel(x, y);
int alpha = 0xFF & (argb >> 24);
if (alpha < 0x80) {
// Drop mostly-transparent pixels.
continue;
}
// Remove the alpha channel.
int rgb = argb | 0xFF000000;
Color.colorToHSV(rgb, hsv);
// Bucket colors by the 360 integer hues.
int hue = (int) hsv[0];
if (hue < 0 || hue >= hueScoreHistogram.length) {
// Defensively avoid array bounds violations.
continue;
}
if (pixelCount < samples) {
pixels[pixelCount++] = rgb;
}
float score = hsv[1] * hsv[2];
hueScoreHistogram[hue] += score;
if (hueScoreHistogram[hue] > highScore) {
highScore = hueScoreHistogram[hue];
bestHue = hue;
}
}
}
SparseArray<Float> rgbScores = new SparseArray<>();
int bestColor = 0xff000000;
highScore = -1;
// Go back over the RGB colors that match the winning hue,
// creating a histogram of weighted s*v scores, for up to 100*100 [s,v] buckets.
// The highest-scoring RGB color wins.
for (int i = 0; i < pixelCount; i++) {
int rgb = pixels[i];
Color.colorToHSV(rgb, hsv);
int hue = (int) hsv[0];
if (hue == bestHue) {
float s = hsv[1];
float v = hsv[2];
int bucket = (int) (s * 100) + (int) (v * 10000);
// Score by cumulative saturation * value.
float score = s * v;
Float oldTotal = rgbScores.get(bucket);
float newTotal = oldTotal == null ? score : oldTotal + score;
rgbScores.put(bucket, newTotal);
if (newTotal > highScore) {
highScore = newTotal;
// All the colors in the winning bucket are very similar. Last in wins.
bestColor = rgb;
}
}
}
return bestColor;
}
public static boolean isSingleColor(Drawable drawable, int color) {
if (drawable == null) return true;
final int testColor = posterize(color);
if (drawable instanceof ColorDrawable) {
return posterize(((ColorDrawable) drawable).getColor()) == testColor;
}
final Bitmap bitmap = Utilities.drawableToBitmap(drawable);
if (bitmap == null) {
return false;
}
final int height = bitmap.getHeight();
final int width = bitmap.getWidth();
int[] pixels = new int[height * width];
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
Set<Integer> set = new HashSet<>(ArraysKt.asList(pixels));
Integer[] distinctPixels = new Integer[set.size()];
set.toArray(distinctPixels);
for (int pixel : distinctPixels) {
if (testColor != posterize(pixel)) {
return false;
}
}
return true;
}
private static final int MAGIC_NUMBER = 25;
/*
* References:
* https://www.cs.umb.edu/~jreyes/csit114-fall-2007/project4/filters.html#posterize
* https://github.com/gitgraghu/image-processing/blob/master/src/Effects/Posterize.java
*/
public static int posterize(int rgb) {
int red = (0xff & (rgb >> 16));
int green = (0xff & (rgb >> 8));
int blue = (0xff & rgb);
red -= red % MAGIC_NUMBER;
green -= green % MAGIC_NUMBER;
blue -= blue % MAGIC_NUMBER;
if (red < 0) {
red = 0;
}
if (green < 0) {
green = 0;
}
if (blue < 0) {
blue = 0;
}
return red << 16 | green << 8 | blue;
}
}
......@@ -175,7 +175,8 @@ public class IconsHandler {
// Search first in cache
Drawable systemIcon = cacheGetDrawable(key);
if (systemIcon != null) {
if (systemIcon != null
&& !activityInfo.getApplicationInfo().packageName.equalsIgnoreCase("com.app.restclient")) {
return systemIcon;
}
......@@ -184,14 +185,16 @@ public class IconsHandler {
systemIcon = new AdaptiveIconDrawableCompat(
((AdaptiveIconDrawable) systemIcon).getBackground(),
((AdaptiveIconDrawable) systemIcon).getForeground());
return systemIcon;
} else {
// Icon is not adaptive, try to load using reflection.
Drawable adaptiveIcon = new AdaptiveIconProvider().load(ctx,
componentName.getPackageName());
if (adaptiveIcon != null) {
systemIcon = adaptiveIcon;
} else {
systemIcon = graphicsUtil.convertToRoundedCorner(ctx,
graphicsUtil.addBackground(systemIcon, false));
// Failed to load adaptive icon, Generate an adaptive icon from app default icon.
systemIcon = new AdaptiveIconGenerator(ctx, getDefaultAppDrawable(activityInfo, userHandle)).getResult();
}
}
......@@ -317,8 +320,7 @@ public class IconsHandler {
}
public Drawable convertIcon(Drawable icon) {
return graphicsUtil.convertToRoundedCorner(ctx,
graphicsUtil.addBackground(icon, false));
return new AdaptiveIconGenerator(ctx, icon).getResult();
}
/**
......
......@@ -153,7 +153,7 @@ public class AdaptiveIconDrawableCompat extends Drawable implements Drawable.Cal
mTransparentRegion = new Region();
}
@SuppressLint("PrivateApi")
@SuppressLint({"PrivateApi", "DiscouragedPrivateApi"})
private void initReflections() {
try {
Class<?> pathParser = getClass().getClassLoader().loadClass("android.util.PathParser");
......
......@@ -198,8 +198,6 @@ public class GraphicsUtil {
if (Color.alpha(bitmap.getPixel(x, y)) == 255) {
int color = bitmap.getPixel(x, y);
colors.put(color, (colors.containsKey(color) ? colors.get(color) : 0) + 1);
} else if ((Color.alpha(bitmap.getPixel(x, y)) < 0xF9)) {
count++;
}
}
}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment