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.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.Interpolators; 38 import com.android.systemui.R; 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 jumpToCurrentState()228 public void jumpToCurrentState() { 229 endAnimations("jumpToCurrentState", false /* cancel */); 230 } 231 232 @Override isStateful()233 public boolean isStateful() { 234 return true; 235 } 236 237 @Override hasFocusStateSpecified()238 public boolean hasFocusStateSpecified() { 239 return true; 240 } 241 setPressed(boolean pressed)242 public void setPressed(boolean pressed) { 243 if (mDark != mLastDark && pressed) { 244 mRipplePaint = null; 245 mLastDark = mDark; 246 } 247 if (mSupportHardware) { 248 setPressedHardware(pressed); 249 } else { 250 setPressedSoftware(pressed); 251 } 252 } 253 254 /** 255 * Abort the ripple while it is delayed and before shown used only when setShouldDelayStartTouch 256 * is enabled. 257 */ abortDelayedRipple()258 public void abortDelayedRipple() { 259 mHandler.removeCallbacksAndMessages(null); 260 } 261 endAnimations(String reason, boolean cancel)262 private void endAnimations(String reason, boolean cancel) { 263 Trace.beginSection("KeyButtonRipple.endAnim: reason=" + reason + " cancel=" + cancel); 264 Trace.endSection(); 265 mVisible = false; 266 mTmpArray.addAll(mRunningAnimations); 267 int size = mTmpArray.size(); 268 for (int i = 0; i < size; i++) { 269 Animator a = mTmpArray.get(i); 270 if (cancel) { 271 a.cancel(); 272 } else { 273 a.end(); 274 } 275 } 276 mTmpArray.clear(); 277 mRunningAnimations.clear(); 278 mHandler.removeCallbacksAndMessages(null); 279 } 280 setPressedSoftware(boolean pressed)281 private void setPressedSoftware(boolean pressed) { 282 if (pressed) { 283 if (mDelayTouchFeedback) { 284 if (mRunningAnimations.isEmpty()) { 285 mHandler.removeCallbacksAndMessages(null); 286 mHandler.postDelayed(this::enterSoftware, ViewConfiguration.getTapTimeout()); 287 } else if (mVisible) { 288 enterSoftware(); 289 } 290 } else { 291 enterSoftware(); 292 } 293 } else { 294 exitSoftware(); 295 } 296 } 297 enterSoftware()298 private void enterSoftware() { 299 endAnimations("enterSoftware", true /* cancel */); 300 mVisible = true; 301 mGlowAlpha = getMaxGlowAlpha(); 302 ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, "glowScale", 303 0f, GLOW_MAX_SCALE_FACTOR); 304 scaleAnimator.setInterpolator(mInterpolator); 305 scaleAnimator.setDuration(ANIMATION_DURATION_SCALE); 306 scaleAnimator.addListener(mAnimatorListener); 307 scaleAnimator.start(); 308 mRunningAnimations.add(scaleAnimator); 309 310 // With the delay, it could eventually animate the enter animation with no pressed state, 311 // then immediately show the exit animation. If this is skipped there will be no ripple. 312 if (mDelayTouchFeedback && !mPressed) { 313 exitSoftware(); 314 } 315 } 316 exitSoftware()317 private void exitSoftware() { 318 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, 0f); 319 alphaAnimator.setInterpolator(Interpolators.ALPHA_OUT); 320 alphaAnimator.setDuration(ANIMATION_DURATION_FADE); 321 alphaAnimator.addListener(mAnimatorListener); 322 alphaAnimator.start(); 323 mRunningAnimations.add(alphaAnimator); 324 } 325 setPressedHardware(boolean pressed)326 private void setPressedHardware(boolean pressed) { 327 if (pressed) { 328 if (mDelayTouchFeedback) { 329 if (mRunningAnimations.isEmpty()) { 330 mHandler.removeCallbacksAndMessages(null); 331 mHandler.postDelayed(this::enterHardware, ViewConfiguration.getTapTimeout()); 332 } else if (mVisible) { 333 enterHardware(); 334 } 335 } else { 336 enterHardware(); 337 } 338 } else { 339 exitHardware(); 340 } 341 } 342 343 /** 344 * Sets the left/top property for the round rect to {@code prop} depending on whether we are 345 * horizontal or vertical mode. 346 */ setExtendStart(CanvasProperty<Float> prop)347 private void setExtendStart(CanvasProperty<Float> prop) { 348 if (isHorizontal()) { 349 mLeftProp = prop; 350 } else { 351 mTopProp = prop; 352 } 353 } 354 getExtendStart()355 private CanvasProperty<Float> getExtendStart() { 356 return isHorizontal() ? mLeftProp : mTopProp; 357 } 358 359 /** 360 * Sets the right/bottom property for the round rect to {@code prop} depending on whether we are 361 * horizontal or vertical mode. 362 */ setExtendEnd(CanvasProperty<Float> prop)363 private void setExtendEnd(CanvasProperty<Float> prop) { 364 if (isHorizontal()) { 365 mRightProp = prop; 366 } else { 367 mBottomProp = prop; 368 } 369 } 370 getExtendEnd()371 private CanvasProperty<Float> getExtendEnd() { 372 return isHorizontal() ? mRightProp : mBottomProp; 373 } 374 getExtendSize()375 private int getExtendSize() { 376 return isHorizontal() ? getBounds().width() : getBounds().height(); 377 } 378 getRippleSize()379 private int getRippleSize() { 380 int size = isHorizontal() ? getBounds().width() : getBounds().height(); 381 return Math.min(size, mMaxWidth); 382 } 383 enterHardware()384 private void enterHardware() { 385 endAnimations("enterHardware", true /* cancel */); 386 mVisible = true; 387 mDrawingHardwareGlow = true; 388 setExtendStart(CanvasProperty.createFloat(getExtendSize() / 2)); 389 final RenderNodeAnimator startAnim = new RenderNodeAnimator(getExtendStart(), 390 getExtendSize()/2 - GLOW_MAX_SCALE_FACTOR * getRippleSize()/2); 391 startAnim.setDuration(ANIMATION_DURATION_SCALE); 392 startAnim.setInterpolator(mInterpolator); 393 startAnim.addListener(mAnimatorListener); 394 startAnim.setTarget(mTargetView); 395 396 setExtendEnd(CanvasProperty.createFloat(getExtendSize() / 2)); 397 final RenderNodeAnimator endAnim = new RenderNodeAnimator(getExtendEnd(), 398 getExtendSize()/2 + GLOW_MAX_SCALE_FACTOR * getRippleSize()/2); 399 endAnim.setDuration(ANIMATION_DURATION_SCALE); 400 endAnim.setInterpolator(mInterpolator); 401 endAnim.addListener(mAnimatorListener); 402 endAnim.addListener(mEnterHwTraceAnimator); 403 endAnim.setTarget(mTargetView); 404 405 if (isHorizontal()) { 406 mTopProp = CanvasProperty.createFloat(0f); 407 mBottomProp = CanvasProperty.createFloat(getBounds().height()); 408 mRxProp = CanvasProperty.createFloat(getBounds().height()/2); 409 mRyProp = CanvasProperty.createFloat(getBounds().height()/2); 410 } else { 411 mLeftProp = CanvasProperty.createFloat(0f); 412 mRightProp = CanvasProperty.createFloat(getBounds().width()); 413 mRxProp = CanvasProperty.createFloat(getBounds().width()/2); 414 mRyProp = CanvasProperty.createFloat(getBounds().width()/2); 415 } 416 417 mGlowScale = GLOW_MAX_SCALE_FACTOR; 418 mGlowAlpha = getMaxGlowAlpha(); 419 mRipplePaint = getRipplePaint(); 420 mRipplePaint.setAlpha((int) (mGlowAlpha * 255)); 421 mPaintProp = CanvasProperty.createPaint(mRipplePaint); 422 423 startAnim.start(); 424 endAnim.start(); 425 mRunningAnimations.add(startAnim); 426 mRunningAnimations.add(endAnim); 427 428 invalidateSelf(); 429 430 // With the delay, it could eventually animate the enter animation with no pressed state, 431 // then immediately show the exit animation. If this is skipped there will be no ripple. 432 if (mDelayTouchFeedback && !mPressed) { 433 exitHardware(); 434 } 435 } 436 exitHardware()437 private void exitHardware() { 438 mPaintProp = CanvasProperty.createPaint(getRipplePaint()); 439 final RenderNodeAnimator opacityAnim = new RenderNodeAnimator(mPaintProp, 440 RenderNodeAnimator.PAINT_ALPHA, 0); 441 opacityAnim.setDuration(ANIMATION_DURATION_FADE); 442 opacityAnim.setInterpolator(Interpolators.ALPHA_OUT); 443 opacityAnim.addListener(mAnimatorListener); 444 opacityAnim.addListener(mExitHwTraceAnimator); 445 opacityAnim.setTarget(mTargetView); 446 447 opacityAnim.start(); 448 mRunningAnimations.add(opacityAnim); 449 450 invalidateSelf(); 451 } 452 453 private final AnimatorListenerAdapter mAnimatorListener = 454 new AnimatorListenerAdapter() { 455 @Override 456 public void onAnimationEnd(Animator animation) { 457 mRunningAnimations.remove(animation); 458 if (mRunningAnimations.isEmpty() && !mPressed) { 459 mVisible = false; 460 mDrawingHardwareGlow = false; 461 invalidateSelf(); 462 } 463 } 464 }; 465 466 private static final class TraceAnimatorListener extends AnimatorListenerAdapter { 467 private final String mName; TraceAnimatorListener(String name)468 TraceAnimatorListener(String name) { 469 mName = name; 470 } 471 472 @Override onAnimationStart(Animator animation)473 public void onAnimationStart(Animator animation) { 474 Trace.beginSection("KeyButtonRipple.start." + mName); 475 Trace.endSection(); 476 } 477 478 @Override onAnimationCancel(Animator animation)479 public void onAnimationCancel(Animator animation) { 480 Trace.beginSection("KeyButtonRipple.cancel." + mName); 481 Trace.endSection(); 482 } 483 484 @Override onAnimationEnd(Animator animation)485 public void onAnimationEnd(Animator animation) { 486 Trace.beginSection("KeyButtonRipple.end." + mName); 487 Trace.endSection(); 488 } 489 } 490 491 /** 492 * Interpolator with a smooth log deceleration 493 */ 494 private static final class LogInterpolator implements Interpolator { 495 @Override getInterpolation(float input)496 public float getInterpolation(float input) { 497 return 1 - (float) Math.pow(400, -input * 1.4); 498 } 499 } 500 } 501