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