Android 圖片吸色,增加過渡效果

Android 圖片吸色,增加過渡效果

效果圖如下:
吸色
參考實現:

LongImageDownloadUtil imgLoadUtil = new LongImageDownloadUtil();
            imgLoadUtil.loadImageForListener(mContext.getApplicationContext(), data.imgUrl, new LongImageDownloadUtil.OnBitmapLoadListener() {
                @Override
                public void onBitmapLoaded(Bitmap bitmap) {
                    if (bitmap != null) {
                        Palette.from(bitmap).generate(new Palette.PaletteAsyncListener() {
                            @Override
                            public void onGenerated(@Nullable Palette palette) {
                                int dominantColor = palette.getDominantColor(Color.TRANSPARENT);
                                float[] hsl = new float[3];
                                ColorUtils.colorToHSL(dominantColor, hsl);
                                //修改飽和度和亮度
                                hsl[1] = 0.75f; //飽和度
                                hsl[2] = 0.45f; //亮度
                                dominantColor = ColorUtils.HSLToColor(hsl);
                                int colors[] = { 0x00000000 , dominantColor}; // 透明過渡到吸色
                                GradientDrawable bg = new GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, colors);
                                float [] bgRadii= new float[] {0,0,0,0,0,0,0,0}; // 圓角
                                float round = ExtendUtil.dip2px(getContext(), 10);
                                if (position == 0) {
                                    bgRadii = new float[]{0, 0, 0, 0, 0, 0, round, round};
                                } else if (position == 2) {
                                    bgRadii = new float[]{0, 0, 0, 0, round, round, 0, 0};
                                }
                                bg.setCornerRadii(bgRadii);
                                mViewCommentBg.setBackground(bg);
                            }
                        });
                    }
                }
                @Override
                public void onBitmapLoadFail() {

                }
            });

Palette.java 代碼

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.AsyncTask;
import android.support.annotation.ColorInt;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.annotation.Px;
import android.support.v4.graphics.ColorUtils;
import android.support.v4.util.ArrayMap;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.TimingLogger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;

public final class Palette {
    static final int DEFAULT_RESIZE_BITMAP_AREA = 12544;
    static final int DEFAULT_CALCULATE_NUMBER_COLORS = 16;
    static final float MIN_CONTRAST_TITLE_TEXT = 3.0F;
    static final float MIN_CONTRAST_BODY_TEXT = 4.5F;
    static final String LOG_TAG = "Palette";
    static final boolean LOG_TIMINGS = false;
    private final List<Palette.Swatch> mSwatches;
    private final List<Target> mTargets;
    private final Map<Target, Palette.Swatch> mSelectedSwatches;
    private final SparseBooleanArray mUsedColors;
    @Nullable
    private final Palette.Swatch mDominantSwatch;
    static final Palette.Filter DEFAULT_FILTER = new Palette.Filter() {
        private static final float BLACK_MAX_LIGHTNESS = 0.05F;
        private static final float WHITE_MIN_LIGHTNESS = 0.95F;

        public boolean isAllowed(int rgb, float[] hsl) {
            return !this.isWhite(hsl) && !this.isBlack(hsl) && !this.isNearRedILine(hsl);
        }

        private boolean isBlack(float[] hslColor) {
            return hslColor[2] <= 0.05F;
        }

        private boolean isWhite(float[] hslColor) {
            return hslColor[2] >= 0.95F;
        }

        private boolean isNearRedILine(float[] hslColor) {
            return hslColor[0] >= 10.0F && hslColor[0] <= 37.0F && hslColor[1] <= 0.82F;
        }
    };

    @NonNull
    public static Palette.Builder from(@NonNull Bitmap bitmap) {
        return new Palette.Builder(bitmap);
    }

    @NonNull
    public static Palette from(@NonNull List<Palette.Swatch> swatches) {
        return (new Palette.Builder(swatches)).generate();
    }

    /** @deprecated */
    @Deprecated
    public static Palette generate(Bitmap bitmap) {
        return from(bitmap).generate();
    }

    /** @deprecated */
    @Deprecated
    public static Palette generate(Bitmap bitmap, int numColors) {
        return from(bitmap).maximumColorCount(numColors).generate();
    }

    /** @deprecated */
    @Deprecated
    public static AsyncTask<Bitmap, Void, Palette> generateAsync(Bitmap bitmap, Palette.PaletteAsyncListener listener) {
        return from(bitmap).generate(listener);
    }

    /** @deprecated */
    @Deprecated
    public static AsyncTask<Bitmap, Void, Palette> generateAsync(Bitmap bitmap, int numColors, Palette.PaletteAsyncListener listener) {
        return from(bitmap).maximumColorCount(numColors).generate(listener);
    }

    Palette(List<Palette.Swatch> swatches, List<Target> targets) {
        this.mSwatches = swatches;
        this.mTargets = targets;
        this.mUsedColors = new SparseBooleanArray();
        this.mSelectedSwatches = new ArrayMap();
        this.mDominantSwatch = this.findDominantSwatch();
    }

    @NonNull
    public List<Palette.Swatch> getSwatches() {
        return Collections.unmodifiableList(this.mSwatches);
    }

    @NonNull
    public List<Target> getTargets() {
        return Collections.unmodifiableList(this.mTargets);
    }

    @Nullable
    public Palette.Swatch getVibrantSwatch() {
        return this.getSwatchForTarget(Target.VIBRANT);
    }

    @Nullable
    public Palette.Swatch getLightVibrantSwatch() {
        return this.getSwatchForTarget(Target.LIGHT_VIBRANT);
    }

    @Nullable
    public Palette.Swatch getDarkVibrantSwatch() {
        return this.getSwatchForTarget(Target.DARK_VIBRANT);
    }

    @Nullable
    public Palette.Swatch getMutedSwatch() {
        return this.getSwatchForTarget(Target.MUTED);
    }

    @Nullable
    public Palette.Swatch getLightMutedSwatch() {
        return this.getSwatchForTarget(Target.LIGHT_MUTED);
    }

