1/* 2 * Copyright (C) 2014 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.systemui.statusbar.policy; 18 19import android.animation.Animator; 20import android.animation.AnimatorListenerAdapter; 21import android.animation.ObjectAnimator; 22import android.content.Context; 23import android.graphics.Canvas; 24import android.graphics.CanvasProperty; 25import android.graphics.ColorFilter; 26import android.graphics.Paint; 27import android.graphics.PixelFormat; 28import android.graphics.drawable.Drawable; 29import android.view.DisplayListCanvas; 30import android.view.RenderNodeAnimator; 31import android.view.View; 32import android.view.animation.Interpolator; 33 34import com.android.systemui.Interpolators; 35import com.android.systemui.R; 36 37import java.util.ArrayList; 38import java.util.HashSet; 39 40public class KeyButtonRipple extends Drawable { 41 42 private static final float GLOW_MAX_SCALE_FACTOR = 1.35f; 43 private static final float GLOW_MAX_ALPHA = 0.2f; 44 private static final int ANIMATION_DURATION_SCALE = 350; 45 private static final int ANIMATION_DURATION_FADE = 450; 46 47 private Paint mRipplePaint; 48 private CanvasProperty<Float> mLeftProp; 49 private CanvasProperty<Float> mTopProp; 50 private CanvasProperty<Float> mRightProp; 51 private CanvasProperty<Float> mBottomProp; 52 private CanvasProperty<Float> mRxProp; 53 private CanvasProperty<Float> mRyProp; 54 private CanvasProperty<Paint> mPaintProp; 55 private float mGlowAlpha = 0f; 56 private float mGlowScale = 1f; 57 private boolean mPressed; 58 private boolean mDrawingHardwareGlow; 59 private int mMaxWidth; 60 61 private final Interpolator mInterpolator = new LogInterpolator(); 62 private boolean mSupportHardware; 63 private final View mTargetView; 64 65 private final HashSet<Animator> mRunningAnimations = new HashSet<>(); 66 private final ArrayList<Animator> mTmpArray = new ArrayList<>(); 67 68 public KeyButtonRipple(Context ctx, View targetView) { 69 mMaxWidth = ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width); 70 mTargetView = targetView; 71 } 72 73 private Paint getRipplePaint() { 74 if (mRipplePaint == null) { 75 mRipplePaint = new Paint(); 76 mRipplePaint.setAntiAlias(true); 77 mRipplePaint.setColor(0xffffffff); 78 } 79 return mRipplePaint; 80 } 81 82 private void drawSoftware(Canvas canvas) { 83 if (mGlowAlpha > 0f) { 84 final Paint p = getRipplePaint(); 85 p.setAlpha((int)(mGlowAlpha * 255f)); 86 87 final float w = getBounds().width(); 88 final float h = getBounds().height(); 89 final boolean horizontal = w > h; 90 final float diameter = getRippleSize() * mGlowScale; 91 final float radius = diameter * .5f; 92 final float cx = w * .5f; 93 final float cy = h * .5f; 94 final float rx = horizontal ? radius : cx; 95 final float ry = horizontal ? cy : radius; 96 final float corner = horizontal ? cy : cx; 97 98 canvas.drawRoundRect(cx - rx, cy - ry, 99 cx + rx, cy + ry, 100 corner, corner, p); 101 } 102 } 103 104 @Override 105 public void draw(Canvas canvas) { 106 mSupportHardware = canvas.isHardwareAccelerated(); 107 if (mSupportHardware) { 108 drawHardware((DisplayListCanvas) canvas); 109 } else { 110 drawSoftware(canvas); 111 } 112 } 113 114 @Override 115 public void setAlpha(int alpha) { 116 // Not supported. 117 } 118 119 @Override 120 public void setColorFilter(ColorFilter colorFilter) { 121 // Not supported. 122 } 123 124 @Override 125 public int getOpacity() { 126 return PixelFormat.TRANSLUCENT; 127 } 128 129 private boolean isHorizontal() { 130 return getBounds().width() > getBounds().height(); 131 } 132 133 private void drawHardware(DisplayListCanvas c) { 134 if (mDrawingHardwareGlow) { 135 c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp, 136 mPaintProp); 137 } 138 } 139 140 public float getGlowAlpha() { 141 return mGlowAlpha; 142 } 143 144 public void setGlowAlpha(float x) { 145 mGlowAlpha = x; 146 invalidateSelf(); 147 } 148 149 public float getGlowScale() { 150 return mGlowScale; 151 } 152 153 public void setGlowScale(float x) { 154 mGlowScale = x; 155 invalidateSelf(); 156 } 157 158 @Override 159 protected boolean onStateChange(int[] state) { 160 boolean pressed = false; 161 for (int i = 0; i < state.length; i++) { 162 if (state[i] == android.R.attr.state_pressed) { 163 pressed = true; 164 break; 165 } 166 } 167 if (pressed != mPressed) { 168 setPressed(pressed); 169 mPressed = pressed; 170 return true; 171 } else { 172 return false; 173 } 174 } 175 176 @Override 177 public void jumpToCurrentState() { 178 cancelAnimations(); 179 } 180 181 @Override 182 public boolean isStateful() { 183 return true; 184 } 185 186 public void setPressed(boolean pressed) { 187 if (mSupportHardware) { 188 setPressedHardware(pressed); 189 } else { 190 setPressedSoftware(pressed); 191 } 192 } 193 194 private void cancelAnimations() { 195 mTmpArray.addAll(mRunningAnimations); 196 int size = mTmpArray.size(); 197 for (int i = 0; i < size; i++) { 198 Animator a = mTmpArray.get(i); 199 a.cancel(); 200 } 201 mTmpArray.clear(); 202 mRunningAnimations.clear(); 203 } 204 205 private void setPressedSoftware(boolean pressed) { 206 if (pressed) { 207 enterSoftware(); 208 } else { 209 exitSoftware(); 210 } 211 } 212 213 private void enterSoftware() { 214 cancelAnimations(); 215 mGlowAlpha = GLOW_MAX_ALPHA; 216 ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale", 217 0f, GLOW_MAX_SCALE_FACTOR); 218 scaleAnimator.setInterpolator(mInterpolator); 219 scaleAnimator.setDuration(ANIMATION_DURATION_SCALE); 220 scaleAnimator.addListener(mAnimatorListener); 221 scaleAnimator.start(); 222 mRunningAnimations.add(scaleAnimator); 223 } 224 225 private void exitSoftware() { 226 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f); 227 alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT); 228 alphaAnimator.setDuration(ANIMATION_DURATION_FADE); 229 alphaAnimator.addListener(mAnimatorListener); 230 alphaAnimator.start(); 231 mRunningAnimations.add(alphaAnimator); 232 } 233 234 private void setPressedHardware(boolean pressed) { 235 if (pressed) { 236 enterHardware(); 237 } else { 238 exitHardware(); 239 } 240 } 241 242 /** 243 * Sets the left/top property for the round rect to {@code prop} depending on whether we are 244 * horizontal or vertical mode. 245 */ 246 private void setExtendStart(CanvasProperty<Float> prop) { 247 if (isHorizontal()) { 248 mLeftProp = prop; 249 } else { 250 mTopProp = prop; 251 } 252 } 253 254 private CanvasProperty<Float> getExtendStart() { 255 return isHorizontal() ? mLeftProp : mTopProp; 256 } 257 258 /** 259 * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are 260 * horizontal or vertical mode. 261 */ 262 private void setExtendEnd(CanvasProperty<Float> prop) { 263 if (isHorizontal()) { 264 mRightProp = prop; 265 } else { 266 mBottomProp = prop; 267 } 268 } 269 270 private CanvasProperty<Float> getExtendEnd() { 271 return isHorizontal() ? mRightProp : mBottomProp; 272 } 273 274 private int getExtendSize() { 275 return isHorizontal() ? getBounds().width() : getBounds().height(); 276 } 277 278 private int getRippleSize() { 279 int size = isHorizontal() ? getBounds().width() : getBounds().height(); 280 return Math.min(size, mMaxWidth); 281 } 282 283 private void enterHardware() { 284 cancelAnimations(); 285 mDrawingHardwareGlow = true; 286 setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2)); 287 final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(), 288 getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2); 289 startAnim.setDuration(ANIMATION_DURATION_SCALE); 290 startAnim.setInterpolator(mInterpolator); 291 startAnim.addListener(mAnimatorListener); 292 startAnim.setTarget(mTargetView); 293 294 setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2)); 295 final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(), 296 getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2); 297 endAnim.setDuration(ANIMATION_DURATION_SCALE); 298 endAnim.setInterpolator(mInterpolator); 299 endAnim.addListener(mAnimatorListener); 300 endAnim.setTarget(mTargetView); 301 302 if (isHorizontal()) { 303 mTopProp = CanvasProperty.createFloat(0f); 304 mBottomProp = CanvasProperty.createFloat(getBounds().height()); 305 mRxProp = CanvasProperty.createFloat(getBounds().height()/2); 306 mRyProp = CanvasProperty.createFloat(getBounds().height()/2); 307 } else { 308 mLeftProp = CanvasProperty.createFloat(0f); 309 mRightProp = CanvasProperty.createFloat(getBounds().width()); 310 mRxProp = CanvasProperty.createFloat(getBounds().width()/2); 311 mRyProp = CanvasProperty.createFloat(getBounds().width()/2); 312 } 313 314 mGlowScale = GLOW_MAX_SCALE_FACTOR; 315 mGlowAlpha = GLOW_MAX_ALPHA; 316 mRipplePaint = getRipplePaint(); 317 mRipplePaint.setAlpha((int) (mGlowAlpha * 255)); 318 mPaintProp = CanvasProperty.createPaint(mRipplePaint); 319 320 startAnim.start(); 321 endAnim.start(); 322 mRunningAnimations.add(startAnim); 323 mRunningAnimations.add(endAnim); 324 325 invalidateSelf(); 326 } 327 328 private void exitHardware() { 329 mPaintProp = CanvasProperty.createPaint(getRipplePaint()); 330 final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp, 331 RenderNodeAnimator.PAINT_ALPHA, 0); 332 opacityAnim.setDuration(ANIMATION_DURATION_FADE); 333 opacityAnim.setInterpolator(Interpolators.ALPHA_OUT); 334 opacityAnim.addListener(mAnimatorListener); 335 opacityAnim.setTarget(mTargetView); 336 337 opacityAnim.start(); 338 mRunningAnimations.add(opacityAnim); 339 340 invalidateSelf(); 341 } 342 343 private final AnimatorListenerAdapter mAnimatorListener = 344 new AnimatorListenerAdapter() { 345 @Override 346 public void onAnimationEnd(Animator animation) { 347 mRunningAnimations.remove(animation); 348 if (mRunningAnimations.isEmpty() && !mPressed) { 349 mDrawingHardwareGlow = false; 350 invalidateSelf(); 351 } 352 } 353 }; 354 355 /** 356 * Interpolator with a smooth log deceleration 357 */ 358 private static final class LogInterpolator implements Interpolator { 359 @Override 360 public float getInterpolation(float input) { 361 return 1 - (float) Math.pow(400, -input * 1.4); 362 } 363 } 364} 365