Loading app/src/main/java/foundation/e/blisslauncher/FixedScaleDrawable.java 0 → 100644 +55 −0 Original line number Diff line number Diff line 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; } } } app/src/main/java/foundation/e/blisslauncher/core/AdaptiveIconGenerator.java 0 → 100644 +242 −0 Original line number Diff line number Diff line 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 useWhiteBackground = true; 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; // Currently, it's set to true so a white background is used for all the icons. if(useWhiteBackground) { //backgroundColor = Color.WHITE; backgroundColor = Color.WHITE & 0x80FFFFFF; onExitLoop(); return; } 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; } } app/src/main/java/foundation/e/blisslauncher/core/AdaptiveIconProvider.java +24 −10 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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]; } Loading @@ -77,13 +83,18 @@ public class AdaptiveIconProvider { XmlResourceParser parser = null; if (iconId != 0) { 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"; Loading @@ -91,7 +102,6 @@ public class AdaptiveIconProvider { parser = assetManager.openXmlResourceParser(path); } catch (Exception e) { e.printStackTrace(); continue; } if (parser != null) { Loading @@ -99,7 +109,7 @@ public class AdaptiveIconProvider { } } } } }*/ int backgroundRes = -1, foregroundRes = -1; if (parser != null) { Loading Loading @@ -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); Loading @@ -160,7 +173,7 @@ public class AdaptiveIconProvider { packageName), theme); } catch (Resources.NotFoundException ignored) { } } }*/ } } Loading @@ -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); Loading @@ -179,7 +193,7 @@ public class AdaptiveIconProvider { packageName), theme); } catch (Resources.NotFoundException ignored) { } } }*/ } } } catch (Exception e) { Loading app/src/main/java/foundation/e/blisslauncher/core/ColorExtractor.java 0 → 100644 +178 −0 Original line number Diff line number Diff line 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; } } app/src/main/java/foundation/e/blisslauncher/core/IconsHandler.java +5 −4 Original line number Diff line number Diff line Loading @@ -184,14 +184,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(); } } Loading Loading @@ -317,8 +319,7 @@ public class IconsHandler { } public Drawable convertIcon(Drawable icon) { return graphicsUtil.convertToRoundedCorner(ctx, graphicsUtil.addBackground(icon, false)); return new AdaptiveIconGenerator(ctx, icon).getResult(); } /** Loading Loading
app/src/main/java/foundation/e/blisslauncher/FixedScaleDrawable.java 0 → 100644 +55 −0 Original line number Diff line number Diff line 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; } } }
app/src/main/java/foundation/e/blisslauncher/core/AdaptiveIconGenerator.java 0 → 100644 +242 −0 Original line number Diff line number Diff line 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 useWhiteBackground = true; 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; // Currently, it's set to true so a white background is used for all the icons. if(useWhiteBackground) { //backgroundColor = Color.WHITE; backgroundColor = Color.WHITE & 0x80FFFFFF; onExitLoop(); return; } 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; } }
app/src/main/java/foundation/e/blisslauncher/core/AdaptiveIconProvider.java +24 −10 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -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]; } Loading @@ -77,13 +83,18 @@ public class AdaptiveIconProvider { XmlResourceParser parser = null; if (iconId != 0) { 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"; Loading @@ -91,7 +102,6 @@ public class AdaptiveIconProvider { parser = assetManager.openXmlResourceParser(path); } catch (Exception e) { e.printStackTrace(); continue; } if (parser != null) { Loading @@ -99,7 +109,7 @@ public class AdaptiveIconProvider { } } } } }*/ int backgroundRes = -1, foregroundRes = -1; if (parser != null) { Loading Loading @@ -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); Loading @@ -160,7 +173,7 @@ public class AdaptiveIconProvider { packageName), theme); } catch (Resources.NotFoundException ignored) { } } }*/ } } Loading @@ -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); Loading @@ -179,7 +193,7 @@ public class AdaptiveIconProvider { packageName), theme); } catch (Resources.NotFoundException ignored) { } } }*/ } } } catch (Exception e) { Loading
app/src/main/java/foundation/e/blisslauncher/core/ColorExtractor.java 0 → 100644 +178 −0 Original line number Diff line number Diff line 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; } }
app/src/main/java/foundation/e/blisslauncher/core/IconsHandler.java +5 −4 Original line number Diff line number Diff line Loading @@ -184,14 +184,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(); } } Loading Loading @@ -317,8 +319,7 @@ public class IconsHandler { } public Drawable convertIcon(Drawable icon) { return graphicsUtil.convertToRoundedCorner(ctx, graphicsUtil.addBackground(icon, false)); return new AdaptiveIconGenerator(ctx, icon).getResult(); } /** Loading