    @Nullable
    public Palette.Swatch getDarkMutedSwatch() {
        return this.getSwatchForTarget(Target.DARK_MUTED);
    }

    @ColorInt
    public int getVibrantColor(@ColorInt int defaultColor) {
        return this.getColorForTarget(Target.VIBRANT, defaultColor);
    }

    @ColorInt
    public int getLightVibrantColor(@ColorInt int defaultColor) {
        return this.getColorForTarget(Target.LIGHT_VIBRANT, defaultColor);
    }

    @ColorInt
    public int getDarkVibrantColor(@ColorInt int defaultColor) {
        return this.getColorForTarget(Target.DARK_VIBRANT, defaultColor);
    }

    @ColorInt
    public int getMutedColor(@ColorInt int defaultColor) {
        return this.getColorForTarget(Target.MUTED, defaultColor);
    }

    @ColorInt
    public int getLightMutedColor(@ColorInt int defaultColor) {
        return this.getColorForTarget(Target.LIGHT_MUTED, defaultColor);
    }

    @ColorInt
    public int getDarkMutedColor(@ColorInt int defaultColor) {
        return this.getColorForTarget(Target.DARK_MUTED, defaultColor);
    }

    @Nullable
    public Palette.Swatch getSwatchForTarget(@NonNull Target target) {
        return (Palette.Swatch)this.mSelectedSwatches.get(target);
    }

    @ColorInt
    public int getColorForTarget(@NonNull Target target, @ColorInt int defaultColor) {
        Palette.Swatch swatch = this.getSwatchForTarget(target);
        return swatch != null ? swatch.getRgb() : defaultColor;
    }

    @Nullable
    public Palette.Swatch getDominantSwatch() {
        return this.mDominantSwatch;
    }

    @ColorInt
    public int getDominantColor(@ColorInt int defaultColor) {
        return this.mDominantSwatch != null ? this.mDominantSwatch.getRgb() : defaultColor;
    }

    void generate() {
        int i = 0;

        for(int count = this.mTargets.size(); i < count; ++i) {
            Target target = (Target)this.mTargets.get(i);
            target.normalizeWeights();
            this.mSelectedSwatches.put(target, this.generateScoredTarget(target));
        }

        this.mUsedColors.clear();
    }

    @Nullable
    private Palette.Swatch generateScoredTarget(Target target) {
        Palette.Swatch maxScoreSwatch = this.getMaxScoredSwatchForTarget(target);
        if (maxScoreSwatch != null && target.isExclusive()) {
            this.mUsedColors.append(maxScoreSwatch.getRgb(), true);
        }

        return maxScoreSwatch;
    }

    @Nullable
    private Palette.Swatch getMaxScoredSwatchForTarget(Target target) {
        float maxScore = 0.0F;
        Palette.Swatch maxScoreSwatch = null;
        int i = 0;

        for(int count = this.mSwatches.size(); i < count; ++i) {
            Palette.Swatch swatch = (Palette.Swatch)this.mSwatches.get(i);
            if (this.shouldBeScoredForTarget(swatch, target)) {
                float score = this.generateScore(swatch, target);
                if (maxScoreSwatch == null || score > maxScore) {
                    maxScoreSwatch = swatch;
                    maxScore = score;
                }
            }
        }

        return maxScoreSwatch;
    }

    private boolean shouldBeScoredForTarget(Palette.Swatch swatch, Target target) {
        float[] hsl = swatch.getHsl();
        return hsl[1] >= target.getMinimumSaturation() && hsl[1] <= target.getMaximumSaturation() && hsl[2] >= target.getMinimumLightness() && hsl[2] <= target.getMaximumLightness() && !this.mUsedColors.get(swatch.getRgb());
    }

    private float generateScore(Palette.Swatch swatch, Target target) {
        float[] hsl = swatch.getHsl();
        float saturationScore = 0.0F;
        float luminanceScore = 0.0F;
        float populationScore = 0.0F;
        int maxPopulation = this.mDominantSwatch != null ? this.mDominantSwatch.getPopulation() : 1;
        if (target.getSaturationWeight() > 0.0F) {
            saturationScore = target.getSaturationWeight() * (1.0F - Math.abs(hsl[1] - target.getTargetSaturation()));
        }

        if (target.getLightnessWeight() > 0.0F) {
            luminanceScore = target.getLightnessWeight() * (1.0F - Math.abs(hsl[2] - target.getTargetLightness()));
        }

        if (target.getPopulationWeight() > 0.0F) {
            populationScore = target.getPopulationWeight() * ((float)swatch.getPopulation() / (float)maxPopulation);
        }

        return saturationScore + luminanceScore + populationScore;
    }

    @Nullable
    private Palette.Swatch findDominantSwatch() {
        int maxPop = -2147483648;
        Palette.Swatch maxSwatch = null;
        int i = 0;

        for(int count = this.mSwatches.size(); i < count; ++i) {
            Palette.Swatch swatch = (Palette.Swatch)this.mSwatches.get(i);
            if (swatch.getPopulation() > maxPop) {
                maxSwatch = swatch;
                maxPop = swatch.getPopulation();
            }
        }

        return maxSwatch;
    }

    public interface Filter {
        boolean isAllowed(@ColorInt int var1, @NonNull float[] var2);
    }

    public static final class Builder {
        @Nullable
        private final List<Palette.Swatch> mSwatches;
        @Nullable
        private final Bitmap mBitmap;
        private final List<Target> mTargets = new ArrayList();
        private int mMaxColors = 16;
        private int mResizeArea = 12544;
        private int mResizeMaxDimension = -1;
        private final List<Palette.Filter> mFilters = new ArrayList();
        @Nullable
        private Rect mRegion;

