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}