diff options
author | Hyunyoung Song <hyunyoungs@google.com> | 2018-11-01 23:12:54 -0700 |
---|---|---|
committer | Sunny Goyal <sunnygoyal@google.com> | 2018-11-02 14:18:45 -0700 |
commit | 719eee2be266bd84bba33ce35142b0cad46e40ea (patch) | |
tree | fba75356c0acc6c1572a7901e9fe7c5bcba0379b /iconloaderlib | |
parent | b1513bd811d4efd6ec9b0251c5663c544c278909 (diff) | |
download | android_packages_apps_Trebuchet-719eee2be266bd84bba33ce35142b0cad46e40ea.tar.gz android_packages_apps_Trebuchet-719eee2be266bd84bba33ce35142b0cad46e40ea.tar.bz2 android_packages_apps_Trebuchet-719eee2be266bd84bba33ce35142b0cad46e40ea.zip |
Create iconloader library
Bug: 115891474
Test: Builds everything
Change-Id: I1d75702d4e5a10d694eeb839784a629de2f74dd2
Diffstat (limited to 'iconloaderlib')
-rw-r--r-- | iconloaderlib/Android.bp | 28 | ||||
-rw-r--r-- | iconloaderlib/AndroidManifest.xml | 20 | ||||
-rw-r--r-- | iconloaderlib/build.gradle | 50 | ||||
-rw-r--r-- | iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml | 22 | ||||
-rw-r--r-- | iconloaderlib/res/drawable/ic_instant_app_badge.xml | 39 | ||||
-rw-r--r-- | iconloaderlib/res/values/colors.xml | 21 | ||||
-rw-r--r-- | iconloaderlib/res/values/dimens.xml | 19 | ||||
-rw-r--r-- | iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java | 303 | ||||
-rw-r--r-- | iconloaderlib/src/com/android.launcher3/icons/BitmapInfo.java | 49 | ||||
-rw-r--r-- | iconloaderlib/src/com/android.launcher3/icons/ColorExtractor.java | 127 | ||||
-rw-r--r-- | iconloaderlib/src/com/android.launcher3/icons/FixedScaleDrawable.java | 56 | ||||
-rw-r--r-- | iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java | 280 | ||||
-rw-r--r-- | iconloaderlib/src/com/android.launcher3/icons/ShadowGenerator.java | 166 |
13 files changed, 1180 insertions, 0 deletions
diff --git a/iconloaderlib/Android.bp b/iconloaderlib/Android.bp new file mode 100644 index 000000000..8a71f94ed --- /dev/null +++ b/iconloaderlib/Android.bp @@ -0,0 +1,28 @@ +// Copyright (C) 2018 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. + +android_library { + name: "iconloader", + sdk_version: "28", + min_sdk_version: "21", + static_libs: [ + "androidx.core_core", + ], + resource_dirs: [ + "res", + ], + srcs: [ + "src/**/*.java", + ], +} diff --git a/iconloaderlib/AndroidManifest.xml b/iconloaderlib/AndroidManifest.xml new file mode 100644 index 000000000..b30258da2 --- /dev/null +++ b/iconloaderlib/AndroidManifest.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2018 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.launcher3.icons"> +</manifest> diff --git a/iconloaderlib/build.gradle b/iconloaderlib/build.gradle new file mode 100644 index 000000000..d08029386 --- /dev/null +++ b/iconloaderlib/build.gradle @@ -0,0 +1,50 @@ +buildscript { + repositories { + mavenCentral() + google() + } + dependencies { + classpath GRADLE_CLASS_PATH + } +} + +apply plugin: 'com.android.library' + +android { + compileSdkVersion COMPILE_SDK.toInteger() + buildToolsVersion BUILD_TOOLS_VERSION + publishNonDefault true + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 28 + versionCode 1 + versionName "1.0" + } + + sourceSets { + main { + java.srcDirs = ['src'] + manifest.srcFile 'AndroidManifest.xml' + res.srcDirs = ['res'] + } + } + + lintOptions { + abortOnError false + } + + tasks.withType(JavaCompile) { + options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + } +} + + +repositories { + mavenCentral() + google() +} + +dependencies { + implementation "androidx.core:core:${ANDROID_X_VERSION}" +} diff --git a/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml new file mode 100644 index 000000000..9f13cf571 --- /dev/null +++ b/iconloaderlib/res/drawable-v26/adaptive_icon_drawable_wrapper.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + 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. +--> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@color/legacy_icon_background"/> + <foreground> + <com.android.launcher3.icons.FixedScaleDrawable /> + </foreground> +</adaptive-icon> diff --git a/iconloaderlib/res/drawable/ic_instant_app_badge.xml b/iconloaderlib/res/drawable/ic_instant_app_badge.xml new file mode 100644 index 000000000..b74317e5f --- /dev/null +++ b/iconloaderlib/res/drawable/ic_instant_app_badge.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="@dimen/profile_badge_size" + android:height="@dimen/profile_badge_size" + android:viewportWidth="18" + android:viewportHeight="18"> + + <path + android:fillColor="@android:color/black" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@android:color/white" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@android:color/white" + android:strokeWidth="1" + android:pathData="M 9 0 C 13.9705627485 0 18 4.02943725152 18 9 C 18 13.9705627485 13.9705627485 18 9 18 C 4.02943725152 18 0 13.9705627485 0 9 C 0 4.02943725152 4.02943725152 0 9 0 Z" /> + <path + android:fillColor="@android:color/black" + android:fillAlpha="0.87" + android:strokeWidth="1" + android:pathData="M 6 10.4123279 L 8.63934949 10.4123279 L 8.63934949 15.6 L 12.5577168 7.84517705 L 9.94547194 7.84517705 L 9.94547194 2 Z" /> +</vector> diff --git a/iconloaderlib/res/values/colors.xml b/iconloaderlib/res/values/colors.xml new file mode 100644 index 000000000..873b2fc5f --- /dev/null +++ b/iconloaderlib/res/values/colors.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +** +** Copyright 2018, 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. +*/ +--> +<resources> + <color name="legacy_icon_background">#FFFFFF</color> +</resources> diff --git a/iconloaderlib/res/values/dimens.xml b/iconloaderlib/res/values/dimens.xml new file mode 100644 index 000000000..e8c0c44f7 --- /dev/null +++ b/iconloaderlib/res/values/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2018 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. +--> + +<resources> + <dimen name="profile_badge_size">24dp</dimen> +</resources> diff --git a/iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java b/iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java new file mode 100644 index 000000000..681c03c7c --- /dev/null +++ b/iconloaderlib/src/com/android.launcher3/icons/BaseIconFactory.java @@ -0,0 +1,303 @@ +package com.android.launcher3.icons; + +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.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.os.Build; +import android.os.Process; +import android.os.UserHandle; + +import com.android.launcher3.icons.R; + +import static android.graphics.Paint.DITHER_FLAG; +import static android.graphics.Paint.FILTER_BITMAP_FLAG; +import static com.android.launcher3.icons.ShadowGenerator.BLUR_FACTOR; + +/** + * This class will be moved to androidx library. There shouldn't be any dependency outside + * this package. + */ +public class BaseIconFactory { + + private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE; + public static final boolean ATLEAST_OREO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; + + private final Rect mOldBounds = new Rect(); + private final Context mContext; + private final Canvas mCanvas; + private final PackageManager mPm; + private final ColorExtractor mColorExtractor; + private boolean mDisableColorExtractor; + + private int mFillResIconDpi; + private int mIconBitmapSize; + + private IconNormalizer mNormalizer; + private ShadowGenerator mShadowGenerator; + + private Drawable mWrapperIcon; + private int mWrapperBackgroundColor; + + protected BaseIconFactory(Context context, int fillResIconDpi, int iconBitmapSize) { + mContext = context.getApplicationContext(); + + mFillResIconDpi = fillResIconDpi; + mIconBitmapSize = iconBitmapSize; + + mPm = mContext.getPackageManager(); + mColorExtractor = new ColorExtractor(); + + mCanvas = new Canvas(); + mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG)); + } + + protected void clear() { + mWrapperBackgroundColor = DEFAULT_WRAPPER_BACKGROUND; + mDisableColorExtractor = false; + } + + public ShadowGenerator getShadowGenerator() { + if (mShadowGenerator == null) { + mShadowGenerator = new ShadowGenerator(mIconBitmapSize); + } + return mShadowGenerator; + } + + public IconNormalizer getNormalizer() { + if (mNormalizer == null) { + mNormalizer = new IconNormalizer(mContext, mIconBitmapSize); + } + return mNormalizer; + } + + 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; + } + + public BitmapInfo createIconBitmap(Bitmap icon) { + if (mIconBitmapSize == icon.getWidth() && mIconBitmapSize == icon.getHeight()) { + return BitmapInfo.fromBitmap(icon); + } + return BitmapInfo.fromBitmap( + createIconBitmap(new BitmapDrawable(mContext.getResources(), icon), 1f)); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons) { + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, false, null); + } + + public BitmapInfo createBadgedIconBitmap(Drawable icon, UserHandle user, + boolean shrinkNonAdaptiveIcons, boolean isInstantApp) { + return createBadgedIconBitmap(icon, user, shrinkNonAdaptiveIcons, isInstantApp, null); + } + + /** + * 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(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); + } + + final Bitmap result; + if (user != null && !Process.myUserHandle().equals(user)) { + BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap); + Drawable badged = mPm.getUserBadgedIcon(drawable, user); + if (badged instanceof BitmapDrawable) { + result = ((BitmapDrawable) badged).getBitmap(); + } else { + result = createIconBitmap(badged, 1f); + } + } else if (isInstantApp) { + badgeWithDrawable(bitmap, mContext.getDrawable(R.drawable.ic_instant_app_badge)); + result = bitmap; + } else { + result = bitmap; + } + return BitmapInfo.fromBitmap(result, mDisableColorExtractor ? null : mColorExtractor); + } + + 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))); + } + + /** + * 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(Drawable icon, boolean shrinkNonAdaptiveIcons, + RectF outIconBounds, float[] outScale) { + float scale = 1f; + + if (shrinkNonAdaptiveIcons) { + boolean[] outShape = new boolean[1]; + if (mWrapperIcon == null) { + mWrapperIcon = mContext.getDrawable(R.drawable.adaptive_icon_drawable_wrapper) + .mutate(); + } + AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon; + dr.setBounds(0, 0, 1, 1); + scale = getNormalizer().getScale(icon, outIconBounds); + if (ATLEAST_OREO && !(icon instanceof AdaptiveIconDrawable)) { + FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground()); + fsd.setDrawable(icon); + fsd.setScale(scale); + icon = dr; + scale = getNormalizer().getScale(icon, outIconBounds); + + ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor); + } + } else { + scale = getNormalizer().getScale(icon, outIconBounds); + } + + 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 = mContext.getResources().getDimensionPixelSize(R.dimen.profile_badge_size); + badge.setBounds(mIconBitmapSize - badgeSize, mIconBitmapSize - badgeSize, + mIconBitmapSize, mIconBitmapSize); + badge.draw(target); + } + + /** + * @param scale the scale to apply before drawing {@param icon} on the canvas + */ + private Bitmap createIconBitmap(Drawable icon, float scale) { + Bitmap bitmap = Bitmap.createBitmap(mIconBitmapSize, mIconBitmapSize, + Bitmap.Config.ARGB_8888); + mCanvas.setBitmap(bitmap); + mOldBounds.set(icon.getBounds()); + + if (ATLEAST_OREO && icon instanceof AdaptiveIconDrawable) { + int offset = Math.max((int) Math.ceil(BLUR_FACTOR * mIconBitmapSize), + Math.round(mIconBitmapSize * (1 - scale) / 2 )); + icon.setBounds(offset, offset, mIconBitmapSize - offset, mIconBitmapSize - offset); + 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 = mIconBitmapSize; + int height = mIconBitmapSize; + + 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 = (mIconBitmapSize - width) / 2; + final int top = (mIconBitmapSize - height) / 2; + icon.setBounds(left, top, left + width, top + height); + mCanvas.save(); + mCanvas.scale(scale, scale, mIconBitmapSize / 2, mIconBitmapSize / 2); + icon.draw(mCanvas); + mCanvas.restore(); + + } + icon.setBounds(mOldBounds); + mCanvas.setBitmap(null); + return bitmap; + } + + /** + * 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(); + } + } +} diff --git a/iconloaderlib/src/com/android.launcher3/icons/BitmapInfo.java b/iconloaderlib/src/com/android.launcher3/icons/BitmapInfo.java new file mode 100644 index 000000000..245561ea5 --- /dev/null +++ b/iconloaderlib/src/com/android.launcher3/icons/BitmapInfo.java @@ -0,0 +1,49 @@ +/* + * 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. + */ +package com.android.launcher3.icons; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; + +public class BitmapInfo { + + public static final Bitmap LOW_RES_ICON = Bitmap.createBitmap(1, 1, Config.ALPHA_8); + + public Bitmap icon; + public int color; + + public void applyTo(BitmapInfo info) { + info.icon = icon; + info.color = color; + } + + public final boolean isLowRes() { + return LOW_RES_ICON == icon; + } + + public static BitmapInfo fromBitmap(Bitmap bitmap) { + return fromBitmap(bitmap, null); + } + + public static BitmapInfo fromBitmap(Bitmap bitmap, ColorExtractor dominantColorExtractor) { + BitmapInfo info = new BitmapInfo(); + info.icon = bitmap; + info.color = dominantColorExtractor != null + ? dominantColorExtractor.findDominantColorByHue(bitmap) + : 0; + return info; + } +} diff --git a/iconloaderlib/src/com/android.launcher3/icons/ColorExtractor.java b/iconloaderlib/src/com/android.launcher3/icons/ColorExtractor.java new file mode 100644 index 000000000..87bda825c --- /dev/null +++ b/iconloaderlib/src/com/android.launcher3/icons/ColorExtractor.java @@ -0,0 +1,127 @@ +/* + * 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. + */ +package com.android.launcher3.icons; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.util.SparseArray; +import java.util.Arrays; + +/** + * Utility class for extracting colors from a bitmap. + */ +public class ColorExtractor { + + private final int NUM_SAMPLES = 20; + private final float[] mTmpHsv = new float[3]; + private final float[] mTmpHueScoreHistogram = new float[360]; + private final int[] mTmpPixels = new int[NUM_SAMPLES]; + private final SparseArray<Float> mTmpRgbScores = new SparseArray<>(); + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + */ + public int findDominantColorByHue(Bitmap bitmap) { + return findDominantColorByHue(bitmap, NUM_SAMPLES); + } + + /** + * This picks a dominant color, looking for high-saturation, high-value, repeated hues. + * @param bitmap The bitmap to scan + */ + public 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 = mTmpHsv; + Arrays.fill(hsv, 0); + + // 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 = mTmpHueScoreHistogram; + Arrays.fill(hueScoreHistogram, 0); + float highScore = -1; + int bestHue = -1; + + int[] pixels = mTmpPixels; + Arrays.fill(pixels, 0); + 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 = mTmpRgbScores; + rgbScores.clear(); + 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; + } +} diff --git a/iconloaderlib/src/com/android.launcher3/icons/FixedScaleDrawable.java b/iconloaderlib/src/com/android.launcher3/icons/FixedScaleDrawable.java new file mode 100644 index 000000000..e594f477e --- /dev/null +++ b/iconloaderlib/src/com/android.launcher3/icons/FixedScaleDrawable.java @@ -0,0 +1,56 @@ +package com.android.launcher3.icons; + +import android.annotation.TargetApi; +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.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 + private 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, 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; + } + } +} diff --git a/iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java b/iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java new file mode 100644 index 000000000..05908df99 --- /dev/null +++ b/iconloaderlib/src/com/android.launcher3/icons/IconNormalizer.java @@ -0,0 +1,280 @@ +/* + * 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.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.drawable.AdaptiveIconDrawable; +import android.graphics.drawable.Drawable; + +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; + + 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 byte[] mPixels; + + private final Rect mAdaptiveIconBounds; + private float mAdaptiveIconScale; + + // 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; + + /** package private **/ + IconNormalizer(Context context, int iconBitmapSize) { + // 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 Rect(); + + mAdaptiveIconScale = SCALE_NOT_INITIALIZED; + } + + /** + * 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) { + if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable) { + if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) { + 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; + } + + // Area of the rectangle required to fit the convex hull + float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX); + float hullByRect = area / rectArea; + + float scaleRequired; + if (hullByRect < CIRCLE_AREA_BY_RECT) { + scaleRequired = MAX_CIRCLE_AREA_FACTOR; + } else { + scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect); + } + 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); + } + float areaScale = area / (width * height); + // Use sqrt of the final ratio as the images is scaled across both width and height. + float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1; + if (BaseIconFactory.ATLEAST_OREO && d instanceof AdaptiveIconDrawable && + mAdaptiveIconScale == SCALE_NOT_INITIALIZED) { + mAdaptiveIconScale = scale; + mAdaptiveIconBounds.set(mBounds); + } + return scale; + } + + /** + * 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)); + } +} diff --git a/iconloaderlib/src/com/android.launcher3/icons/ShadowGenerator.java b/iconloaderlib/src/com/android.launcher3/icons/ShadowGenerator.java new file mode 100644 index 000000000..6491b7ec1 --- /dev/null +++ b/iconloaderlib/src/com/android.launcher3/icons/ShadowGenerator.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2016 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.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BlurMaskFilter; +import android.graphics.BlurMaskFilter.Blur; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.RectF; + +import androidx.core.graphics.ColorUtils; + +/** + * Utility class to add shadows to bitmaps. + */ +public class ShadowGenerator { + public static final float BLUR_FACTOR = 0.5f/48; + + // Percent of actual icon size + public static final float KEY_SHADOW_DISTANCE = 1f/48; + private static final int KEY_SHADOW_ALPHA = 61; + // Percent of actual icon size + private static final float HALF_DISTANCE = 0.5f; + private static final int AMBIENT_SHADOW_ALPHA = 30; + + private final int mIconSize; + + private final Paint mBlurPaint; + private final Paint mDrawPaint; + private final BlurMaskFilter mDefaultBlurMaskFilter; + + public ShadowGenerator(int iconSize) { + mIconSize = iconSize; + mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + mDefaultBlurMaskFilter = new BlurMaskFilter(mIconSize * BLUR_FACTOR, Blur.NORMAL); + } + + public synchronized void recreateIcon(Bitmap icon, Canvas out) { + recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out); + } + + public synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter, + int ambientAlpha, int keyAlpha, Canvas out) { + int[] offset = new int[2]; + mBlurPaint.setMaskFilter(blurMaskFilter); + Bitmap shadow = icon.extractAlpha(mBlurPaint, offset); + + // Draw ambient shadow + mDrawPaint.setAlpha(ambientAlpha); + out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint); + + // Draw key shadow + mDrawPaint.setAlpha(keyAlpha); + out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconSize, mDrawPaint); + + // Draw the icon + mDrawPaint.setAlpha(255); + out.drawBitmap(icon, 0, 0, mDrawPaint); + } + + /** + * Returns the minimum amount by which an icon with {@param bounds} should be scaled + * so that the shadows do not get clipped. + */ + public static float getScaleForBounds(RectF bounds) { + float scale = 1; + + // For top, left & right, we need same space. + float minSide = Math.min(Math.min(bounds.left, bounds.right), bounds.top); + if (minSide < BLUR_FACTOR) { + scale = (HALF_DISTANCE - BLUR_FACTOR) / (HALF_DISTANCE - minSide); + } + + float bottomSpace = BLUR_FACTOR + KEY_SHADOW_DISTANCE; + if (bounds.bottom < bottomSpace) { + scale = Math.min(scale, (HALF_DISTANCE - bottomSpace) / (HALF_DISTANCE - bounds.bottom)); + } + return scale; + } + + public static class Builder { + + public final RectF bounds = new RectF(); + public final int color; + + public int ambientShadowAlpha = AMBIENT_SHADOW_ALPHA; + + public float shadowBlur; + + public float keyShadowDistance; + public int keyShadowAlpha = KEY_SHADOW_ALPHA; + public float radius; + + public Builder(int color) { + this.color = color; + } + + public Builder setupBlurForSize(int height) { + shadowBlur = height * 1f / 24; + keyShadowDistance = height * 1f / 16; + return this; + } + + public Bitmap createPill(int width, int height) { + radius = height / 2f; + + int centerX = Math.round(width / 2f + shadowBlur); + int centerY = Math.round(radius + shadowBlur + keyShadowDistance); + int center = Math.max(centerX, centerY); + bounds.set(0, 0, width, height); + bounds.offsetTo(center - width / 2f, center - height / 2f); + + int size = center * 2; + Bitmap result = Bitmap.createBitmap(size, size, Config.ARGB_8888); + drawShadow(new Canvas(result)); + return result; + } + + public void drawShadow(Canvas c) { + Paint p = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + p.setColor(color); + + // Key shadow + p.setShadowLayer(shadowBlur, 0, keyShadowDistance, + ColorUtils.setAlphaComponent(Color.BLACK, keyShadowAlpha)); + c.drawRoundRect(bounds, radius, radius, p); + + // Ambient shadow + p.setShadowLayer(shadowBlur, 0, 0, + ColorUtils.setAlphaComponent(Color.BLACK, ambientShadowAlpha)); + c.drawRoundRect(bounds, radius, radius, p); + + if (Color.alpha(color) < 255) { + // Clear any content inside the pill-rect for translucent fill. + p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); + p.clearShadowLayer(); + p.setColor(Color.BLACK); + c.drawRoundRect(bounds, radius, radius, p); + + p.setXfermode(null); + p.setColor(color); + c.drawRoundRect(bounds, radius, radius, p); + } + } + } +} |