        public Builder(@NonNull Bitmap bitmap) {
            if (bitmap != null && !bitmap.isRecycled()) {
                this.mFilters.add(Palette.DEFAULT_FILTER);
                this.mBitmap = bitmap;
                this.mSwatches = null;
                this.mTargets.add(Target.LIGHT_VIBRANT);
                this.mTargets.add(Target.VIBRANT);
                this.mTargets.add(Target.DARK_VIBRANT);
                this.mTargets.add(Target.LIGHT_MUTED);
                this.mTargets.add(Target.MUTED);
                this.mTargets.add(Target.DARK_MUTED);
            } else {
                throw new IllegalArgumentException("Bitmap is not valid");
            }
        }

        public Builder(@NonNull List<Palette.Swatch> swatches) {
            if (swatches != null && !swatches.isEmpty()) {
                this.mFilters.add(Palette.DEFAULT_FILTER);
                this.mSwatches = swatches;
                this.mBitmap = null;
            } else {
                throw new IllegalArgumentException("List of Swatches is not valid");
            }
        }

        @NonNull
        public Palette.Builder maximumColorCount(int colors) {
            this.mMaxColors = colors;
            return this;
        }

        /** @deprecated */
        @Deprecated
        @NonNull
        public Palette.Builder resizeBitmapSize(int maxDimension) {
            this.mResizeMaxDimension = maxDimension;
            this.mResizeArea = -1;
            return this;
        }

        @NonNull
        public Palette.Builder resizeBitmapArea(int area) {
            this.mResizeArea = area;
            this.mResizeMaxDimension = -1;
            return this;
        }

        @NonNull
        public Palette.Builder clearFilters() {
            this.mFilters.clear();
            return this;
        }

        @NonNull
        public Palette.Builder addFilter(Palette.Filter filter) {
            if (filter != null) {
                this.mFilters.add(filter);
            }

            return this;
        }

        @NonNull
        public Palette.Builder setRegion(@Px int left, @Px int top, @Px int right, @Px int bottom) {
            if (this.mBitmap != null) {
                if (this.mRegion == null) {
                    this.mRegion = new Rect();
                }

                this.mRegion.set(0, 0, this.mBitmap.getWidth(), this.mBitmap.getHeight());
                if (!this.mRegion.intersect(left, top, right, bottom)) {
                    throw new IllegalArgumentException("The given region must intersect with the Bitmap's dimensions.");
                }
            }

            return this;
        }

        @NonNull
        public Palette.Builder clearRegion() {
            this.mRegion = null;
            return this;
        }

        @NonNull
        public Palette.Builder addTarget(@NonNull Target target) {
            if (!this.mTargets.contains(target)) {
                this.mTargets.add(target);
            }

            return this;
        }

        @NonNull
        public Palette.Builder clearTargets() {
            if (this.mTargets != null) {
                this.mTargets.clear();
            }

            return this;
        }

        @NonNull
        public Palette generate() {
            TimingLogger logger = null;
            List swatches;
            if (this.mBitmap != null) {
                Bitmap bitmap = this.scaleBitmapDown(this.mBitmap);
                if (logger != null) {
                    ((TimingLogger)logger).addSplit("Processed Bitmap");
                }

                Rect region = this.mRegion;
                if (bitmap != this.mBitmap && region != null) {
                    double scale = (double)bitmap.getWidth() / (double)this.mBitmap.getWidth();
                    region.left = (int)Math.floor((double)region.left * scale);
                    region.top = (int)Math.floor((double)region.top * scale);
                    region.right = Math.min((int)Math.ceil((double)region.right * scale), bitmap.getWidth());
                    region.bottom = Math.min((int)Math.ceil((double)region.bottom * scale), bitmap.getHeight());
                }

                ColorCutQuantizer quantizer = new ColorCutQuantizer(this.getPixelsFromBitmap(bitmap), this.mMaxColors, this.mFilters.isEmpty() ? null : (Palette.Filter[])this.mFilters.toArray(new Palette.Filter[this.mFilters.size()]));
                if (bitmap != this.mBitmap) {
                    bitmap.recycle();
                }

                swatches = quantizer.getQuantizedColors();
                if (logger != null) {
                    ((TimingLogger)logger).addSplit("Color quantization completed");
                }
            } else {
                if (this.mSwatches == null) {
                    throw new AssertionError();
                }

                swatches = this.mSwatches;
            }

            Palette p = new Palette(swatches, this.mTargets);
            p.generate();
            if (logger != null) {
                ((TimingLogger)logger).addSplit("Created Palette");
                ((TimingLogger)logger).dumpToLog();
            }

            return p;
        }

        @NonNull
        public AsyncTask<Bitmap, Void, Palette> generate(@NonNull final Palette.PaletteAsyncListener listener) {
            if (listener == null) {
                throw new IllegalArgumentException("listener can not be null");
            } else {
                return (new AsyncTask<Bitmap, Void, Palette>() {
                    @Nullable
                    protected Palette doInBackground(Bitmap... params) {
                        try {
                            return Builder.this.generate();
                        } catch (Exception var3) {
                            Log.e("Palette", "Exception thrown during async generate", var3);
                            return null;
                        }
                    }

                    protected void onPostExecute(@Nullable Palette colorExtractor) {
                        listener.onGenerated(colorExtractor);
                    }
                }).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Bitmap[]{this.mBitmap});
            }
        }

        private int[] getPixelsFromBitmap(Bitmap bitmap) {
            int bitmapWidth = bitmap.getWidth();
            int bitmapHeight = bitmap.getHeight();
            int[] pixels = new int[bitmapWidth * bitmapHeight];
            bitmap.getPixels(pixels, 0, bitmapWidth, 0, 0, bitmapWidth, bitmapHeight);
            if (this.mRegion == null) {
                return pixels;
            } else {
                int regionWidth = this.mRegion.width();
                int regionHeight = this.mRegion.height();
                int[] subsetPixels = new int[regionWidth * regionHeight];

                for(int row = 0; row < regionHeight; ++row) {
                    System.arraycopy(pixels, (row + this.mRegion.top) * bitmapWidth + this.mRegion.left, subsetPixels, row * regionWidth, regionWidth);
                }

                return subsetPixels;
            }
        }

