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