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