        private Bitmap scaleBitmapDown(Bitmap bitmap) {
            double scaleRatio = -1.0D;
            int maxDimension;
            if (this.mResizeArea > 0) {
                maxDimension = bitmap.getWidth() * bitmap.getHeight();
                if (maxDimension > this.mResizeArea) {
                    scaleRatio = Math.sqrt((double)this.mResizeArea / (double)maxDimension);
                }
            } else if (this.mResizeMaxDimension > 0) {
                maxDimension = Math.max(bitmap.getWidth(), bitmap.getHeight());
                if (maxDimension > this.mResizeMaxDimension) {
                    scaleRatio = (double)this.mResizeMaxDimension / (double)maxDimension;
                }
            }

            return scaleRatio <= 0.0D ? bitmap : Bitmap.createScaledBitmap(bitmap, (int)Math.ceil((double)bitmap.getWidth() * scaleRatio), (int)Math.ceil((double)bitmap.getHeight() * scaleRatio), false);
        }
    }

    public static final class Swatch {
        private final int mRed;
        private final int mGreen;
        private final int mBlue;
        private final int mRgb;
        private final int mPopulation;
        private boolean mGeneratedTextColors;
        private int mTitleTextColor;
        private int mBodyTextColor;
        @Nullable
        private float[] mHsl;

        public Swatch(@ColorInt int color, int population) {
            this.mRed = Color.red(color);
            this.mGreen = Color.green(color);
            this.mBlue = Color.blue(color);
            this.mRgb = color;
            this.mPopulation = population;
        }

        Swatch(int red, int green, int blue, int population) {
            this.mRed = red;
            this.mGreen = green;
            this.mBlue = blue;
            this.mRgb = Color.rgb(red, green, blue);
            this.mPopulation = population;
        }

        Swatch(float[] hsl, int population) {
            this(ColorUtils.HSLToColor(hsl), population);
            this.mHsl = hsl;
        }

        @ColorInt
        public int getRgb() {
            return this.mRgb;
        }

        @NonNull
        public float[] getHsl() {
            if (this.mHsl == null) {
                this.mHsl = new float[3];
            }

            ColorUtils.RGBToHSL(this.mRed, this.mGreen, this.mBlue, this.mHsl);
            return this.mHsl;
        }

        public int getPopulation() {
            return this.mPopulation;
        }

        @ColorInt
        public int getTitleTextColor() {
            this.ensureTextColorsGenerated();
            return this.mTitleTextColor;
        }

        @ColorInt
        public int getBodyTextColor() {
            this.ensureTextColorsGenerated();
            return this.mBodyTextColor;
        }

        private void ensureTextColorsGenerated() {
            if (!this.mGeneratedTextColors) {
                int lightBodyAlpha = ColorUtils.calculateMinimumAlpha(-1, this.mRgb, 4.5F);
                int lightTitleAlpha = ColorUtils.calculateMinimumAlpha(-1, this.mRgb, 3.0F);
                if (lightBodyAlpha != -1 && lightTitleAlpha != -1) {
                    this.mBodyTextColor = ColorUtils.setAlphaComponent(-1, lightBodyAlpha);
                    this.mTitleTextColor = ColorUtils.setAlphaComponent(-1, lightTitleAlpha);
                    this.mGeneratedTextColors = true;
                    return;
                }

                int darkBodyAlpha = ColorUtils.calculateMinimumAlpha(-16777216, this.mRgb, 4.5F);
                int darkTitleAlpha = ColorUtils.calculateMinimumAlpha(-16777216, this.mRgb, 3.0F);
                if (darkBodyAlpha != -1 && darkTitleAlpha != -1) {
                    this.mBodyTextColor = ColorUtils.setAlphaComponent(-16777216, darkBodyAlpha);
                    this.mTitleTextColor = ColorUtils.setAlphaComponent(-16777216, darkTitleAlpha);
                    this.mGeneratedTextColors = true;
                    return;
                }

                this.mBodyTextColor = lightBodyAlpha != -1 ? ColorUtils.setAlphaComponent(-1, lightBodyAlpha) : ColorUtils.setAlphaComponent(-16777216, darkBodyAlpha);
                this.mTitleTextColor = lightTitleAlpha != -1 ? ColorUtils.setAlphaComponent(-1, lightTitleAlpha) : ColorUtils.setAlphaComponent(-16777216, darkTitleAlpha);
                this.mGeneratedTextColors = true;
            }

        }

        public String toString() {
            return this.getClass().getSimpleName() + " [RGB: #" + Integer.toHexString(this.getRgb()) + ']' + " [HSL: " + Arrays.toString(this.getHsl()) + ']' + " [Population: " + this.mPopulation + ']' + " [Title Text: #" + Integer.toHexString(this.getTitleTextColor()) + ']' + " [Body Text: #" + Integer.toHexString(this.getBodyTextColor()) + ']';
        }

        public boolean equals(Object o) {
            if (this == o) {
                return true;
            } else if (o != null && this.getClass() == o.getClass()) {
                Palette.Swatch swatch = (Palette.Swatch)o;
                return this.mPopulation == swatch.mPopulation && this.mRgb == swatch.mRgb;
            } else {
                return false;
            }
        }

        public int hashCode() {
            return 31 * this.mRgb + this.mPopulation;
        }
    }

    public interface PaletteAsyncListener {
        void onGenerated(@Nullable Palette var1);
    }
}

Target.java

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//


import android.support.annotation.FloatRange;
import android.support.annotation.NonNull;

