1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License
15 */
16
17package com.android.internal.colorextraction.drawable;
18
19import android.animation.Animator;
20import android.animation.AnimatorListenerAdapter;
21import android.animation.ValueAnimator;
22import android.annotation.NonNull;
23import android.annotation.Nullable;
24import android.content.Context;
25import android.graphics.Canvas;
26import android.graphics.ColorFilter;
27import android.graphics.Paint;
28import android.graphics.PixelFormat;
29import android.graphics.RadialGradient;
30import android.graphics.Rect;
31import android.graphics.Shader;
32import android.graphics.Xfermode;
33import android.graphics.drawable.Drawable;
34import android.view.animation.DecelerateInterpolator;
35
36import com.android.internal.annotations.VisibleForTesting;
37import com.android.internal.colorextraction.ColorExtractor;
38import com.android.internal.graphics.ColorUtils;
39
40/**
41 * Draws a gradient based on a Palette
42 */
43public class GradientDrawable extends Drawable {
44    private static final String TAG = "GradientDrawable";
45
46    private static final float CENTRALIZED_CIRCLE_1 = -2;
47    private static final int GRADIENT_RADIUS = 480; // in dp
48    private static final long COLOR_ANIMATION_DURATION = 2000;
49
50    private int mAlpha = 255;
51
52    private float mDensity;
53    private final Paint mPaint;
54    private final Rect mWindowBounds;
55    private final Splat mSplat;
56
57    private int mMainColor;
58    private int mSecondaryColor;
59    private ValueAnimator mColorAnimation;
60    private int mMainColorTo;
61    private int mSecondaryColorTo;
62
63    public GradientDrawable(@NonNull Context context) {
64        mDensity = context.getResources().getDisplayMetrics().density;
65        mSplat = new Splat(0.50f, 1.00f, GRADIENT_RADIUS, CENTRALIZED_CIRCLE_1);
66        mWindowBounds = new Rect();
67
68        mPaint = new Paint();
69        mPaint.setStyle(Paint.Style.FILL);
70    }
71
72    public void setColors(@NonNull ColorExtractor.GradientColors colors) {
73        setColors(colors.getMainColor(), colors.getSecondaryColor(), true);
74    }
75
76    public void setColors(@NonNull ColorExtractor.GradientColors colors, boolean animated) {
77        setColors(colors.getMainColor(), colors.getSecondaryColor(), animated);
78    }
79
80    public void setColors(int mainColor, int secondaryColor, boolean animated) {
81        if (mainColor == mMainColorTo && secondaryColor == mSecondaryColorTo) {
82            return;
83        }
84
85        if (mColorAnimation != null && mColorAnimation.isRunning()) {
86            mColorAnimation.cancel();
87        }
88
89        mMainColorTo = mainColor;
90        mSecondaryColorTo = mainColor;
91
92        if (animated) {
93            final int mainFrom = mMainColor;
94            final int secFrom = mSecondaryColor;
95
96            ValueAnimator anim = ValueAnimator.ofFloat(0, 1);
97            anim.setDuration(COLOR_ANIMATION_DURATION);
98            anim.addUpdateListener(animation -> {
99                float ratio = (float) animation.getAnimatedValue();
100                mMainColor = ColorUtils.blendARGB(mainFrom, mainColor, ratio);
101                mSecondaryColor = ColorUtils.blendARGB(secFrom, secondaryColor, ratio);
102                buildPaints();
103                invalidateSelf();
104            });
105            anim.addListener(new AnimatorListenerAdapter() {
106                @Override
107                public void onAnimationEnd(Animator animation, boolean isReverse) {
108                    if (mColorAnimation == animation) {
109                        mColorAnimation = null;
110                    }
111                }
112            });
113            anim.setInterpolator(new DecelerateInterpolator());
114            anim.start();
115            mColorAnimation = anim;
116        } else {
117            mMainColor = mainColor;
118            mSecondaryColor = secondaryColor;
119            buildPaints();
120            invalidateSelf();
121        }
122    }
123
124    @Override
125    public void setAlpha(int alpha) {
126        if (alpha != mAlpha) {
127            mAlpha = alpha;
128            mPaint.setAlpha(mAlpha);
129            invalidateSelf();
130        }
131    }
132
133    @Override
134    public int getAlpha() {
135        return mAlpha;
136    }
137
138    @Override
139    public void setXfermode(@Nullable Xfermode mode) {
140        mPaint.setXfermode(mode);
141        invalidateSelf();
142    }
143
144    @Override
145    public void setColorFilter(ColorFilter colorFilter) {
146        mPaint.setColorFilter(colorFilter);
147    }
148
149    @Override
150    public ColorFilter getColorFilter() {
151        return mPaint.getColorFilter();
152    }
153
154    @Override
155    public int getOpacity() {
156        return PixelFormat.TRANSLUCENT;
157    }
158
159    public void setScreenSize(int width, int height) {
160        mWindowBounds.set(0, 0, width, height);
161        setBounds(0, 0, width, height);
162        buildPaints();
163    }
164
165    private void buildPaints() {
166        Rect bounds = mWindowBounds;
167        if (bounds.width() == 0) {
168            return;
169        }
170
171        float w = bounds.width();
172        float h = bounds.height();
173
174        float x = mSplat.x * w;
175        float y = mSplat.y * h;
176
177        float radius = mSplat.radius * mDensity;
178
179        // When we have only a single alpha gradient, we increase quality
180        // (avoiding banding) by merging the background solid color into
181        // the gradient directly
182        RadialGradient radialGradient = new RadialGradient(x, y, radius,
183                mSecondaryColor, mMainColor, Shader.TileMode.CLAMP);
184        mPaint.setShader(radialGradient);
185    }
186
187    @Override
188    public void draw(@NonNull Canvas canvas) {
189        Rect bounds = mWindowBounds;
190        if (bounds.width() == 0) {
191            throw new IllegalStateException("You need to call setScreenSize before drawing.");
192        }
193
194        // Splat each gradient
195        float w = bounds.width();
196        float h = bounds.height();
197
198        float x = mSplat.x * w;
199        float y = mSplat.y * h;
200
201        float radius = Math.max(w, h);
202        canvas.drawRect(x - radius, y - radius, x + radius, y + radius, mPaint);
203    }
204
205    @VisibleForTesting
206    public int getMainColor() {
207        return mMainColor;
208    }
209
210    @VisibleForTesting
211    public int getSecondaryColor() {
212        return mSecondaryColor;
213    }
214
215    static final class Splat {
216        final float x;
217        final float y;
218        final float radius;
219        final float colorIndex;
220
221        Splat(float x, float y, float radius, float colorIndex) {
222            this.x = x;
223            this.y = y;
224            this.radius = radius;
225            this.colorIndex = colorIndex;
226        }
227    }
228}