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

Commit 65f56581 authored by Lucas Dupin's avatar Lucas Dupin
Browse files

Color extraction shareable library

Contains a class to listen to color updates and a
drawable to render a gradient.

Bug: 36856508
Test: manual
Change-Id: I4e54dea770fb55f4f3d0e67d63bd38876a37c884
parent aff81954
Loading
Loading
Loading
Loading
+26 −0
Original line number Diff line number Diff line
# 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.

LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)

LOCAL_USE_AAPT2 := true
LOCAL_SRC_FILES := $(call all-java-files-under, src)
LOCAL_STATIC_ANDROID_LIBRARIES := android-support-annotations \
     android-support-v7-palette \
     android-support-v4
LOCAL_MODULE := colorextraction

include $(BUILD_STATIC_JAVA_LIBRARY)
+21 −0
Original line number Diff line number Diff line
<?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.
  -->

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.google.android.colorextraction">
</manifest>
+170 −0
Original line number Diff line number Diff line
/*
 * 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.google.android.colorextraction;

import android.app.WallpaperColors;
import android.app.WallpaperManager;
import android.content.Context;
import android.graphics.Color;
import android.util.Log;

import com.google.android.colorextraction.types.ExtractionType;
import com.google.android.colorextraction.types.Tonal;

/**
 * Class to process wallpaper colors and generate a tonal palette based on them.
 */
public class ColorExtractor implements WallpaperManager.OnColorsChangedListener {
    private static final String TAG = "ColorExtractor";
    private static final int FALLBACK_COLOR = Color.BLACK;

    private int mMainFallbackColor = FALLBACK_COLOR;
    private int mSecondaryFallbackColor = FALLBACK_COLOR;
    private final GradientColors mSystemColors;
    private final GradientColors mLockColors;
    private final Context mContext;
    private final ExtractionType mExtractionType;
    private OnColorsChangedListener mListener;

    public ColorExtractor(Context context) {
        mContext = context;
        mSystemColors = new GradientColors();
        mLockColors = new GradientColors();
        mExtractionType = new Tonal();

        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);

        if (wallpaperManager == null) {
            Log.w(TAG, "Can't listen to color changes!");
        } else {
            wallpaperManager.addOnColorsChangedListener(this);
            extractInto(wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM),
                    mSystemColors);
            extractInto(wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_LOCK),
                    mLockColors);
        }
    }

    public GradientColors getColors(int which) {
        if (which == WallpaperManager.FLAG_LOCK) {
            return mLockColors;
        } else if (which == WallpaperManager.FLAG_SYSTEM) {
            return mSystemColors;
        } else {
            throw new IllegalArgumentException("which should be either FLAG_SYSTEM or FLAG_LOCK");
        }
    }

    public void setListener(OnColorsChangedListener listener) {
        mListener = listener;
    }

    @Override
    public void onColorsChanged(WallpaperColors colors, int which) {
        if ((which & WallpaperManager.FLAG_LOCK) != 0) {
            extractInto(colors, mLockColors);
            if (mListener != null) {
                mListener.onColorsChanged(mLockColors, WallpaperManager.FLAG_LOCK);
            }
        }
        if ((which & WallpaperManager.FLAG_SYSTEM) != 0) {
            extractInto(colors, mSystemColors);
            if (mListener != null) {
                mListener.onColorsChanged(mSystemColors, WallpaperManager.FLAG_SYSTEM);
            }
        }
    }

    private void extractInto(WallpaperColors inWallpaperColors, GradientColors outGradientColors) {
        applyFallback(outGradientColors);
        if (inWallpaperColors == null) {
            return;
        }
        mExtractionType.extractInto(inWallpaperColors, outGradientColors);
    }

    private void applyFallback(GradientColors outGradientColors) {
        outGradientColors.setMainColor(mMainFallbackColor);
        outGradientColors.setSecondaryColor(mSecondaryFallbackColor);
    }

    public void destroy() {
        WallpaperManager wallpaperManager = mContext.getSystemService(WallpaperManager.class);
        if (wallpaperManager != null) {
            wallpaperManager.removeOnColorsChangedListener(this);
        }
    }

    public static class GradientColors {
        private int mMainColor = FALLBACK_COLOR;
        private int mSecondaryColor = FALLBACK_COLOR;
        private boolean mSupportsDarkText;

        public void setMainColor(int mainColor) {
            mMainColor = mainColor;
        }

        public void setSecondaryColor(int secondaryColor) {
            mSecondaryColor = secondaryColor;
        }

        public void setSupportsDarkText(boolean supportsDarkText) {
            mSupportsDarkText = supportsDarkText;
        }

        public void set(GradientColors other) {
            mMainColor = other.mMainColor;
            mSecondaryColor = other.mSecondaryColor;
            mSupportsDarkText = other.mSupportsDarkText;
        }

        public int getMainColor() {
            return mMainColor;
        }

        public int getSecondaryColor() {
            return mSecondaryColor;
        }

        public boolean supportsDarkText() {
            return mSupportsDarkText;
        }

        @Override
        public boolean equals(Object o) {
            if (o == null || o.getClass() != getClass()) {
                return false;
            }
            GradientColors other = (GradientColors) o;
            return other.mMainColor == mMainColor &&
                    other.mSecondaryColor == mSecondaryColor &&
                    other.mSupportsDarkText == mSupportsDarkText;
        }

        @Override
        public int hashCode() {
            int code = mMainColor;
            code = 31 * code + mSecondaryColor;
            code = 31 * code + (mSupportsDarkText ? 0 : 1);
            return code;
        }
    }

    public interface OnColorsChangedListener {
        void onColorsChanged(GradientColors colors, int which);
    }
}
+206 −0
Original line number Diff line number Diff line
/*
 * 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.google.android.colorextraction.drawable;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.RadialGradient;
import android.graphics.Rect;
import android.graphics.Shader;
import android.graphics.Xfermode;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.graphics.ColorUtils;
import android.view.animation.DecelerateInterpolator;

import com.google.android.colorextraction.ColorExtractor;

/**
 * Draws a gradient based on a Palette
 */