public final class Target {
    private static final float TARGET_DARK_LUMA = 0.26F;
    private static final float MAX_DARK_LUMA = 0.45F;
    private static final float MIN_LIGHT_LUMA = 0.55F;
    private static final float TARGET_LIGHT_LUMA = 0.74F;
    private static final float MIN_NORMAL_LUMA = 0.3F;
    private static final float TARGET_NORMAL_LUMA = 0.5F;
    private static final float MAX_NORMAL_LUMA = 0.7F;
    private static final float TARGET_MUTED_SATURATION = 0.3F;
    private static final float MAX_MUTED_SATURATION = 0.4F;
    private static final float TARGET_VIBRANT_SATURATION = 1.0F;
    private static final float MIN_VIBRANT_SATURATION = 0.35F;
    private static final float WEIGHT_SATURATION = 0.24F;
    private static final float WEIGHT_LUMA = 0.52F;
    private static final float WEIGHT_POPULATION = 0.24F;
    static final int INDEX_MIN = 0;
    static final int INDEX_TARGET = 1;
    static final int INDEX_MAX = 2;
    static final int INDEX_WEIGHT_SAT = 0;
    static final int INDEX_WEIGHT_LUMA = 1;
    static final int INDEX_WEIGHT_POP = 2;
    public static final Target LIGHT_VIBRANT = new Target();
    public static final Target VIBRANT;
    public static final Target DARK_VIBRANT;
    public static final Target LIGHT_MUTED;
    public static final Target MUTED;
    public static final Target DARK_MUTED;
    final float[] mSaturationTargets = new float[3];
    final float[] mLightnessTargets = new float[3];
    final float[] mWeights = new float[3];
    boolean mIsExclusive = true;

    Target() {
        setTargetDefaultValues(this.mSaturationTargets);
        setTargetDefaultValues(this.mLightnessTargets);
        this.setDefaultWeights();
    }

    Target(@NonNull Target from) {
        System.arraycopy(from.mSaturationTargets, 0, this.mSaturationTargets, 0, this.mSaturationTargets.length);
        System.arraycopy(from.mLightnessTargets, 0, this.mLightnessTargets, 0, this.mLightnessTargets.length);
        System.arraycopy(from.mWeights, 0, this.mWeights, 0, this.mWeights.length);
    }

    @FloatRange(
        from = 0.0D,
        to = 1.0D
    )
    public float getMinimumSaturation() {
        return this.mSaturationTargets[0];
    }

    @FloatRange(
        from = 0.0D,
        to = 1.0D
    )
    public float getTargetSaturation() {
        return this.mSaturationTargets[1];
    }

    @FloatRange(
        from = 0.0D,
        to = 1.0D
    )
    public float getMaximumSaturation() {
        return this.mSaturationTargets[2];
    }

    @FloatRange(
        from = 0.0D,
        to = 1.0D
    )
    public float getMinimumLightness() {
        return this.mLightnessTargets[0];
    }

    @FloatRange(
        from = 0.0D,
        to = 1.0D
    )
    public float getTargetLightness() {
        return this.mLightnessTargets[1];
    }

    @FloatRange(
        from = 0.0D,
        to = 1.0D
    )
    public float getMaximumLightness() {
        return this.mLightnessTargets[2];
    }

    public float getSaturationWeight() {
        return this.mWeights[0];
    }

    public float getLightnessWeight() {
        return this.mWeights[1];
    }

    public float getPopulationWeight() {
        return this.mWeights[2];
    }

    public boolean isExclusive() {
        return this.mIsExclusive;
    }

    private static void setTargetDefaultValues(float[] values) {
        values[0] = 0.0F;
        values[1] = 0.5F;
        values[2] = 1.0F;
    }

    private void setDefaultWeights() {
        this.mWeights[0] = 0.24F;
        this.mWeights[1] = 0.52F;
        this.mWeights[2] = 0.24F;
    }

    void normalizeWeights() {
        float sum = 0.0F;
        int i = 0;

        int z;
        for(z = this.mWeights.length; i < z; ++i) {
            float weight = this.mWeights[i];
            if (weight > 0.0F) {
                sum += weight;
            }
        }

        if (sum != 0.0F) {
            i = 0;

            for(z = this.mWeights.length; i < z; ++i) {
                if (this.mWeights[i] > 0.0F) {
                    float[] var10000 = this.mWeights;
                    var10000[i] /= sum;
                }
            }
        }

    }

    private static void setDefaultDarkLightnessValues(Target target) {
        target.mLightnessTargets[1] = 0.26F;
        target.mLightnessTargets[2] = 0.45F;
    }

    private static void setDefaultNormalLightnessValues(Target target) {
        target.mLightnessTargets[0] = 0.3F;
        target.mLightnessTargets[1] = 0.5F;
        target.mLightnessTargets[2] = 0.7F;
    }

    private static void setDefaultLightLightnessValues(Target target) {
        target.mLightnessTargets[0] = 0.55F;
        target.mLightnessTargets[1] = 0.74F;
    }

    private static void setDefaultVibrantSaturationValues(Target target) {
        target.mSaturationTargets[0] = 0.35F;
        target.mSaturationTargets[1] = 1.0F;
    }

    private static void setDefaultMutedSaturationValues(Target target) {
        target.mSaturationTargets[1] = 0.3F;
        target.mSaturationTargets[2] = 0.4F;
    }

    static {
        setDefaultLightLightnessValues(LIGHT_VIBRANT);
        setDefaultVibrantSaturationValues(LIGHT_VIBRANT);
        VIBRANT = new Target();
        setDefaultNormalLightnessValues(VIBRANT);
        setDefaultVibrantSaturationValues(VIBRANT);
        DARK_VIBRANT = new Target();
        setDefaultDarkLightnessValues(DARK_VIBRANT);
        setDefaultVibrantSaturationValues(DARK_VIBRANT);
        LIGHT_MUTED = new Target();
        setDefaultLightLightnessValues(LIGHT_MUTED);
        setDefaultMutedSaturationValues(LIGHT_MUTED);
        MUTED = new Target();
        setDefaultNormalLightnessValues(MUTED);
        setDefaultMutedSaturationValues(MUTED);
        DARK_MUTED = new Target();
        setDefaultDarkLightnessValues(DARK_MUTED);
        setDefaultMutedSaturationValues(DARK_MUTED);
    }

    public static final class Builder {
        private final Target mTarget;

        public Builder() {
            this.mTarget = new Target();
        }

        public Builder(@NonNull Target target) {
            this.mTarget = new Target(target);
        }

        @NonNull
        public Target.Builder setMinimumSaturation(@FloatRange(from = 0.0D,to = 1.0D) float value) {
            this.mTarget.mSaturationTargets[0] = value;
            return this;
        }

