Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 555b1752 authored by d34d's avatar d34d Committed by Clark Scheff
Browse files

Themes: Add palettized icon background support

This feature allows a theme to supply a single icon background,
usually a white or gray scale image, which will then be tinted using
a color from a palette of six color styles.

To use this new feature a theme designer will need to include the
new <paletteback /> tag which has the following syntax:

<paletteback
    img="name_of_png_drawable"
    swatchType=["vibrant" | "vibrantLight" | "vibrantDark" | "muted" | "mutedLight" | "mutedDark"]
    defaultSwatchColor1="hex_color"
    defaultSwatchColor2="hex_color"
    ⋮
    defaultSwatchColorN="hex_color"
    />

ex:
<paletteback
    img="iconback_palette"
    swatchType="vibrantDark"
    defaultSwatchColor1="#009688"
    defaultSwatchColor2="#cc0000"
    defaultSwatchColor3="#ff8800"
    />

You should specify some default swatch colors for those cases when
the original icon did not have any colors to match the swatchType
you requested. If no default swatch colors are defined, the original
un-tinted background will be used.

Change-Id: Ifd6dd65cc34ad4cd966fa9670f68704ac5671960
parent 9ad73a96
Loading
Loading
Loading
Loading
+36 −0
Original line number Diff line number Diff line
@@ -27,8 +27,15 @@ public class ComposedIconInfo implements Parcelable {
    public int iconSize;
    public float[] colorFilter;

    // Palettized background items
    public int iconPaletteBack;
    public SwatchType swatchType;
    public int[] defaultSwatchColors;

    public ComposedIconInfo() {
        super();
        iconPaletteBack = 0;
        swatchType = SwatchType.None;
    }

    private ComposedIconInfo(Parcel source) {
@@ -51,6 +58,15 @@ public class ComposedIconInfo implements Parcelable {
                colorFilter[i] = source.readFloat();
            }
        }
        iconPaletteBack = source.readInt();
        swatchType = SwatchType.values()[source.readInt()];
        int numDefaultColors = source.readInt();
        if (numDefaultColors > 0) {
            defaultSwatchColors = new int[numDefaultColors];
            for (int i = 0; i < numDefaultColors; i++) {
                defaultSwatchColors[i] = source.readInt();
            }
        }
    }

    @Override
@@ -79,6 +95,16 @@ public class ComposedIconInfo implements Parcelable {
        } else {
            dest.writeInt(0);
        }
        dest.writeInt(iconPaletteBack);
        dest.writeInt(swatchType.ordinal());
        if (defaultSwatchColors != null) {
            dest.writeInt(defaultSwatchColors.length);
            for (int color : defaultSwatchColors) {
                dest.writeInt(color);
            }
        } else {
            dest.writeInt(0);
        }
    }

    public static final Creator<ComposedIconInfo> CREATOR
@@ -93,4 +119,14 @@ public class ComposedIconInfo implements Parcelable {
            return new ComposedIconInfo[0];
        }
    };

    public enum SwatchType {
        None,
        Vibrant,
        VibrantLight,
        VibrantDark,
        Muted,
        MutedLight,
        MutedDark
    }
}
+143 −13
Original line number Diff line number Diff line
@@ -34,6 +34,7 @@ import android.graphics.ColorMatrixColorFilter;
import android.graphics.Paint;
import android.graphics.PaintFlagsDrawFilter;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffColorFilter;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.drawable.BitmapDrawable;
@@ -43,6 +44,7 @@ import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Log;
import android.util.TypedValue;
import com.android.internal.util.cm.palette.Palette;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;
@@ -63,12 +65,28 @@ import android.util.DisplayMetrics;
/** @hide */
public class IconPackHelper {
    private static final String TAG = IconPackHelper.class.getSimpleName();

    private static final boolean DEBUG = false;

