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.RecordingCanvas; 29 import android.graphics.drawable.Drawable; 30 import android.os.Handler; 31 import android.view.RenderNodeAnimator; 32 import android.view.View; 33 import android.view.ViewConfiguration; 34 import android.view.animation.Interpolator; 35 36 import com.android.systemui.Interpolators; 37 import com.android.systemui.R; 38 39 import java.util.ArrayList; 40 import java.util.HashSet; 41 42 public class KeyButtonRipple extends Drawable { 43 44 private static final float GLOW_MAX_SCALE_FACTOR = 1.35f; 45 private static final float GLOW_MAX_ALPHA = 0.2f; 46 private static final float GLOW_MAX_ALPHA_DARK = 0.1f; 47 private static final int ANIMATION_DURATION_SCALE = 350; 48 private static final int ANIMATION_DURATION_FADE = 450; 49 50 private Paint mRipplePaint; 51 private CanvasProperty<Float> mLeftProp; 52 private CanvasProperty<Float> mTopProp; 53 private CanvasProperty<Float> mRightProp; 54 private CanvasProperty<Float> mBottomProp; 55 private CanvasProperty<Float> mRxProp; 56 private CanvasProperty<Float> mRyProp; 57 private CanvasProperty<Paint> mPaintProp; 58 private float mGlowAlpha = 0f; 59 private float mGlowScale = 1f; 60 private boolean mPressed; 61 private boolean mVisible; 62 private boolean mDrawingHardwareGlow; 63 private int mMaxWidth; 64 private boolean mLastDark; 65 private boolean mDark; 66 private boolean mDelayTouchFeedback; 67 68 private final Interpolator mInterpolator = new LogInterpolator(); 69 private boolean mSupportHardware; 70 private final View mTargetView; 71 private final Handler mHandler = new Handler(); 72 73 private final HashSet<Animator> mRunningAnimations = new HashSet<>(); 74 private final ArrayList<Animator> mTmpArray = new ArrayList<>(); 75 76 public enum Type { 77 OVAL, 78 ROUNDED_RECT 79 } 80 81 private Type mType = Type.ROUNDED_RECT; 82 KeyButtonRipple(Context ctx, View targetView)83 public KeyButtonRipple(Context ctx, View targetView) { 84 mMaxWidth = ctx.getResources().getDimensionPixelSize(R.dimen.key_button_ripple_max_width); 85 mTargetView = targetView; 86 } 87 setDarkIntensity(float darkIntensity)88 public void setDarkIntensity(float darkIntensity) { 89 mDark = darkIntensity >= 0.5f; 90 } 91 setDelayTouchFeedback(boolean delay)92 public void setDelayTouchFeedback(boolean delay) { 93 mDelayTouchFeedback = delay; 94 } 95 setType(Type type)96 public void setType(Type type) { 97 mType = type; 98 } 99 getRipplePaint()100 private Paint getRipplePaint() { 101 if (mRipplePaint == null) { 102 mRipplePaint = new Paint(); 103 mRipplePaint.setAntiAlias(true); 104 mRipplePaint.setColor(mLastDark ? 0xff000000 : 0xffffffff); 105 } 106 return mRipplePaint; 107 } 108 drawSoftware(Canvas canvas)109 private void drawSoftware(Canvas canvas) { 110 if (mGlowAlpha > 0f) { 111 final Paint p = getRipplePaint(); 112 p.setAlpha((int)(mGlowAlpha * 255f)); 113 114 final float w = getBounds().width(); 115 final float h = getBounds().height(); 116 final boolean horizontal = w > h; 117 final float diameter = getRippleSize() * mGlowScale; 118 final float radius = diameter * .5f; 119 final float cx = w * .5f; 120 final float cy = h * .5f; 121 final float rx = horizontal ? radius : cx; 122 final float ry = horizontal ? cy : radius; 123 final float corner = horizontal ? cy : cx; 124 125 if (mType == Type.ROUNDED_RECT) { 126 canvas.drawRoundRect(cx - rx, cy - ry, cx + rx, cy + ry, corner, corner, p); 127 } else { 128 canvas.save(); 129 canvas.translate(cx, cy); 130 float r = Math.min(rx, ry); 131 canvas.drawOval(-r, -r, r, r, p); 132 canvas.restore(); 133 } 134 } 135 } 136 137 @Override draw(Canvas canvas)138 public void draw(Canvas canvas) { 139 mSupportHardware = canvas.isHardwareAccelerated(); 140 if (mSupportHardware) { 141 drawHardware((RecordingCanvas) canvas); 142 } else { 143 drawSoftware(canvas); 144 } 145 } 146 147 @Override setAlpha(int alpha)148 public void setAlpha(int alpha) { 149 // Not supported. 150 } 151 152 @Override setColorFilter(ColorFilter colorFilter)153 public void setColorFilter(ColorFilter colorFilter) { 154 // Not supported. 155 } 156 157 @Override getOpacity()158 public int getOpacity() { 159 return PixelFormat.TRANSLUCENT; 160 } 161 isHorizontal()162 private boolean isHorizontal() { 163 return getBounds().width() > getBounds().height(); 164 } 165 drawHardware(RecordingCanvas c)166 private void drawHardware(RecordingCanvas c) { 167 if (mDrawingHardwareGlow) { 168 if (mType == Type.ROUNDED_RECT) { 169 c.drawRoundRect(mLeftProp, mTopProp, mRightProp, mBottomProp, mRxProp, mRyProp, 170 mPaintProp); 171 } else { 172 CanvasProperty<Float> cx = CanvasProperty.createFloat(getBounds().width() / 2); 173 CanvasProperty<Float> cy = CanvasProperty.createFloat(getBounds().height() / 2); 174 int d = Math.min(getBounds().width(), getBounds().height()); 175 CanvasProperty<Float> r = CanvasProperty.createFloat(1.0f * d / 2); 176 c.drawCircle(cx, cy, r, mPaintProp); 177 } 178 } 179 } 180 getGlowAlpha()181 public float getGlowAlpha() { 182 return mGlowAlpha; 183 } 184 setGlowAlpha(float x)185 public void setGlowAlpha(float x) { 186 mGlowAlpha = x; 187 invalidateSelf(); 188 } 189 getGlowScale()190 public float getGlowScale() { 191 return mGlowScale; 192 } 193 setGlowScale(float x)194 public void setGlowScale(float x) { 195 mGlowScale = x; 196 invalidateSelf(); 197 } 198 getMaxGlowAlpha()199 private float getMaxGlowAlpha() { 200 return mLastDark ? GLOW_MAX_ALPHA_DARK : GLOW_MAX_ALPHA; 201 } 202 203 @Override onStateChange(int[] state)204 protected boolean onStateChange(int[] state) { 205 boolean pressed = false; 206 for (int i = 0; i < state.length; i++) { 207 if (state[i] == android.R.attr.state_pressed) { 208 pressed = true; 209 break; 210 } 211 } 212 if (pressed != mPressed) { 213 setPressed(pressed); 214 mPressed = pressed; 215 return true; 216 } else { 217 return false; 218 } 219 } 220 221 @Override jumpToCurrentState()222 public void jumpToCurrentState() { 223 cancelAnimations(); 224 } 225 226 @Override isStateful()227 public boolean isStateful() { 228 return true; 229 } 230 231 @Override hasFocusStateSpecified()232 public boolean hasFocusStateSpecified() { 233 return true; 234 } 235 setPressed(boolean pressed)236 public void setPressed(boolean pressed) { 237 if (mDark != mLastDark && pressed) { 238 mRipplePaint = null; 239 mLastDark = mDark; 240 } 241 if (mSupportHardware) { 242 setPressedHardware(pressed); 243 } else { 244 setPressedSoftware(pressed); 245 } 246 } 247 248 /** 249 * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch 250 * is enabled. 251 */ abortDelayedRipple()252 public void abortDelayedRipple() { 253 mHandler.removeCallbacksAndMessages(null); 254 } 255 cancelAnimations()256 private void cancelAnimations() { 257 mVisible = false; 258 mTmpArray.addAll(mRunningAnimations); 259 int size = mTmpArray.size(); 260 for (int i = 0; i < size; i++) { 261 Animator a = mTmpArray.get(i); 262 a.cancel(); 263 } 264 mTmpArray.clear(); 265 mRunningAnimations.clear(); 266 mHandler.removeCallbacksAndMessages(null); 267 } 268 setPressedSoftware(boolean pressed)269 private void setPressedSoftware(boolean pressed) { 270 if (pressed) { 271 if (mDelayTouchFeedback) { 272 if (mRunningAnimations.isEmpty()) { 273 mHandler.removeCallbacksAndMessages(null); 274 mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout()); 275 } else if (mVisible) { 276 enterSoftware(); 277 } 278 } else { 279 enterSoftware(); 280 } 281 } else { 282 exitSoftware(); 283 } 284 } 285 enterSoftware()286 private void enterSoftware() { 287 cancelAnimations(); 288 mVisible = true; 289 mGlowAlpha = getMaxGlowAlpha(); 290 ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale", 291 0f, GLOW_MAX_SCALE_FACTOR); 292 scaleAnimator.setInterpolator(mInterpolator); 293 scaleAnimator.setDuration(ANIMATION_DURATION_SCALE); 294 scaleAnimator.addListener(mAnimatorListener); 295 scaleAnimator.start(); 296 mRunningAnimations.add(scaleAnimator); 297 298 // With the delay, it could eventually animate the enter animation with no pressed state, 299 // then immediately show the exit animation. If this is skipped there will be no ripple. 300 if (mDelayTouchFeedback && !mPressed) { 301 exitSoftware(); 302 } 303 } 304 exitSoftware()305 private void exitSoftware() { 306 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f); 307 alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT); 308 alphaAnimator.setDuration(ANIMATION_DURATION_FADE); 309 alphaAnimator.addListener(mAnimatorListener); 310 alphaAnimator.start(); 311 mRunningAnimations.add(alphaAnimator); 312 } 313 setPressedHardware(boolean pressed)314 private void setPressedHardware(boolean pressed) { 315 if (pressed) { 316 if (mDelayTouchFeedback) { 317 if (mRunningAnimations.isEmpty()) { 318 mHandler.removeCallbacksAndMessages(null); 319 mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout()); 320 } else if (mVisible) { 321 enterHardware(); 322 } 323 } else { 324 enterHardware(); 325 } 326 } else { 327 exitHardware(); 328 } 329 } 330 331 /** 332 * Sets the left/top property for the round rect to {@code prop} depending on whether we are 333 * horizontal or vertical mode. 334 */ setExtendStart(CanvasProperty<Float> prop)335 private void setExtendStart(CanvasProperty<Float> prop) { 336 if (isHorizontal()) { 337 mLeftProp = prop; 338 } else { 339 mTopProp = prop; 340 } 341 } 342 getExtendStart()343 private CanvasProperty<Float> getExtendStart() { 344 return isHorizontal() ? mLeftProp : mTopProp; 345 } 346 347 /** 348 * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are 349 * horizontal or vertical mode. 350 */ setExtendEnd(CanvasProperty<Float> prop)351 private void setExtendEnd(CanvasProperty<Float> prop) { 352 if (isHorizontal()) { 353 mRightProp = prop; 354 } else { 355 mBottomProp = prop; 356 } 357 } 358 getExtendEnd()359 private CanvasProperty<Float> getExtendEnd() { 360 return isHorizontal() ? mRightProp : mBottomProp; 361 } 362 getExtendSize()363 private int getExtendSize() { 364 return isHorizontal() ? getBounds().width() : getBounds().height(); 365 } 366 getRippleSize()367 private int getRippleSize() { 368 int size = isHorizontal() ? getBounds().width() : getBounds().height(); 369 return Math.min(size, mMaxWidth); 370 } 371 enterHardware()372 private void enterHardware() { 373 cancelAnimations(); 374 mVisible = true; 375 mDrawingHardwareGlow = true; 376 setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2)); 377 final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(), 378 getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2); 379 startAnim.setDuration(ANIMATION_DURATION_SCALE); 380 startAnim.setInterpolator(mInterpolator); 381 startAnim.addListener(mAnimatorListener); 382 startAnim.setTarget(mTargetView); 383 384 setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2)); 385 final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(), 386 getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2); 387 endAnim.setDuration(ANIMATION_DURATION_SCALE); 388 endAnim.setInterpolator(mInterpolator); 389 endAnim.addListener(mAnimatorListener); 390 endAnim.setTarget(mTargetView); 391 392 if (isHorizontal()) { 393 mTopProp = CanvasProperty.createFloat(0f); 394 mBottomProp = CanvasProperty.createFloat(getBounds().height()); 395 mRxProp = CanvasProperty.createFloat(getBounds().height()/2); 396 mRyProp = CanvasProperty.createFloat(getBounds().height()/2); 397 } else { 398 mLeftProp = CanvasProperty.createFloat(0f); 399 mRightProp = CanvasProperty.createFloat(getBounds().width()); 400 mRxProp = CanvasProperty.createFloat(getBounds().width()/2); 401 mRyProp = CanvasProperty.createFloat(getBounds().width()/2); 402 } 403 404 mGlowScale = GLOW_MAX_SCALE_FACTOR; 405 mGlowAlpha = getMaxGlowAlpha(); 406 mRipplePaint = getRipplePaint(); 407 mRipplePaint.setAlpha((int) (mGlowAlpha * 255)); 408 mPaintProp = CanvasProperty.createPaint(mRipplePaint); 409 410 startAnim.start(); 411 endAnim.start(); 412 mRunningAnimations.add(startAnim); 413 mRunningAnimations.add(endAnim); 414 415 invalidateSelf(); 416 417 // With the delay, it could eventually animate the enter animation with no pressed state, 418 // then immediately show the exit animation. If this is skipped there will be no ripple. 419 if (mDelayTouchFeedback && !mPressed) { 420 exitHardware(); 421 } 422 } 423 exitHardware()424 private void exitHardware() { 425 mPaintProp = CanvasProperty.createPaint(getRipplePaint()); 426 final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp, 427 RenderNodeAnimator.PAINT_ALPHA, 0); 428 opacityAnim.setDuration(ANIMATION_DURATION_FADE); 429 opacityAnim.setInterpolator(Interpolators.ALPHA_OUT); 430 opacityAnim.addListener(mAnimatorListener); 431 opacityAnim.setTarget(mTargetView); 432 433 opacityAnim.start(); 434 mRunningAnimations.add(opacityAnim); 435 436 invalidateSelf(); 437 } 438 439 private final AnimatorListenerAdapter mAnimatorListener = 440 new AnimatorListenerAdapter() { 441 @Override 442 public void onAnimationEnd(Animator animation) { 443 mRunningAnimations.remove(animation); 444 if (mRunningAnimations.isEmpty() && !mPressed) { 445 mVisible = false; 446 mDrawingHardwareGlow = false; 447 invalidateSelf(); 448 } 449 } 450 }; 451 452 /** 453 * Interpolator with a smooth log deceleration 454 */ 455 private static final class LogInterpolator implements Interpolator { 456 @Override getInterpolation(float input)457 public float getInterpolation(float input) { 458 return 1 - (float) Math.pow(400, -input * 1.4); 459 } 460 } 461 } 462