        @NonNull
        public Target.Builder setTargetSaturation(@FloatRange(from = 0.0D,to = 1.0D) float value) {
            this.mTarget.mSaturationTargets[1] = value;
            return this;
        }

        @NonNull
        public Target.Builder setMaximumSaturation(@FloatRange(from = 0.0D,to = 1.0D) float value) {
            this.mTarget.mSaturationTargets[2] = value;
            return this;
        }

        @NonNull
        public Target.Builder setMinimumLightness(@FloatRange(from = 0.0D,to = 1.0D) float value) {
            this.mTarget.mLightnessTargets[0] = value;
            return this;
        }

        @NonNull
        public Target.Builder setTargetLightness(@FloatRange(from = 0.0D,to = 1.0D) float value) {
            this.mTarget.mLightnessTargets[1] = value;
            return this;
        }

        @NonNull
        public Target.Builder setMaximumLightness(@FloatRange(from = 0.0D,to = 1.0D) float value) {
            this.mTarget.mLightnessTargets[2] = value;
            return this;
        }

        @NonNull
        public Target.Builder setSaturationWeight(@FloatRange(from = 0.0D) float weight) {
            this.mTarget.mWeights[0] = weight;
            return this;
        }

        @NonNull
        public Target.Builder setLightnessWeight(@FloatRange(from = 0.0D) float weight) {
            this.mTarget.mWeights[1] = weight;
            return this;
        }

        @NonNull
        public Target.Builder setPopulationWeight(@FloatRange(from = 0.0D) float weight) {
            this.mTarget.mWeights[2] = weight;
            return this;
        }

        @NonNull
        public Target.Builder setExclusive(boolean exclusive) {
            this.mTarget.mIsExclusive = exclusive;
            return this;
        }

        @NonNull
        public Target build() {
            return this.mTarget;
        }
    }
}

ColorCutQuantizer.java

/*
 * 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.
 */

import android.graphics.Color;
import android.support.v4.graphics.ColorUtils;
import android.util.TimingLogger;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.PriorityQueue;

/**
 * An color quantizer based on the Median-cut algorithm, but optimized for picking out distinct
 * colors rather than representation colors.
 *
 * The color space is represented as a 3-dimensional cube with each dimension being an RGB
 * component. The cube is then repeatedly divided until we have reduced the color space to the
 * requested number of colors. An average color is then generated from each cube.
 *
 * What makes this different to median-cut is that median-cut divided cubes so that all of the cubes
 * have roughly the same population, where this quantizer divides boxes based on their color volume.
 * This means that the color space is divided into distinct colors, rather than representative
 * colors.
 */
final class ColorCutQuantizer {

    private static final String LOG_TAG = "ColorCutQuantizer";
    private static final boolean LOG_TIMINGS = false;

    private static final int COMPONENT_RED = -3;
    private static final int COMPONENT_GREEN = -2;
    private static final int COMPONENT_BLUE = -1;

    private static final int QUANTIZE_WORD_WIDTH = 5;
    private static final int QUANTIZE_WORD_MASK = (1 << QUANTIZE_WORD_WIDTH) - 1;

    final int[] mColors;
    final int[] mHistogram;
    final List<Palette.Swatch> mQuantizedColors;
    final TimingLogger mTimingLogger;
    final Palette.Filter[] mFilters;

    private final float[] mTempHsl = new float[3];

    /**
     * Constructor.
     *
     * @param pixels histogram representing an image's pixel data
     * @param maxColors The maximum number of colors that should be in the result palette.
     * @param filters Set of filters to use in the quantization stage
     */
    ColorCutQuantizer(final int[] pixels, final int maxColors, final Palette.Filter[] filters) {
        mTimingLogger = LOG_TIMINGS ? new TimingLogger(LOG_TAG, "Creation") : null;
        mFilters = filters;

        final int[] hist = mHistogram = new int[1 << (QUANTIZE_WORD_WIDTH * 3)];
        for (int i = 0; i < pixels.length; i++) {
            final int quantizedColor = quantizeFromRgb888(pixels[i]);
            // Now update the pixel value to the quantized value
            pixels[i] = quantizedColor;
            // And update the histogram
            hist[quantizedColor]++;
        }

        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Histogram created");
        }