    private static final String ICON_MASK_TAG = "iconmask";
    private static final String ICON_BACK_TAG = "iconback";
    private static final String ICON_UPON_TAG = "iconupon";
    private static final String ICON_SCALE_TAG = "scale";
    private static final String ICON_BACK_FORMAT = "iconback%d";

    // Palettized icon background constants
    private static final String ICON_PALETTIZED_BACK_TAG = "paletteback";
    private static final String IMG_ATTR = "img";
    private static final String SWATCH_TYPE_ATTR = "swatchType";
    private static final String DEFAULT_SWATCH_COLOR_ATTR = "defaultSwatchColor";
    private static final String VIBRANT_VALUE = "vibrant";
    private static final String VIBRANT_LIGHT_VALUE = "vibrantLight";
    private static final String VIBRANT_DARK_VALUE = "vibrantDark";
    private static final String MUTED_VALUE = "muted";
    private static final String MUTED_LIGHT_VALUE = "mutedLight";
    private static final String MUTED_DARK_VALUE = "mutedDark";
    private static final int NUM_PALETTE_COLORS = 32;

    private static final ComponentName ICON_BACK_COMPONENT;
    private static final ComponentName ICON_MASK_COMPONENT;
    private static final ComponentName ICON_UPON_COMPONENT;
@@ -168,9 +186,10 @@ public class IconPackHelper {
    }

    private boolean isComposedIconComponent(String tag) {
        return tag.equalsIgnoreCase(ICON_MASK_TAG) ||
                tag.equalsIgnoreCase(ICON_BACK_TAG) ||
                tag.equalsIgnoreCase(ICON_UPON_TAG);
        return ICON_MASK_TAG.equalsIgnoreCase(tag) ||
                ICON_BACK_TAG.equalsIgnoreCase(tag) ||
                ICON_UPON_TAG.equalsIgnoreCase(tag) ||
                ICON_PALETTIZED_BACK_TAG.equalsIgnoreCase(tag);
    }

