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