        // Now let's count the number of distinct colors
        int distinctColorCount = 0;
        for (int color = 0; color < hist.length; color++) {
            if (hist[color] > 0 && shouldIgnoreColor(color)) {
                // If we should ignore the color, set the population to 0
                hist[color] = 0;
            }
            if (hist[color] > 0) {
                // If the color has population, increase the distinct color count
                distinctColorCount++;
            }
        }

        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Filtered colors and distinct colors counted");
        }

        // Now lets go through create an array consisting of only distinct colors
        final int[] colors = mColors = new int[distinctColorCount];
        int distinctColorIndex = 0;
        for (int color = 0; color < hist.length; color++) {
            if (hist[color] > 0) {
                colors[distinctColorIndex++] = color;
            }
        }

        if (LOG_TIMINGS) {
            mTimingLogger.addSplit("Distinct colors copied into array");
        }

        if (distinctColorCount <= maxColors) {
            // The image has fewer colors than the maximum requested, so just return the colors
            mQuantizedColors = new ArrayList<>();
            for (int color : colors) {
                mQuantizedColors.add(new Palette.Swatch(approximateToRgb888(color), hist[color]));
            }

            if (LOG_TIMINGS) {
                mTimingLogger.addSplit("Too few colors present. Copied to Swatches");
                mTimingLogger.dumpToLog();
            }
        } else {
            // We need use quantization to reduce the number of colors
            mQuantizedColors = quantizePixels(maxColors);

            if (LOG_TIMINGS) {
                mTimingLogger.addSplit("Quantized colors computed");
                mTimingLogger.dumpToLog();
            }
        }
    }

    /**
     * @return the list of quantized colors
     */
    List<Palette.Swatch> getQuantizedColors() {
        return mQuantizedColors;
    }

    private List<Palette.Swatch> quantizePixels(int maxColors) {
        // Create the priority queue which is sorted by volume descending. This means we always
        // split the largest box in the queue
        final PriorityQueue<Vbox> pq = new PriorityQueue<>(maxColors, VBOX_COMPARATOR_VOLUME);

        // To start, offer a box which contains all of the colors
        pq.offer(new Vbox(0, mColors.length - 1));

        // Now go through the boxes, splitting them until we have reached maxColors or there are no
        // more boxes to split
        splitBoxes(pq, maxColors);

        // Finally, return the average colors of the color boxes
        return generateAverageColors(pq);
    }

    /**
     * Iterate through the {@link java.util.Queue}, popping
     * {@link ColorCutQuantizer.Vbox} objects from the queue
     * and splitting them. Once split, the new box and the remaining box are offered back to the
     * queue.
     *
     * @param queue {@link java.util.PriorityQueue} to poll for boxes
     * @param maxSize Maximum amount of boxes to split
     */
    private void splitBoxes(final PriorityQueue<Vbox> queue, final int maxSize) {
        while (queue.size() < maxSize) {
            final Vbox vbox = queue.poll();

            if (vbox != null && vbox.canSplit()) {
                // First split the box, and offer the result
                queue.offer(vbox.splitBox());

                if (LOG_TIMINGS) {
                    mTimingLogger.addSplit("Box split");
                }
                // Then offer the box back
                queue.offer(vbox);
            } else {
                if (LOG_TIMINGS) {
                    mTimingLogger.addSplit("All boxes split");
                }
                // If we get here then there are no more boxes to split, so return
                return;
            }
        }
    }

    private List<Palette.Swatch> generateAverageColors(Collection<Vbox> vboxes) {
        ArrayList<Palette.Swatch> colors = new ArrayList<>(vboxes.size());
        for (Vbox vbox : vboxes) {
            Palette.Swatch swatch = vbox.getAverageColor();
            if (!shouldIgnoreColor(swatch)) {
                // As we're averaging a color box, we can still get colors which we do not want, so
                // we check again here
                colors.add(swatch);
            }
        }
        return colors;
    }

    /**
     * Represents a tightly fitting box around a color space.
     */
    private class Vbox {
        // lower and upper index are inclusive
        private int mLowerIndex;
        private int mUpperIndex;
        // Population of colors within this box
        private int mPopulation;

        private int mMinRed, mMaxRed;
        private int mMinGreen, mMaxGreen;
        private int mMinBlue, mMaxBlue;

        Vbox(int lowerIndex, int upperIndex) {
            mLowerIndex = lowerIndex;
            mUpperIndex = upperIndex;
            fitBox();
        }

        final int getVolume() {
            return (mMaxRed - mMinRed + 1) * (mMaxGreen - mMinGreen + 1) *
                    (mMaxBlue - mMinBlue + 1);
        }

        final boolean canSplit() {
            return getColorCount() > 1;
        }

        final int getColorCount() {
            return 1 + mUpperIndex - mLowerIndex;
        }

        /**
         * Recomputes the boundaries of this box to tightly fit the colors within the box.
         */
        final void fitBox() {
            final int[] colors = mColors;
            final int[] hist = mHistogram;

            // Reset the min and max to opposite values
            int minRed, minGreen, minBlue;
            minRed = minGreen = minBlue = Integer.MAX_VALUE;
            int maxRed, maxGreen, maxBlue;
            maxRed = maxGreen = maxBlue = Integer.MIN_VALUE;
            int count = 0;

            for (int i = mLowerIndex; i <= mUpperIndex; i++) {
                final int color = colors[i];
                count += hist[color];

                final int r = quantizedRed(color);
                final int g = quantizedGreen(color);
                final int b = quantizedBlue(color);
                if (r > maxRed) {
                    maxRed = r;
                }
                if (r < minRed) {
                    minRed = r;
                }
                if (g > maxGreen) {
                    maxGreen = g;
                }
                if (g < minGreen) {
                    minGreen = g;
                }
                if (b > maxBlue) {
                    maxBlue = b;
                }
                if (b < minBlue) {
                    minBlue = b;
                }
            }

            mMinRed = minRed;
            mMaxRed = maxRed;
            mMinGreen = minGreen;
            mMaxGreen = maxGreen;
            mMinBlue = minBlue;
            mMaxBlue = maxBlue;
            mPopulation = count;
        }

        /**
         * Split this color box at the mid-point along it's longest dimension
         *
         * @return the new ColorBox
         */
        final Vbox splitBox() {
            if (!canSplit()) {
                throw new IllegalStateException("Can not split a box with only 1 color");
            }

            // find median along the longest dimension
            final int splitPoint = findSplitPoint();

            Vbox newBox = new Vbox(splitPoint + 1, mUpperIndex);

            // Now change this box's upperIndex and recompute the color boundaries
            mUpperIndex = splitPoint;
            fitBox();

            return newBox;
        }

        /**
         * @return the dimension which this box is largest in
         */
        final int getLongestColorDimension() {
            final int redLength = mMaxRed - mMinRed;
            final int greenLength = mMaxGreen - mMinGreen;
            final int blueLength = mMaxBlue - mMinBlue;

            if (redLength >= greenLength && redLength >= blueLength) {
                return COMPONENT_RED;
            } else if (greenLength >= redLength && greenLength >= blueLength) {
                return COMPONENT_GREEN;
            } else {
                return COMPONENT_BLUE;
            }
        }

        /**
         * Finds the point within this box's lowerIndex and upperIndex index of where to split.
         *
         * This is calculated by finding the longest color dimension, and then sorting the
         * sub-array based on that dimension value in each color. The colors are then iterated over
         * until a color is found with at least the midpoint of the whole box's dimension midpoint.
         *
         * @return the index of the colors array to split from
         */
        final int findSplitPoint() {
            final int longestDimension = getLongestColorDimension();
            final int[] colors = mColors;
            final int[] hist = mHistogram;

            // We need to sort the colors in this box based on the longest color dimension.
            // As we can't use a Comparator to define the sort logic, we modify each color so that
            // it's most significant is the desired dimension
            modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);

            // Now sort... Arrays.sort uses a exclusive toIndex so we need to add 1
            Arrays.sort(colors, mLowerIndex, mUpperIndex + 1);

            // Now revert all of the colors so that they are packed as RGB again
            modifySignificantOctet(colors, longestDimension, mLowerIndex, mUpperIndex);

            final int midPoint = mPopulation / 2;
            for (int i = mLowerIndex, count = 0; i <= mUpperIndex; i++)  {
                count += hist[colors[i]];
                if (count >= midPoint) {
                    return i;
                }
            }

            return mLowerIndex;
        }

        /**
         * @return the average color of this box.
         */
        final Palette.Swatch getAverageColor() {
            final int[] colors = mColors;
            final int[] hist = mHistogram;
            int redSum = 0;
            int greenSum = 0;
            int blueSum = 0;
            int totalPopulation = 0;

            for (int i = mLowerIndex; i <= mUpperIndex; i++) {
                final int color = colors[i];
                final int colorPopulation = hist[color];

                totalPopulation += colorPopulation;
                redSum += colorPopulation * quantizedRed(color);
                greenSum += colorPopulation * quantizedGreen(color);
                blueSum += colorPopulation * quantizedBlue(color);
            }

            final int redMean = Math.round(redSum / (float) totalPopulation);
            final int greenMean = Math.round(greenSum / (float) totalPopulation);
            final int blueMean = Math.round(blueSum / (float) totalPopulation);

            return new Palette.Swatch(approximateToRgb888(redMean, greenMean, blueMean), totalPopulation);
        }
    }

    /**
     * Modify the significant octet in a packed color int. Allows sorting based on the value of a
     * single color component. This relies on all components being the same word size.
     *
     * @see Vbox#findSplitPoint()
     */
    private static void modifySignificantOctet(final int[] a, final int dimension,
            final int lower, final int upper) {
        switch (dimension) {
            case COMPONENT_RED:
                // Already in RGB, no need to do anything
                break;
            case COMPONENT_GREEN:
                // We need to do a RGB to GRB swap, or vice-versa
                for (int i = lower; i <= upper; i++) {
                    final int color = a[i];
                    a[i] = quantizedGreen(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
                            | quantizedRed(color) << QUANTIZE_WORD_WIDTH
                            | quantizedBlue(color);
                }
                break;
            case COMPONENT_BLUE:
                // We need to do a RGB to BGR swap, or vice-versa
                for (int i = lower; i <= upper; i++) {
                    final int color = a[i];
                    a[i] = quantizedBlue(color) << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)
                            | quantizedGreen(color) << QUANTIZE_WORD_WIDTH
                            | quantizedRed(color);
                }
                break;
        }
    }

    private boolean shouldIgnoreColor(int color565) {
        final int rgb = approximateToRgb888(color565);
        ColorUtils.colorToHSL(rgb, mTempHsl);
        return shouldIgnoreColor(rgb, mTempHsl);
    }

    private boolean shouldIgnoreColor(Palette.Swatch color) {
        return shouldIgnoreColor(color.getRgb(), color.getHsl());
    }

    private boolean shouldIgnoreColor(int rgb, float[] hsl) {
        if (mFilters != null && mFilters.length > 0) {
            for (int i = 0, count = mFilters.length; i < count; i++) {
                if (!mFilters[i].isAllowed(rgb, hsl)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Comparator which sorts {@link Vbox} instances based on their volume, in descending order
     */
    private static final Comparator<Vbox> VBOX_COMPARATOR_VOLUME = new Comparator<Vbox>() {
        @Override
        public int compare(Vbox lhs, Vbox rhs) {
            return rhs.getVolume() - lhs.getVolume();
        }
    };

    /**
     * Quantized a RGB888 value to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
     */
    private static int quantizeFromRgb888(int color) {
        int r = modifyWordWidth(Color.red(color), 8, QUANTIZE_WORD_WIDTH);
        int g = modifyWordWidth(Color.green(color), 8, QUANTIZE_WORD_WIDTH);
        int b = modifyWordWidth(Color.blue(color), 8, QUANTIZE_WORD_WIDTH);
        return r << (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH) | g << QUANTIZE_WORD_WIDTH | b;
    }

    /**
     * Quantized RGB888 values to have a word width of {@value #QUANTIZE_WORD_WIDTH}.
     */
    private static int approximateToRgb888(int r, int g, int b) {
        return Color.rgb(modifyWordWidth(r, QUANTIZE_WORD_WIDTH, 8),
                modifyWordWidth(g, QUANTIZE_WORD_WIDTH, 8),
                modifyWordWidth(b, QUANTIZE_WORD_WIDTH, 8));
    }

    private static int approximateToRgb888(int color) {
        return approximateToRgb888(quantizedRed(color), quantizedGreen(color), quantizedBlue(color));
    }

    /**
     * @return red component of the quantized color
     */
    private static int quantizedRed(int color) {
        return (color >> (QUANTIZE_WORD_WIDTH + QUANTIZE_WORD_WIDTH)) & QUANTIZE_WORD_MASK;
    }

    /**
     * @return green component of a quantized color
     */
    private static int quantizedGreen(int color) {
        return (color >> QUANTIZE_WORD_WIDTH) & QUANTIZE_WORD_MASK;
    }

    /**
     * @return blue component of a quantized color
     */
    private static int quantizedBlue(int color) {
        return color & QUANTIZE_WORD_MASK;
    }

    private static int modifyWordWidth(int value, int currentWidth, int targetWidth) {
        final int newValue;
        if (targetWidth > currentWidth) {
            // If we're approximating up in word width, we'll shift up
            newValue = value << (targetWidth - currentWidth);
        } else {
            // Else, we will just shift and keep the MSB
            newValue = value >> (currentWidth - targetWidth);
        }
        return newValue & ((1 << targetWidth) - 1);
    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章