Android12 launcher3修改App图标白边问题
1.前言:
今天在Android12 Rom定制客制化系统应用时发现改变系统App图标的形状会出现一个问题,那就是图标被缩小了,没有显示完整,有一个白边,这在普通的App开发很少遇到,在Android系统Rom定制时才会出现此问题,记录一下修改过程.
2.修改前的截图如下:
2.1 圆角图标:
可以看到App的图标没有铺满,中间的图标有的是方形,有的是圆角,而且出现一个白边,显示不正常
2.2 圆形图标:
发现切换圆形图标后问题也有问题,更难看,而且有个图标还被放大了,白边很突兀,明细不正常.
3.修改BaseIconFactory:
核心修改方法如下:
/**
* Switches badging to left/right
*/
public void setBadgeOnLeft(boolean badgeOnLeft) {
mBadgeOnLeft = badgeOnLeft;
}
/**
* Sets the background color used for wrapped adaptive icon
*/
public void setWrapperBackgroundColor(int color) {
mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
}
/**
* Disables the dominant color extraction for all icons loaded.
*/
public void disableColorExtraction() {
mDisableColorExtractor = true;
}
private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
if (icon == null) {
return null;
}
float scale = 1f;
/*
if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
if (mWrapperIcon == null) {
mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
.mutate();
}
AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
fsd.setDrawable(icon);
fsd.setScale(scale);
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
}
} else {
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
} */
scale =getNormalizer().getScale(icon, outIconBounds, null, null);
outScale[0] = scale;
return icon;
}
package com.android.launcher3.icons;
import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.InsetDrawable;
import android.os.Build;
import android.os.Process;
import android.os.UserHandle;
import androidx.annotation.NonNull;
import com.android.launcher3.icons.BitmapInfo.Extender;
/**
* This class will be moved to androidx library. There shouldn't be any dependency outside
* this package.
*/
public class BaseIconFactory implements AutoCloseable {
private static final String TAG = "BaseIconFactory";
private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O;
static final boolean ATLEAST_P = Build.VERSION.SDK_INT >= Build.VERSION_CODES.P;
private static final float ICON_BADGE_SCALE = 0.444f;
private final Rect mOldBounds = new Rect();
protected final Context mContext;
private final Canvas mCanvas;
private final PackageManager mPm;
private final ColorExtractor mColorExtractor;
private boolean mDisableColorExtractor;
private boolean mBadgeOnLeft = false;
protected final int mFillResIconDpi;
protected final int mIconBitmapSize;
private IconNormalizer mNormalizer;
private ShadowGenerator mShadowGenerator;
private final boolean mShapeDetection;
private Drawable mWrapperIcon;
private int mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
private Bitmap mUserBadgeBitmap;
private final Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private static final float PLACEHOLDER_TEXT_SIZE = 20f;
private static int PLACEHOLDER_BACKGROUND_COLOR = Color.rgb(240, 240, 240);
protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
boolean shapeDetection) {
mContext = context.getApplicationContext();
mShapeDetection = shapeDetection;
mFillResIconDpi = fillResIconDpi;
mIconBitmapSize = iconBitmapSize;
mPm = mContext.getPackageManager();
mColorExtractor = new ColorExtractor();
mCanvas = new Canvas();
mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
mTextPaint.setTextAlign(Paint.Align.CENTER);
mTextPaint.setColor(PLACEHOLDER_BACKGROUND_COLOR);
mTextPaint.setTextSize(context.getResources().getDisplayMetrics().density *
PLACEHOLDER_TEXT_SIZE);
clear();
}
public BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) {
this(context, fillResIconDpi, iconBitmapSize, false);
}
protected void clear() {
mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND;
mDisableColorExtractor = false;
mBadgeOnLeft = false;
}
public ShadowGenerator getShadowGenerator() {
if (mShadowGenerator == null) {
mShadowGenerator = new ShadowGenerator(mIconBitmapSize);
}
return mShadowGenerator;
}
public IconNormalizer getNormalizer() {
if (mNormalizer == null) {
mNormalizer = new IconNormalizer(mContext, mIconBitmapSize, mShapeDetection);
}
return mNormalizer;
}
@SuppressWarnings("deprecation")
public BitmapInfo createIconBitmap(Intent.ShortcutIconResource iconRes) {
try {
Resources resources = mPm.getResourcesForApplication(iconRes.packageName);
if (resources != null) {
final int id = resources.getIdentifier(iconRes.resourceName, null, null);
// do not stamp old legacy shortcuts as the app may have already forgotten about it
return createBadgedIconBitmap(
resources.getDrawableForDensity(id, mFillResIconDpi),
Process.myUserHandle() /* only available on primary user */,
false /* do not apply legacy treatment */);
}
} catch (Exception e) {
// Icon not found.
}
return null;
}
/**
* Create a placeholder icon using the passed in text.
*
* @param placeholder used for foreground element in the icon bitmap
* @param color used for the foreground text color
* @return
*/
public BitmapInfo createIconBitmap(String placeholder, int color) {
if (!ATLEAST_OREO) return null;
Bitmap placeholderBitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize,
Bitmap.Config.ARGB_8888);
mTextPaint.setColor(color);
Canvas canvas = new Canvas(placeholderBitmap);
canvas.drawText(placeholder, mIconBitmapSize / 2, mIconBitmapSize * 5 / 8, mTextPaint);
AdaptiveIconDrawable drawable = new AdaptiveIconDrawable(
new ColorDrawable(PLACEHOLDER_BACKGROUND_COLOR),
new BitmapDrawable(mContext.getResources(), placeholderBitmap));
Bitmap icon = createIconBitmap(drawable, 1f);
return BitmapInfo.of(icon, extractColor(icon));
}
public BitmapInfo createIconBitmap(Bitmap icon) {
if (mIconBitmapSize != icon.getWidth() || mIconBitmapSize != icon.getHeight()) {
icon = createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f);
}
return BitmapInfo.of(icon, extractColor(icon));
}
/**
* Creates an icon from the bitmap cropped to the current device icon shape
*/
public BitmapInfo createShapedIconBitmap(Bitmap icon, UserHandle user) {
Drawable d = new FixedSizeBitmapDrawable(icon);
if (ATLEAST_OREO) {
float inset = AdaptiveIconDrawable.getExtraInsetFraction();
inset = inset / (1 + 2 * inset);
d = new AdaptiveIconDrawable(new ColorDrawable(Color.BLACK),
new InsetDrawable(d, inset, inset, inset, inset));
}
return createBadgedIconBitmap(d, user, true);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
boolean shrinkNonAdaptiveIcons) {
return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
int iconAppTargetSdk) {
return createBadgedIconBitmap(icon, user, iconAppTargetSdk, false);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
int iconAppTargetSdk, boolean isInstantApp) {
return createBadgedIconBitmap(icon, user, iconAppTargetSdk, isInstantApp, null);
}
public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user,
int iconAppTargetSdk, boolean isInstantApp, float[] scale) {
boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
(ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, scale);
}
public Bitmap createScaledBitmapWithoutShadow(Drawable icon, int iconAppTargetSdk) {
boolean shrinkNonAdaptiveIcons = ATLEAST_P ||
(ATLEAST_OREO && iconAppTargetSdk >= Build.VERSION_CODES.O);
return createScaledBitmapWithoutShadow(icon, shrinkNonAdaptiveIcons);
}
/**
* Creates bitmap using the source drawable and various parameters.
* The bitmap is visually normalized with other icons and has enough spacing to add shadow.
*
* @param icon source of the icon
* @param user info can be used for a badge
* @param shrinkNonAdaptiveIcons {@code true} if non adaptive icons should be treated
* @param isInstantApp info can be used for a badge
* @param scale returns the scale result from normalization
* @return a bitmap suitable for disaplaying as an icon at various system UIs.
*/
public BitmapInfo createBadgedIconBitmap(@NonNull Drawable icon, UserHandle user,
boolean shrinkNonAdaptiveIcons, boolean isInstantApp, float[] scale) {
if (scale == null) {
scale = new float[1];
}
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, null, scale);
Bitmap bitmap = createIconBitmap(icon, scale[0]);
if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
mCanvas.setBitmap(bitmap);
getShadowGenerator().recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
mCanvas.setBitmap(null);
}
if (isInstantApp) {
badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge));
}
if (user != null) {
BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
Drawable badged = mPm.getUserBadgedIcon(drawable, user);
if (badged instanceof BitmapDrawable) {
bitmap = ((BitmapDrawable) badged).getBitmap();
} else {
bitmap = createIconBitmap(badged, 1f);
}
}
int color = extractColor(bitmap);
return icon instanceof BitmapInfo.Extender
? ((BitmapInfo.Extender) icon).getExtendedInfo(bitmap, color, this, scale[0], user)
: BitmapInfo.of(bitmap, color);
}
public Bitmap getUserBadgeBitmap(UserHandle user) {
if (mUserBadgeBitmap == null) {
Bitmap bitmap = Bitmap.createBitmap(
mIconBitmapSize, mIconBitmapSize, Bitmap.Config.ARGB_8888);
Drawable badgedDrawable = mPm.getUserBadgedIcon(
new FixedSizeBitmapDrawable(bitmap), user);
if (badgedDrawable instanceof BitmapDrawable) {
mUserBadgeBitmap = ((BitmapDrawable) badgedDrawable).getBitmap();
} else {
badgedDrawable.setBounds(0, 0, mIconBitmapSize, mIconBitmapSize);
mUserBadgeBitmap = BitmapRenderer.createSoftwareBitmap(
mIconBitmapSize, mIconBitmapSize, badgedDrawable::draw);
}
}
return mUserBadgeBitmap;
}
public Bitmap createScaledBitmapWithoutShadow(Drawable icon, boolean shrinkNonAdaptiveIcons) {
RectF iconBounds = new RectF();
float[] scale = new float[1];
icon = normalizeAndWrapToAdaptiveIcon(icon, shrinkNonAdaptiveIcons, iconBounds, scale);
return createIconBitmap(icon,
Math.min(scale[0], ShadowGenerator.getScaleForBounds(iconBounds)));
}
/**
* Switches badging to left/right
*/
public void setBadgeOnLeft(boolean badgeOnLeft) {
mBadgeOnLeft = badgeOnLeft;
}
/**
* Sets the background color used for wrapped adaptive icon
*/
public void setWrapperBackgroundColor(int color) {
mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
}
/**
* Disables the dominant color extraction for all icons loaded.
*/
public void disableColorExtraction() {
mDisableColorExtractor = true;
}
private Drawable normalizeAndWrapToAdaptiveIcon(@NonNull Drawable icon,
boolean shrinkNonAdaptiveIcons, RectF outIconBounds, float[] outScale) {
if (icon == null) {
return null;
}
float scale = 1f;
/*
if (shrinkNonAdaptiveIcons && ATLEAST_OREO) {
if (mWrapperIcon == null) {
mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper)
.mutate();
}
AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
dr.setBounds(0, 0, 1, 1);
boolean[] outShape = new boolean[1];
scale = getNormalizer().getScale(icon, outIconBounds, dr.getIconMask(), outShape);
if (!(icon instanceof AdaptiveIconDrawable) && !outShape[0]) {
FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
fsd.setDrawable(icon);
fsd.setScale(scale);
icon = dr;
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
}
} else {
scale = getNormalizer().getScale(icon, outIconBounds, null, null);
} */
scale =getNormalizer().getScale(icon, outIconBounds, null, null);
outScale[0] = scale;
return icon;
}
/**
* Adds the {@param badge} on top of {@param target} using the badge dimensions.
*/
public void badgeWithDrawable(Bitmap target, Drawable badge) {
mCanvas.setBitmap(target);
badgeWithDrawable(mCanvas, badge);
mCanvas.setBitmap(null);
}
/**
* Adds the {@param badge} on top of {@param target} using the badge dimensions.
*/
public void badgeWithDrawable(Canvas target, Drawable badge) {
int badgeSize = getBadgeSizeForIconSize(mIconBitmapSize);
if (mBadgeOnLeft) {
badge.setBounds(0, mIconBitmapSize - badgeSize, badgeSize, mIconBitmapSize);
} else {
badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize,
mIconBitmapSize, mIconBitmapSize);
}
badge.draw(target);
}
private Bitmap createIconBitmap(Drawable icon, float scale) {
return createIconBitmap(icon, scale, mIconBitmapSize);
}
/**
* @param icon drawable that should be flattened to a bitmap
* @param scale the scale to apply before drawing {@param icon} on the canvas
*/
public Bitmap createIconBitmap(@NonNull Drawable icon, float scale, int size) {
Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
if (icon == null) {
return bitmap;
}
mCanvas.setBitmap(bitmap);
mOldBounds.set(icon.getBounds());
if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) {
int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size),
Math.round(size * (1 - scale) / 2 ));
icon.setBounds(offset, offset, size - offset, size - offset);
if (icon instanceof BitmapInfo.Extender) {
((Extender) icon).drawForPersistence(mCanvas);
} else {
icon.draw(mCanvas);
}
} else {
if (icon instanceof BitmapDrawable) {
BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
Bitmap b = bitmapDrawable.getBitmap();
if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
}
}
int width = size;
int height = size;
int intrinsicWidth = icon.getIntrinsicWidth();
int intrinsicHeight = icon.getIntrinsicHeight();
if (intrinsicWidth > 0 && intrinsicHeight > 0) {
// Scale the icon proportionally to the icon dimensions
final float ratio = (float) intrinsicWidth / intrinsicHeight;
if (intrinsicWidth > intrinsicHeight) {
height = (int) (width / ratio);
} else if (intrinsicHeight > intrinsicWidth) {
width = (int) (height * ratio);
}
}
final int left = (size - width) / 2;
final int top = (size - height) / 2;
icon.setBounds(left, top, left + width, top + height);
mCanvas.save();
mCanvas.scale(scale, scale, size / 2, size / 2);
icon.draw(mCanvas);
mCanvas.restore();
}
icon.setBounds(mOldBounds);
mCanvas.setBitmap(null);
return bitmap;
}
@Override
public void close() {
clear();
}
public BitmapInfo makeDefaultIcon(UserHandle user) {
return createBadgedIconBitmap(getFullResDefaultActivityIcon(mFillResIconDpi),
user, Build.VERSION.SDK_INT);
}
public static Drawable getFullResDefaultActivityIcon(int iconDpi) {
return Resources.getSystem().getDrawableForDensity(
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
? android.R.drawable.sym_def_app_icon : android.R.mipmap.sym_def_app_icon,
iconDpi);
}
/**
* Badges the provided source with the badge info
*/
public BitmapInfo badgeBitmap(Bitmap source, BitmapInfo badgeInfo) {
Bitmap icon = BitmapRenderer.createHardwareBitmap(mIconBitmapSize, mIconBitmapSize, (c) -> {
getShadowGenerator().recreateIcon(source, c);
badgeWithDrawable(c, new FixedSizeBitmapDrawable(badgeInfo.icon));
});
return BitmapInfo.of(icon, badgeInfo.color);
}
private int extractColor(Bitmap bitmap) {
return mDisableColorExtractor ? 0 : mColorExtractor.findDominantColorByHue(bitmap);
}
/**
* Returns the correct badge size given an icon size
*/
public static int getBadgeSizeForIconSize(int iconSize) {
return (int) (ICON_BADGE_SCALE * iconSize);
}
/**
* An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
* This allows the badging to be done based on the action bitmap size rather than
* the scaled bitmap size.
*/
private static class FixedSizeBitmapDrawable extends BitmapDrawable {
public FixedSizeBitmapDrawable(Bitmap bitmap) {
super(null, bitmap);
}
@Override
public int getIntrinsicHeight() {
return getBitmap().getWidth();
}
@Override
public int getIntrinsicWidth() {
return getBitmap().getWidth();
}
}
}
4.修改FixedScaleDrawable :
核心修改如下:
private static final float LEGACY_ICON_SCALE = 1.0f;
package com.android.launcher3.icons;
import android.content.res.Resources;
import android.content.res.Resources.Theme;
import android.graphics.Canvas;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.DrawableWrapper;
import android.util.AttributeSet;
import org.xmlpull.v1.XmlPullParser;
/**
* Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
*/
public class FixedScaleDrawable extends DrawableWrapper {
// TODO b/33553066 use the constant defined in MaskableIconDrawable
private static final float LEGACY_ICON_SCALE = 1.0f;
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, 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;
}
}
}
5.修改IconNormalizer:
核心修改方法如下:
private static float getScale(float hullArea, float boundingArea, float fullArea) {
float hullByRect = hullArea / boundingArea;
float scaleRequired;
if (hullByRect < CIRCLE_AREA_BY_RECT) {
scaleRequired = MAX_CIRCLE_AREA_FACTOR;
} else {
scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
}
float areaScale = hullArea / fullArea;
// Use sqrt of the final ratio as the images is scaled across both width and height.
float scale = (float) Math.sqrt(scaleRequired / areaScale);
return scale < 1f ? scale : 1f;
}
/*
* Copyright (C) 2015 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.
*/
package com.android.launcher3.icons;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Region;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.util.Log;
import java.nio.ByteBuffer;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
public class IconNormalizer {
private static final String TAG = "IconNormalizer";
private static final boolean DEBUG = false;
// Ratio of icon visible area to full icon size for a square shaped icon
private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
// Ratio of icon visible area to full icon size for a circular shaped icon
private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
// Slope used to calculate icon visible area to full icon size for any generic shaped icon.
private static final float LINEAR_SCALE_SLOPE =
(MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
private static final int MIN_VISIBLE_ALPHA = 40;
// Shape detection related constants
private static final float BOUND_RATIO_MARGIN = .05f;
private static final float PIXEL_DIFF_PERCENTAGE_THRESHOLD = 0.005f;
private static final float SCALE_NOT_INITIALIZED = 0;
// Ratio of the diameter of an normalized circular icon to the actual icon size.
public static final float ICON_VISIBLE_AREA_FACTOR = 0.92f;
private final int mMaxSize;
private final Bitmap mBitmap;
private final Canvas mCanvas;
private final Paint mPaintMaskShape;
private final Paint mPaintMaskShapeOutline;
private final byte[] mPixels;
private final RectF mAdaptiveIconBounds;
private float mAdaptiveIconScale;
private boolean mEnableShapeDetection;
// for each y, stores the position of the leftmost x and the rightmost x
private final float[] mLeftBorder;
private final float[] mRightBorder;
private final Rect mBounds;
private final Path mShapePath;
private final Matrix mMatrix;
/** package private **/
IconNormalizer(Context context, int iconBitmapSize, boolean shapeDetection) {
// Use twice the icon size as maximum size to avoid scaling down twice.
mMaxSize = iconBitmapSize * 2;
mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
mCanvas = new Canvas(mBitmap);
mPixels = new byte[mMaxSize * mMaxSize];
mLeftBorder = new float[mMaxSize];
mRightBorder = new float[mMaxSize];
mBounds = new Rect();
mAdaptiveIconBounds = new RectF();
mPaintMaskShape = new Paint();
mPaintMaskShape.setColor(Color.RED);
mPaintMaskShape.setStyle(Paint.Style.FILL);
mPaintMaskShape.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.XOR));
mPaintMaskShapeOutline = new Paint();
mPaintMaskShapeOutline.setStrokeWidth(
2 * context.getResources().getDisplayMetrics().density);
mPaintMaskShapeOutline.setStyle(Paint.Style.STROKE);
mPaintMaskShapeOutline.setColor(Color.BLACK);
mPaintMaskShapeOutline.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
mShapePath = new Path();
mMatrix = new Matrix();
mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
mEnableShapeDetection = shapeDetection;
}
private static float getScale(float hullArea, float boundingArea, float fullArea) {
float hullByRect = hullArea / boundingArea;
float scaleRequired;
if (hullByRect < CIRCLE_AREA_BY_RECT) {
scaleRequired = MAX_CIRCLE_AREA_FACTOR;
} else {
scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
}
float areaScale = hullArea / fullArea;
// Use sqrt of the final ratio as the images is scaled across both width and height.
float scale = (float) Math.sqrt(scaleRequired / areaScale);
return scale < 1f ? scale : 1f;
}
/**
* @param d Should be AdaptiveIconDrawable
* @param size Canvas size to use
*/
@TargetApi(Build.VERSION_CODES.O)
public static float normalizeAdaptiveIcon(Drawable d, int size, @Nullable RectF outBounds) {
Rect tmpBounds = new Rect(d.getBounds());
d.setBounds(0, 0, size, size);
Path path = ((AdaptiveIconDrawable) d).getIconMask();
Region region = new Region();
region.setPath(path, new Region(0, 0, size, size));
Rect hullBounds = region.getBounds();
int hullArea = GraphicsUtils.getArea(region);
if (outBounds != null) {
float sizeF = size;
outBounds.set(
hullBounds.left / sizeF,
hullBounds.top / sizeF,
1 - (hullBounds.right / sizeF),
1 - (hullBounds.bottom / sizeF));
}
d.setBounds(tmpBounds);
return getScale(hullArea, hullArea, size * size);
}
/**
* Returns if the shape of the icon is same as the path.
* For this method to work, the shape path bounds should be in [0,1]x[0,1] bounds.
*/
private boolean isShape(Path maskPath) {
// Condition1:
// If width and height of the path not close to a square, then the icon shape is
// not same as the mask shape.
float iconRatio = ((float) mBounds.width()) / mBounds.height();
if (Math.abs(iconRatio - 1) > BOUND_RATIO_MARGIN) {
if (DEBUG) {
Log.d(TAG, "Not same as mask shape because width != height. " + iconRatio);
}
return false;
}
// Condition 2:
// Actual icon (white) and the fitted shape (e.g., circle)(red) XOR operation
// should generate transparent image, if the actual icon is equivalent to the shape.
// Fit the shape within the icon's bounding box
mMatrix.reset();
mMatrix.setScale(mBounds.width(), mBounds.height());
mMatrix.postTranslate(mBounds.left, mBounds.top);
maskPath.transform(mMatrix, mShapePath);
// XOR operation
mCanvas.drawPath(mShapePath, mPaintMaskShape);
// DST_OUT operation around the mask path outline
mCanvas.drawPath(mShapePath, mPaintMaskShapeOutline);
// Check if the result is almost transparent
return isTransparentBitmap();
}
/**
* Used to determine if certain the bitmap is transparent.
*/
private boolean isTransparentBitmap() {
ByteBuffer buffer = ByteBuffer.wrap(mPixels);
buffer.rewind();
mBitmap.copyPixelsToBuffer(buffer);
int y = mBounds.top;
// buffer position
int index = y * mMaxSize;
// buffer shift after every row, width of buffer = mMaxSize
int rowSizeDiff = mMaxSize - mBounds.right;
int sum = 0;
for (; y < mBounds.bottom; y++) {
index += mBounds.left;
for (int x = mBounds.left; x < mBounds.right; x++) {
if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
sum++;
}
index++;
}
index += rowSizeDiff;
}
float percentageDiffPixels = ((float) sum) / (mBounds.width() * mBounds.height());
return percentageDiffPixels < PIXEL_DIFF_PERCENTAGE_THRESHOLD;
}
/**
* Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
* matches the design guidelines for a launcher icon.
*
* We first calculate the convex hull of the visible portion of the icon.
* This hull then compared with the bounding rectangle of the hull to find how closely it
* resembles a circle and a square, by comparing the ratio of the areas. Note that this is not an
* ideal solution but it gives satisfactory result without affecting the performance.
*
* This closeness is used to determine the ratio of hull area to the full icon size.
* Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
*
* @param outBounds optional rect to receive the fraction distance from each edge.
*/
public synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds,
@Nullable Path path, @Nullable boolean[] outMaskShape) {
if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) {
if (mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
mAdaptiveIconScale = normalizeAdaptiveIcon(d, mMaxSize, mAdaptiveIconBounds);
}
if (outBounds != null) {
outBounds.set(mAdaptiveIconBounds);
}
return mAdaptiveIconScale;
}
int width = d.getIntrinsicWidth();
int height = d.getIntrinsicHeight();
if (width <= 0 || height <= 0) {
width = width <= 0 || width > mMaxSize ? mMaxSize : width;
height = height <= 0 || height > mMaxSize ? mMaxSize : height;
} else if (width > mMaxSize || height > mMaxSize) {
int max = Math.max(width, height);
width = mMaxSize * width / max;
height = mMaxSize * height / max;
}
mBitmap.eraseColor(Color.TRANSPARENT);
d.setBounds(0, 0, width, height);
d.draw(mCanvas);
ByteBuffer buffer = ByteBuffer.wrap(mPixels);
buffer.rewind();
mBitmap.copyPixelsToBuffer(buffer);
// Overall bounds of the visible icon.
int topY = -1;
int bottomY = -1;
int leftX = mMaxSize + 1;
int rightX = -1;
// Create border by going through all pixels one row at a time and for each row find
// the first and the last non-transparent pixel. Set those values to mLeftBorder and
// mRightBorder and use -1 if there are no visible pixel in the row.
// buffer position
int index = 0;
// buffer shift after every row, width of buffer = mMaxSize
int rowSizeDiff = mMaxSize - width;
// first and last position for any row.
int firstX, lastX;
for (int y = 0; y < height; y++) {
firstX = lastX = -1;
for (int x = 0; x < width; x++) {
if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
if (firstX == -1) {
firstX = x;
}
lastX = x;
}
index++;
}
index += rowSizeDiff;
mLeftBorder[y] = firstX;
mRightBorder[y] = lastX;
// If there is at least one visible pixel, update the overall bounds.
if (firstX != -1) {
bottomY = y;
if (topY == -1) {
topY = y;
}
leftX = Math.min(leftX, firstX);
rightX = Math.max(rightX, lastX);
}
}
if (topY == -1 || rightX == -1) {
// No valid pixels found. Do not scale.
return 1;
}
convertToConvexArray(mLeftBorder, 1, topY, bottomY);
convertToConvexArray(mRightBorder, -1, topY, bottomY);
// Area of the convex hull
float area = 0;
for (int y = 0; y < height; y++) {
if (mLeftBorder[y] <= -1) {
continue;
}
area += mRightBorder[y] - mLeftBorder[y] + 1;
}
mBounds.left = leftX;
mBounds.right = rightX;
mBounds.top = topY;
mBounds.bottom = bottomY;
if (outBounds != null) {
outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
1 - ((float) mBounds.right) / width,
1 - ((float) mBounds.bottom) / height);
}
if (outMaskShape != null && mEnableShapeDetection && outMaskShape.length > 0) {
outMaskShape[0] = isShape(path);
}
// Area of the rectangle required to fit the convex hull
float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
return getScale(area, rectArea, width * height);
}
/**
* Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
* (except on either ends) with appropriate values.
* @param xCoordinates map of x coordinate per y.
* @param direction 1 for left border and -1 for right border.
* @param topY the first Y position (inclusive) with a valid value.
* @param bottomY the last Y position (inclusive) with a valid value.
*/
private static void convertToConvexArray(
float[] xCoordinates, int direction, int topY, int bottomY) {
int total = xCoordinates.length;
// The tangent at each pixel.
float[] angles = new float[total - 1];
int first = topY; // First valid y coordinate
int last = -1; // Last valid y coordinate which didn't have a missing value
float lastAngle = Float.MAX_VALUE;
for (int i = topY + 1; i <= bottomY; i++) {
if (xCoordinates[i] <= -1) {
continue;
}
int start;
if (lastAngle == Float.MAX_VALUE) {
start = first;
} else {
float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
start = last;
// If this position creates a concave angle, keep moving up until we find a
// position which creates a convex angle.
if ((currentAngle - lastAngle) * direction < 0) {
while (start > first) {
start --;
currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
if ((currentAngle - angles[start]) * direction >= 0) {
break;
}
}
}
}
// Reset from last check
lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
// Update all the points from start.
for (int j = start; j < i; j++) {
angles[j] = lastAngle;
xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
}
last = i;
}
}
/**
* @return The diameter of the normalized circle that fits inside of the square (size x size).
*/
public static int getNormalizedCircleSize(int size) {
float area = size * size * MAX_CIRCLE_AREA_FACTOR;
return (int) Math.round(Math.sqrt((4 * area) / Math.PI));
}
}
6.修改后的效果如下:
可以看到图标都正常显示了,不管是圆角还是圆形,当然还有方角、8边形、水滴图标等等。
6.1 圆角图标
6.2 圆形图标:
7.总结:
在Android12 Launcher3中系统设置App桌面图标形状后会出现一个白边,导致显示很难看,这明细需要修改,解决方法就是在系统源码中修改缩放级别,默认不缩小图标,以上方法需要编译源码后重新打包验证.
- 修改BaseIconFactory,
- 修改FixedScaleDrawable
- 修改IconNormalizer
- 当然这只是系统级修改,如果想要App本来的图标就适配Android12启动图标
- 普通App不会有此问题,App在手机上会自行适配,因为这是在系统Launcher设置App形状后出现的.