public class GradientDrawable extends Drawable {
    private static final String TAG = "GradientDrawable";

    private static final float CENTRALIZED_CIRCLE_1 = -2;
    private static final int GRADIENT_RADIUS = 480; // in dp
    private static final long COLOR_ANIMATION_DURATION = 2000;

    private int mAlpha = 255;

    private float mDensity;
    private final Paint mPaint;
    private final Rect mWindowBounds;
    private final Splat mSplat;

    private int mMainColor;
    private int mSecondaryColor;
    private ValueAnimator mColorAnimation;

    public GradientDrawable(@NonNull Context context) {
        mDensity = context.getResources().getDisplayMetrics().density;
        mSplat = new Splat(0.50f, 1.00f, GRADIENT_RADIUS, CENTRALIZED_CIRCLE_1);
        mWindowBounds = new Rect();

        mPaint = new Paint();
        mPaint.setStyle(Paint.Style.FILL);
    }

    public void setColors(@NonNull ColorExtractor.GradientColors colors) {
        setColors(colors.getMainColor(), colors.getSecondaryColor(), true);
    }

    public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) {
        setColors(colors.getMainColor(), colors.getSecondaryColor(), animated);
    }

    public void setColors(int mainColor, int secondaryColor, boolean animated) {
        if (mainColor == mMainColor && secondaryColor == mSecondaryColor) {
            return;
        }

        if (mColorAnimation != null && mColorAnimation.isRunning()) {
            mColorAnimation.cancel();
        }

        if (animated) {
            final int mainFrom = mMainColor;
            final int secFrom = mSecondaryColor;

            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
            anim.setDuration(COLOR_ANIMATION_DURATION);
            anim.addUpdateListener(animation -> {
                float ratio = (float) animation.getAnimatedValue();
                mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio);
                mSecondaryColor = ColorUtils.blendARGB(secFrom, secondaryColor, ratio);
                buildPaints();
                invalidateSelf();
            });
            anim.addListener(new AnimatorListenerAdapter() {
                @Override
                public void onAnimationEnd(Animator animation, boolean isReverse) {
                    if (mColorAnimation == animation) {
                        mColorAnimation = null;
                    }
                }
            });
            anim.setInterpolator(new DecelerateInterpolator());
            anim.start();
            mColorAnimation = anim;
        } else {
            mMainColor = mainColor;
            mSecondaryColor = secondaryColor;
            buildPaints();
            invalidateSelf();
        }
    }

    @Override
    public void setAlpha(int alpha) {
        if (alpha != mAlpha) {
            mAlpha = alpha;
            mPaint.setAlpha(mAlpha);
            invalidateSelf();
        }
    }

    @Override
    public int getAlpha() {
        return mAlpha;
    }

    @Override
    public void setXfermode(@Nullable Xfermode mode) {
        mPaint.setXfermode(mode);
        invalidateSelf();
    }

    @Override
    public void setColorFilter(ColorFilter colorFilter) {
        mPaint.setColorFilter(colorFilter);
    }

    @Override
    public ColorFilter getColorFilter() {
        return mPaint.getColorFilter();
    }

    @Override
    public int getOpacity() {
        return mAlpha == 255 ? PixelFormat.OPAQUE : PixelFormat.TRANSLUCENT;
    }

    public void setScreenSize(int width, int height) {
        mWindowBounds.set(0, 0, width, height);
        setBounds(0, 0, width, height);
        buildPaints();
    }

    private void buildPaints() {
        final Rect bounds = mWindowBounds;

        float w = bounds.width();
        float h = bounds.height();

        float x = mSplat.x * w;
        float y = mSplat.y * h;

        float radius = mSplat.radius * mDensity;

        // When we have only a single alpha gradient, we increase quality
        // (avoiding banding) by merging the background solid color into
        // the gradient directly
        RadialGradient radialGradient = new RadialGradient(x, y, radius,
                mMainColor, mSecondaryColor, Shader.TileMode.CLAMP);
        mPaint.setShader(radialGradient);
    }

    @Override
    public void draw(@NonNull Canvas canvas) {
        final Rect bounds = mWindowBounds;

        // Splat each gradient
        float w = bounds.width();
        float h = bounds.height();

        float x = mSplat.x * w;
        float y = mSplat.y * h;

        float radius = Math.max(w, h);
        canvas.drawRect(x - radius, y - radius, x + radius, y + radius, mPaint);
    }

    static final class Splat {
        final float x;
        final float y;
        final float radius;
        final float colorIndex;

        Splat(float x, float y, float radius, float colorIndex) {
            this.x = x;
            this.y = y;
            this.radius = radius;
            this.colorIndex = colorIndex;
        }
    }
}
 No newline at end of file
+37 −0
Original line number Diff line number Diff line
/*
 * 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.google.android.colorextraction.types;

import android.app.WallpaperColors;

import com.google.android.colorextraction.ColorExtractor;

/**
 * Interface to allow various color extraction implementations.
 */
public interface ExtractionType {

    /**
     * Executes color extraction by reading WallpaperColors and setting
     * main and secondary colors on GradientColors.
     *
     * @param inWallpaperColors where to read from
     * @param outGradientColors object that should receive the colors
     */
    void extractInto(WallpaperColors inWallpaperColors,
            ColorExtractor.GradientColors outGradientColors);
}
Loading