    private boolean parseComposedIconComponent(XmlPullParser parser,
@@ -182,13 +201,15 @@ public class IconPackHelper {
        }

        if (parser.getAttributeCount() >= 1) {
            if (tag.equalsIgnoreCase(ICON_BACK_TAG)) {
            if (ICON_BACK_TAG.equalsIgnoreCase(tag)) {
                mIconBackCount = parser.getAttributeCount();
                for (int i = 0; i < mIconBackCount; i++) {
                    tag = String.format(ICON_BACK_FORMAT, i);
                    icon = parser.getAttributeValue(i);
                    iconPackResources.put(new ComponentName(tag, ""), icon);
                }
            } else if (ICON_PALETTIZED_BACK_TAG.equalsIgnoreCase(tag)) {
                parsePalettizedBackground(parser, mComposedIconInfo);
            } else {
                icon = parser.getAttributeValue(0);
                iconPackResources.put(new ComponentName(tag, ""),
@@ -200,6 +221,62 @@ public class IconPackHelper {
        return false;
    }

    private void parsePalettizedBackground(XmlPullParser parser, ComposedIconInfo iconInfo) {
        int attrCount = parser.getAttributeCount();
        ArrayList<Integer> convertedColors = new ArrayList<Integer>();
        for (int i = 0; i < attrCount; i++) {
            String name = parser.getAttributeName(i);
            String value = parser.getAttributeValue(i);
            if (TextUtils.isEmpty(name)) {
                Log.w(TAG, "Attribute name cannot be empty or null");
                continue;
            }
            if (TextUtils.isEmpty(value)) {
                Log.w(TAG, "Attribute value cannot be empty or null");
                continue;
            }
            if (IMG_ATTR.equalsIgnoreCase(name)) {
                iconInfo.iconPaletteBack = getResourceIdForDrawable(value);
                if (DEBUG) {
                    Log.d(TAG, String.format("img=%s, resId=%d", value,
                            iconInfo.iconPaletteBack));
                }
            } else if (SWATCH_TYPE_ATTR.equalsIgnoreCase(name)) {
                ComposedIconInfo.SwatchType type = ComposedIconInfo.SwatchType.None;
                if (VIBRANT_VALUE.equalsIgnoreCase(value)) {
                    type = ComposedIconInfo.SwatchType.Vibrant;
                } else if (VIBRANT_LIGHT_VALUE.equalsIgnoreCase(value)) {
                    type = ComposedIconInfo.SwatchType.VibrantLight;
                } else if (VIBRANT_DARK_VALUE.equalsIgnoreCase(value)) {
                    type = ComposedIconInfo.SwatchType.VibrantDark;
                } else if (MUTED_VALUE.equalsIgnoreCase(value)) {
                    type = ComposedIconInfo.SwatchType.Muted;
                } else if (MUTED_LIGHT_VALUE.equalsIgnoreCase(value)) {
                    type = ComposedIconInfo.SwatchType.MutedLight;
                } else if (MUTED_DARK_VALUE.equalsIgnoreCase(value)) {
                    type = ComposedIconInfo.SwatchType.MutedDark;
                }
                if (type != ComposedIconInfo.SwatchType.None) {
                    iconInfo.swatchType = type;
                    if (DEBUG) Log.d(TAG, "PaletteType=" + type);
                }
            } else if (name.startsWith(DEFAULT_SWATCH_COLOR_ATTR)) {
                try {
                    // ensure alpha is always 0xff
                    convertedColors.add(Color.parseColor(value) | 0xff000000);
                } catch (IllegalArgumentException e) {
                    Log.w(TAG, "Invalid color format", e);
                }
            }
            if (convertedColors.size() > 0) {
                iconInfo.defaultSwatchColors = new int[convertedColors.size()];
                for (int j = 0; j < convertedColors.size(); j++) {
                    iconInfo.defaultSwatchColors[j] = convertedColors.get(j);
                }
            }
        }
    }

    public void loadIconPack(String packageName) throws NameNotFoundException {
        if (packageName == null) {
            mLoadedIconPackResource = null;
@@ -208,12 +285,14 @@ public class IconPackHelper {
            mComposedIconInfo.iconMask = mComposedIconInfo.iconUpon = 0;
            mComposedIconInfo.iconScale = 0;
            mComposedIconInfo.colorFilter = null;
            mComposedIconInfo.iconPaletteBack = 0;
            mComposedIconInfo.swatchType = ComposedIconInfo.SwatchType.None;
        } else {
            mIconBackCount = 0;
            Resources res = createIconResource(mContext, packageName);
            mIconPackResourceMap = getIconResMapFromXml(res, packageName);
            mLoadedIconPackResource = res;
            mLoadedIconPackName = packageName;
            mIconPackResourceMap = getIconResMapFromXml(res, packageName);
            loadComposedIconComponents();
            ColorMatrix cm = mFilterBuilder.build();
            if (cm != null) {
@@ -454,11 +533,19 @@ public class IconPackHelper {
                ComposedIconInfo iconInfo) {
            if (iconInfo == null) return icon;
            int back = 0;
            if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) {
            int defaultSwatchColor = 0;
            if (iconInfo.swatchType != ComposedIconInfo.SwatchType.None) {
                back = iconInfo.iconPaletteBack;
                if (iconInfo.defaultSwatchColors.length > 0) {
                    defaultSwatchColor = iconInfo.defaultSwatchColors[
                            sRandom.nextInt(iconInfo.defaultSwatchColors.length)];
                }
            } else if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) {
                back = iconInfo.iconBacks[sRandom.nextInt(iconInfo.iconBacks.length)];
            }
            Bitmap bmp = createIconBitmap(icon, res, back, iconInfo.iconMask, iconInfo.iconUpon,
                    iconInfo.iconScale, iconInfo.iconSize, iconInfo.colorFilter);
                    iconInfo.iconScale, iconInfo.iconSize, iconInfo.colorFilter,
                    iconInfo.swatchType, defaultSwatchColor);
            return bmp != null ? new BitmapDrawable(res, bmp): null;
        }

@@ -470,18 +557,28 @@ public class IconPackHelper {
            outValue.assetCookie = COMPOSED_ICON_COOKIE;
            outValue.data = resId & (COMPOSED_ICON_COOKIE << 24 | 0x00ffffff);
            outValue.string = getCachedIconPath(pkgName, resId, outValue.density);
            int hashCode = outValue.string.hashCode() & 0x7fffffff;
            int defaultSwatchColor = 0;

            if (!(new File(outValue.string.toString()).exists())) {
                // compose the icon and cache it
                final ComposedIconInfo iconInfo = res.getComposedIconInfo();
                int back = 0;
                if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) {
                    back = iconInfo.iconBacks[(outValue.string.hashCode() & 0x7fffffff)
                            % iconInfo.iconBacks.length];
                if (iconInfo.swatchType != ComposedIconInfo.SwatchType.None) {
                    back = iconInfo.iconPaletteBack;
                    if (iconInfo.defaultSwatchColors.length > 0) {
                        defaultSwatchColor =iconInfo.defaultSwatchColors[
                                hashCode % iconInfo.defaultSwatchColors.length];
                    }
                } else if (iconInfo.iconBacks != null && iconInfo.iconBacks.length > 0) {
                    back = iconInfo.iconBacks[hashCode % iconInfo.iconBacks.length];
                }
                if (DEBUG) {
                    Log.d(TAG, "Composing icon for " + pkgName);
                }
                Bitmap bmp = createIconBitmap(baseIcon, res, back, iconInfo.iconMask,
                        iconInfo.iconUpon, iconInfo.iconScale, iconInfo.iconSize,
                        iconInfo.colorFilter);
                        iconInfo.colorFilter, iconInfo.swatchType, defaultSwatchColor);
                if (!cacheComposedIcon(bmp, getCachedIconName(pkgName, resId, outValue.density))) {
                    Log.w(TAG, "Unable to cache icon " + outValue.string);
                    // restore the original TypedValue
@@ -491,7 +588,8 @@ public class IconPackHelper {
        }

        private static Bitmap createIconBitmap(Drawable icon, Resources res, int iconBack,
                int iconMask, int iconUpon, float scale, int iconSize, float[] colorFilter) {
                int iconMask, int iconUpon, float scale, int iconSize, float[] colorFilter,
                ComposedIconInfo.SwatchType swatchType, int defaultSwatchColor) {
            if (iconSize <= 0) return null;

            final Canvas canvas = new Canvas();
@@ -499,6 +597,7 @@ public class IconPackHelper {
                    Paint.FILTER_BITMAP_FLAG));

            int width = 0, height = 0;
            int backTintColor = 0;
            if (icon instanceof PaintDrawable) {
                PaintDrawable painter = (PaintDrawable) icon;
                painter.setIntrinsicWidth(iconSize);
@@ -529,6 +628,32 @@ public class IconPackHelper {
                    width = iconSize;
                    height = iconSize;
                }
                if (swatchType != ComposedIconInfo.SwatchType.None) {
                    Palette palette = Palette.generate(bitmap, NUM_PALETTE_COLORS);
                    switch (swatchType) {
                        case Vibrant:
                            backTintColor = palette.getVibrantColor(defaultSwatchColor);
                            break;
                        case VibrantLight:
                            backTintColor = palette.getLightVibrantColor(defaultSwatchColor);
                            break;
                        case VibrantDark:
                            backTintColor = palette.getDarkVibrantColor(defaultSwatchColor);
                            break;
                        case Muted:
                            backTintColor = palette.getMutedColor(defaultSwatchColor);
                            break;
                        case MutedLight:
                            backTintColor = palette.getLightMutedColor(defaultSwatchColor);
                            break;
                        case MutedDark:
                            backTintColor = palette.getDarkMutedColor(defaultSwatchColor);
                            break;
                    }
                    if (DEBUG) {
                        Log.d(TAG, String.format("palette tint color=0x%08x", backTintColor));
                    }
                }
            } else if (icon instanceof VectorDrawable) {
                width = height = iconSize;
            }
@@ -572,8 +697,13 @@ public class IconPackHelper {
                Drawable back = res.getDrawable(iconBack);
                if (back != null) {
                    back.setBounds(icon.getBounds());
                    ((BitmapDrawable) back).getPaint().setXfermode(
                    Paint paint = ((BitmapDrawable) back).getPaint();
                    paint.setXfermode(
                            new PorterDuffXfermode(PorterDuff.Mode.DST_OVER));
                    if (backTintColor != 0) {
                        paint.setColorFilter(new PorterDuffColorFilter(backTintColor,
                                PorterDuff.Mode.MULTIPLY));
                    }
                    back.draw(canvas);
                }
            }
+449 −0

File added.

Preview size limit exceeded, changes collapsed.

+127 −0
Original line number Diff line number Diff line
/*
 * Copyright 2014 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.internal.util.cm.palette;

import java.util.Arrays;

/**
 * Class which provides a histogram for RGB values.
 *
 * @hide
 */
final class ColorHistogram {

    private final int[] mColors;
    private final int[] mColorCounts;
    private final int mNumberColors;

    /**
     * A new {@link ColorHistogram} instance.
     *
     * @param pixels array of image contents
     */
    ColorHistogram(final int[] pixels) {
        // Sort the pixels to enable counting below
        Arrays.sort(pixels);

        // Count number of distinct colors
        mNumberColors = countDistinctColors(pixels);

        // Create arrays
        mColors = new int[mNumberColors];
        mColorCounts = new int[mNumberColors];

        // Finally count the frequency of each color
        countFrequencies(pixels);
    }

    /**
     * @return number of distinct colors in the image.
     */
    int getNumberOfColors() {
        return mNumberColors;
    }

    /**
     * @return an array containing all of the distinct colors in the image.
     */
    int[] getColors() {
        return mColors;
    }

    /**
     * @return an array containing the frequency of a distinct colors within the image.
     */
    int[] getColorCounts() {
        return mColorCounts;
    }

    private static int countDistinctColors(final int[] pixels) {
        if (pixels.length < 2) {
            // If we have less than 2 pixels we can stop here
            return pixels.length;
        }

        // If we have at least 2 pixels, we have a minimum of 1 color...
        int colorCount = 1;
        int currentColor = pixels[0];

        // Now iterate from the second pixel to the end, counting distinct colors
        for (int i = 1; i < pixels.length; i++) {
            // If we encounter a new color, increase the population
            if (pixels[i] != currentColor) {
                currentColor = pixels[i];
                colorCount++;
            }
        }

        return colorCount;
    }

    private void countFrequencies(final int[] pixels) {
        if (pixels.length == 0) {
            return;
        }

        int currentColorIndex = 0;
        int currentColor = pixels[0];

        mColors[currentColorIndex] = currentColor;
        mColorCounts[currentColorIndex] = 1;

        if (pixels.length == 1) {
            // If we only have one pixel, we can stop here
            return;
        }

        // Now iterate from the second pixel to the end, population distinct colors
        for (int i = 1; i < pixels.length; i++) {
            if (pixels[i] == currentColor) {
                // We've hit the same color as before, increase population
                mColorCounts[currentColorIndex]++;
            } else {
                // We've hit a new color, increase index
                currentColor = pixels[i];

                currentColorIndex++;
                mColors[currentColorIndex] = currentColor;
                mColorCounts[currentColorIndex] = 1;
            }
        }
    }

}
+232 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading