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; 18 19 import android.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.animation.ArgbEvaluator; 22 import android.animation.PropertyValuesHolder; 23 import android.animation.ValueAnimator; 24 import android.annotation.Nullable; 25 import android.content.Context; 26 import android.content.res.TypedArray; 27 import android.graphics.Canvas; 28 import android.graphics.CanvasProperty; 29 import android.graphics.Color; 30 import android.graphics.Paint; 31 import android.graphics.PorterDuff; 32 import android.graphics.RecordingCanvas; 33 import android.graphics.drawable.Drawable; 34 import android.util.AttributeSet; 35 import android.view.RenderNodeAnimator; 36 import android.view.View; 37 import android.view.ViewAnimationUtils; 38 import android.view.animation.Interpolator; 39 import android.widget.ImageView; 40 41 import com.android.systemui.Interpolators; 42 import com.android.systemui.R; 43 44 /** 45 * An ImageView which does not have overlapping renderings commands and therefore does not need a 46 * layer when alpha is changed. 47 */ 48 public class KeyguardAffordanceView extends ImageView { 49 50 private static final long CIRCLE_APPEAR_DURATION = 80; 51 private static final long CIRCLE_DISAPPEAR_MAX_DURATION = 200; 52 private static final long NORMAL_ANIMATION_DURATION = 200; 53 public static final float MAX_ICON_SCALE_AMOUNT = 1.5f; 54 public static final float MIN_ICON_SCALE_AMOUNT = 0.8f; 55 56 protected final int mDarkIconColor; 57 protected final int mNormalColor; 58 private final int mMinBackgroundRadius; 59 private final Paint mCirclePaint; 60 private final ArgbEvaluator mColorInterpolator; 61 private final FlingAnimationUtils mFlingAnimationUtils; 62 private float mCircleRadius; 63 private int mCenterX; 64 private int mCenterY; 65 private ValueAnimator mCircleAnimator; 66 private ValueAnimator mAlphaAnimator; 67 private ValueAnimator mScaleAnimator; 68 private float mCircleStartValue; 69 private boolean mCircleWillBeHidden; 70 private int[] mTempPoint = new int[2]; 71 private float mImageScale = 1f; 72 private int mCircleColor; 73 private boolean mIsLeft; 74 private View mPreviewView; 75 private float mCircleStartRadius; 76 private float mMaxCircleSize; 77 private Animator mPreviewClipper; 78 private float mRestingAlpha = 1f; 79 private boolean mSupportHardware; 80 private boolean mFinishing; 81 private boolean mLaunchingAffordance; 82 private boolean mShouldTint = true; 83 84 private CanvasProperty<Float> mHwCircleRadius; 85 private CanvasProperty<Float> mHwCenterX; 86 private CanvasProperty<Float> mHwCenterY; 87 private CanvasProperty<Paint> mHwCirclePaint; 88 89 private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() { 90 @Override 91 public void onAnimationEnd(Animator animation) { 92 mPreviewClipper = null; 93 } 94 }; 95 private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() { 96 @Override 97 public void onAnimationEnd(Animator animation) { 98 mCircleAnimator = null; 99 } 100 }; 101 private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() { 102 @Override 103 public void onAnimationEnd(Animator animation) { 104 mScaleAnimator = null; 105 } 106 }; 107 private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() { 108 @Override 109 public void onAnimationEnd(Animator animation) { 110 mAlphaAnimator = null; 111 } 112 }; 113 KeyguardAffordanceView(Context context)114 public KeyguardAffordanceView(Context context) { 115 this(context, null); 116 } 117 KeyguardAffordanceView(Context context, AttributeSet attrs)118 public KeyguardAffordanceView(Context context, AttributeSet attrs) { 119 this(context, attrs, 0); 120 } 121 KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr)122 public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) { 123 this(context, attrs, defStyleAttr, 0); 124 } 125 KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)126 public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr, 127 int defStyleRes) { 128 super(context, attrs, defStyleAttr, defStyleRes); 129 TypedArray a = context.obtainStyledAttributes(attrs, android.R.styleable.ImageView); 130 131 mCirclePaint = new Paint(); 132 mCirclePaint.setAntiAlias(true); 133 mCircleColor = 0xffffffff; 134 mCirclePaint.setColor(mCircleColor); 135 136 mNormalColor = a.getColor(android.R.styleable.ImageView_tint, 0xffffffff); 137 mDarkIconColor = 0xff000000; 138 mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( 139 R.dimen.keyguard_affordance_min_background_radius); 140 mColorInterpolator = new ArgbEvaluator(); 141 mFlingAnimationUtils = new FlingAnimationUtils(mContext.getResources().getDisplayMetrics(), 142 0.3f); 143 144 a.recycle(); 145 } 146 setImageDrawable(@ullable Drawable drawable, boolean tint)147 public void setImageDrawable(@Nullable Drawable drawable, boolean tint) { 148 super.setImageDrawable(drawable); 149 mShouldTint = tint; 150 updateIconColor(); 151 } 152 153 /** 154 * If current drawable should be tinted. 155 */ shouldTint()156 public boolean shouldTint() { 157 return mShouldTint; 158 } 159 160 @Override onLayout(boolean changed, int left, int top, int right, int bottom)161 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 162 super.onLayout(changed, left, top, right, bottom); 163 mCenterX = getWidth() / 2; 164 mCenterY = getHeight() / 2; 165 mMaxCircleSize = getMaxCircleSize(); 166 } 167 168 @Override onDraw(Canvas canvas)169 protected void onDraw(Canvas canvas) { 170 mSupportHardware = canvas.isHardwareAccelerated(); 171 drawBackgroundCircle(canvas); 172 canvas.save(); 173 canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2); 174 super.onDraw(canvas); 175 canvas.restore(); 176 } 177 setPreviewView(View v)178 public void setPreviewView(View v) { 179 if (mPreviewView == v) { 180 return; 181 } 182 View oldPreviewView = mPreviewView; 183 mPreviewView = v; 184 if (mPreviewView != null) { 185 mPreviewView.setVisibility(mLaunchingAffordance 186 ? oldPreviewView.getVisibility() : INVISIBLE); 187 } 188 } 189 updateIconColor()190 private void updateIconColor() { 191 if (!mShouldTint) return; 192 Drawable drawable = getDrawable().mutate(); 193 float alpha = mCircleRadius / mMinBackgroundRadius; 194 alpha = Math.min(1.0f, alpha); 195 int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mDarkIconColor); 196 drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); 197 } 198 drawBackgroundCircle(Canvas canvas)199 private void drawBackgroundCircle(Canvas canvas) { 200 if (mCircleRadius > 0 || mFinishing) { 201 if (mFinishing && mSupportHardware && mHwCenterX != null) { 202 // Our hardware drawing proparties can be null if the finishing started but we have 203 // never drawn before. In that case we are not doing a render thread animation 204 // anyway, so we need to use the normal drawing. 205 RecordingCanvas recordingCanvas = (RecordingCanvas) canvas; 206 recordingCanvas.drawCircle(mHwCenterX, mHwCenterY, mHwCircleRadius, 207 mHwCirclePaint); 208 } else { 209 updateCircleColor(); 210 canvas.drawCircle(mCenterX, mCenterY, mCircleRadius, mCirclePaint); 211 } 212 } 213 } 214 updateCircleColor()215 private void updateCircleColor() { 216 float fraction = 0.5f + 0.5f * Math.max(0.0f, Math.min(1.0f, 217 (mCircleRadius - mMinBackgroundRadius) / (0.5f * mMinBackgroundRadius))); 218 if (mPreviewView != null && mPreviewView.getVisibility() == VISIBLE) { 219 float finishingFraction = 1 - Math.max(0, mCircleRadius - mCircleStartRadius) 220 / (mMaxCircleSize - mCircleStartRadius); 221 fraction *= finishingFraction; 222 } 223 int color = Color.argb((int) (Color.alpha(mCircleColor) * fraction), 224 Color.red(mCircleColor), 225 Color.green(mCircleColor), Color.blue(mCircleColor)); 226 mCirclePaint.setColor(color); 227 } 228 finishAnimation(float velocity, final Runnable mAnimationEndRunnable)229 public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) { 230 cancelAnimator(mCircleAnimator); 231 cancelAnimator(mPreviewClipper); 232 mFinishing = true; 233 mCircleStartRadius = mCircleRadius; 234 final float maxCircleSize = getMaxCircleSize(); 235 Animator animatorToRadius; 236 if (mSupportHardware) { 237 initHwProperties(); 238 animatorToRadius = getRtAnimatorToRadius(maxCircleSize); 239 startRtAlphaFadeIn(); 240 } else { 241 animatorToRadius = getAnimatorToRadius(maxCircleSize); 242 } 243 mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize, 244 velocity, maxCircleSize); 245 animatorToRadius.addListener(new AnimatorListenerAdapter() { 246 @Override 247 public void onAnimationEnd(Animator animation) { 248 mAnimationEndRunnable.run(); 249 mFinishing = false; 250 mCircleRadius = maxCircleSize; 251 invalidate(); 252 } 253 }); 254 animatorToRadius.start(); 255 setImageAlpha(0, true); 256 if (mPreviewView != null) { 257 mPreviewView.setVisibility(View.VISIBLE); 258 mPreviewClipper = ViewAnimationUtils.createCircularReveal( 259 mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius, 260 maxCircleSize); 261 mFlingAnimationUtils.applyDismissing(mPreviewClipper, mCircleRadius, maxCircleSize, 262 velocity, maxCircleSize); 263 mPreviewClipper.addListener(mClipEndListener); 264 mPreviewClipper.start(); 265 if (mSupportHardware) { 266 startRtCircleFadeOut(animatorToRadius.getDuration()); 267 } 268 } 269 } 270 271 /** 272 * Fades in the Circle on the RenderThread. It's used when finishing the circle when it had 273 * alpha 0 in the beginning. 274 */ startRtAlphaFadeIn()275 private void startRtAlphaFadeIn() { 276 if (mCircleRadius == 0 && mPreviewView == null) { 277 Paint modifiedPaint = new Paint(mCirclePaint); 278 modifiedPaint.setColor(mCircleColor); 279 modifiedPaint.setAlpha(0); 280 mHwCirclePaint = CanvasProperty.createPaint(modifiedPaint); 281 RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint, 282 RenderNodeAnimator.PAINT_ALPHA, 255); 283 animator.setTarget(this); 284 animator.setInterpolator(Interpolators.ALPHA_IN); 285 animator.setDuration(250); 286 animator.start(); 287 } 288 } 289 instantFinishAnimation()290 public void instantFinishAnimation() { 291 cancelAnimator(mPreviewClipper); 292 if (mPreviewView != null) { 293 mPreviewView.setClipBounds(null); 294 mPreviewView.setVisibility(View.VISIBLE); 295 } 296 mCircleRadius = getMaxCircleSize(); 297 setImageAlpha(0, false); 298 invalidate(); 299 } 300 startRtCircleFadeOut(long duration)301 private void startRtCircleFadeOut(long duration) { 302 RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint, 303 RenderNodeAnimator.PAINT_ALPHA, 0); 304 animator.setDuration(duration); 305 animator.setInterpolator(Interpolators.ALPHA_OUT); 306 animator.setTarget(this); 307 animator.start(); 308 } 309 getRtAnimatorToRadius(float circleRadius)310 private Animator getRtAnimatorToRadius(float circleRadius) { 311 RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius); 312 animator.setTarget(this); 313 return animator; 314 } 315 initHwProperties()316 private void initHwProperties() { 317 mHwCenterX = CanvasProperty.createFloat(mCenterX); 318 mHwCenterY = CanvasProperty.createFloat(mCenterY); 319 mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint); 320 mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius); 321 } 322 getMaxCircleSize()323 private float getMaxCircleSize() { 324 getLocationInWindow(mTempPoint); 325 float rootWidth = getRootView().getWidth(); 326 float width = mTempPoint[0] + mCenterX; 327 width = Math.max(rootWidth - width, width); 328 float height = mTempPoint[1] + mCenterY; 329 return (float) Math.hypot(width, height); 330 } 331 setCircleRadius(float circleRadius)332 public void setCircleRadius(float circleRadius) { 333 setCircleRadius(circleRadius, false, false); 334 } 335 setCircleRadius(float circleRadius, boolean slowAnimation)336 public void setCircleRadius(float circleRadius, boolean slowAnimation) { 337 setCircleRadius(circleRadius, slowAnimation, false); 338 } 339 setCircleRadiusWithoutAnimation(float circleRadius)340 public void setCircleRadiusWithoutAnimation(float circleRadius) { 341 cancelAnimator(mCircleAnimator); 342 setCircleRadius(circleRadius, false ,true); 343 } 344 setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation)345 private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) { 346 347 // Check if we need a new animation 348 boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden) 349 || (mCircleAnimator == null && mCircleRadius == 0.0f); 350 boolean nowHidden = circleRadius == 0.0f; 351 boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation; 352 if (!radiusNeedsAnimation) { 353 if (mCircleAnimator == null) { 354 mCircleRadius = circleRadius; 355 updateIconColor(); 356 invalidate(); 357 if (nowHidden) { 358 if (mPreviewView != null) { 359 mPreviewView.setVisibility(View.INVISIBLE); 360 } 361 } 362 } else if (!mCircleWillBeHidden) { 363 364 // We just update the end value 365 float diff = circleRadius - mMinBackgroundRadius; 366 PropertyValuesHolder[] values = mCircleAnimator.getValues(); 367 values[0].setFloatValues(mCircleStartValue + diff, circleRadius); 368 mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime()); 369 } 370 } else { 371 cancelAnimator(mCircleAnimator); 372 cancelAnimator(mPreviewClipper); 373 ValueAnimator animator = getAnimatorToRadius(circleRadius); 374 Interpolator interpolator = circleRadius == 0.0f 375 ? Interpolators.FAST_OUT_LINEAR_IN 376 : Interpolators.LINEAR_OUT_SLOW_IN; 377 animator.setInterpolator(interpolator); 378 long duration = 250; 379 if (!slowAnimation) { 380 float durationFactor = Math.abs(mCircleRadius - circleRadius) 381 / (float) mMinBackgroundRadius; 382 duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor); 383 duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION); 384 } 385 animator.setDuration(duration); 386 animator.start(); 387 if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) { 388 mPreviewView.setVisibility(View.VISIBLE); 389 mPreviewClipper = ViewAnimationUtils.createCircularReveal( 390 mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius, 391 circleRadius); 392 mPreviewClipper.setInterpolator(interpolator); 393 mPreviewClipper.setDuration(duration); 394 mPreviewClipper.addListener(mClipEndListener); 395 mPreviewClipper.addListener(new AnimatorListenerAdapter() { 396 @Override 397 public void onAnimationEnd(Animator animation) { 398 mPreviewView.setVisibility(View.INVISIBLE); 399 } 400 }); 401 mPreviewClipper.start(); 402 } 403 } 404 } 405 getAnimatorToRadius(float circleRadius)406 private ValueAnimator getAnimatorToRadius(float circleRadius) { 407 ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius); 408 mCircleAnimator = animator; 409 mCircleStartValue = mCircleRadius; 410 mCircleWillBeHidden = circleRadius == 0.0f; 411 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 412 @Override 413 public void onAnimationUpdate(ValueAnimator animation) { 414 mCircleRadius = (float) animation.getAnimatedValue(); 415 updateIconColor(); 416 invalidate(); 417 } 418 }); 419 animator.addListener(mCircleEndListener); 420 return animator; 421 } 422 cancelAnimator(Animator animator)423 private void cancelAnimator(Animator animator) { 424 if (animator != null) { 425 animator.cancel(); 426 } 427 } 428 setImageScale(float imageScale, boolean animate)429 public void setImageScale(float imageScale, boolean animate) { 430 setImageScale(imageScale, animate, -1, null); 431 } 432 433 /** 434 * Sets the scale of the containing image 435 * 436 * @param imageScale The new Scale. 437 * @param animate Should an animation be performed 438 * @param duration If animate, whats the duration? When -1 we take the default duration 439 * @param interpolator If animate, whats the interpolator? When null we take the default 440 * interpolator. 441 */ setImageScale(float imageScale, boolean animate, long duration, Interpolator interpolator)442 public void setImageScale(float imageScale, boolean animate, long duration, 443 Interpolator interpolator) { 444 cancelAnimator(mScaleAnimator); 445 if (!animate) { 446 mImageScale = imageScale; 447 invalidate(); 448 } else { 449 ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale); 450 mScaleAnimator = animator; 451 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 452 @Override 453 public void onAnimationUpdate(ValueAnimator animation) { 454 mImageScale = (float) animation.getAnimatedValue(); 455 invalidate(); 456 } 457 }); 458 animator.addListener(mScaleEndListener); 459 if (interpolator == null) { 460 interpolator = imageScale == 0.0f 461 ? Interpolators.FAST_OUT_LINEAR_IN 462 : Interpolators.LINEAR_OUT_SLOW_IN; 463 } 464 animator.setInterpolator(interpolator); 465 if (duration == -1) { 466 float durationFactor = Math.abs(mImageScale - imageScale) 467 / (1.0f - MIN_ICON_SCALE_AMOUNT); 468 durationFactor = Math.min(1.0f, durationFactor); 469 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor); 470 } 471 animator.setDuration(duration); 472 animator.start(); 473 } 474 } 475 getRestingAlpha()476 public float getRestingAlpha() { 477 return mRestingAlpha; 478 } 479 setImageAlpha(float alpha, boolean animate)480 public void setImageAlpha(float alpha, boolean animate) { 481 setImageAlpha(alpha, animate, -1, null, null); 482 } 483 484 /** 485 * Sets the alpha of the containing image 486 * 487 * @param alpha The new alpha. 488 * @param animate Should an animation be performed 489 * @param duration If animate, whats the duration? When -1 we take the default duration 490 * @param interpolator If animate, whats the interpolator? When null we take the default 491 * interpolator. 492 */ setImageAlpha(float alpha, boolean animate, long duration, Interpolator interpolator, Runnable runnable)493 public void setImageAlpha(float alpha, boolean animate, long duration, 494 Interpolator interpolator, Runnable runnable) { 495 cancelAnimator(mAlphaAnimator); 496 alpha = mLaunchingAffordance ? 0 : alpha; 497 int endAlpha = (int) (alpha * 255); 498 final Drawable background = getBackground(); 499 if (!animate) { 500 if (background != null) background.mutate().setAlpha(endAlpha); 501 setImageAlpha(endAlpha); 502 } else { 503 int currentAlpha = getImageAlpha(); 504 ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha); 505 mAlphaAnimator = animator; 506 animator.addUpdateListener(animation -> { 507 int alpha1 = (int) animation.getAnimatedValue(); 508 if (background != null) background.mutate().setAlpha(alpha1); 509 setImageAlpha(alpha1); 510 }); 511 animator.addListener(mAlphaEndListener); 512 if (interpolator == null) { 513 interpolator = alpha == 0.0f 514 ? Interpolators.FAST_OUT_LINEAR_IN 515 : Interpolators.LINEAR_OUT_SLOW_IN; 516 } 517 animator.setInterpolator(interpolator); 518 if (duration == -1) { 519 float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f; 520 durationFactor = Math.min(1.0f, durationFactor); 521 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor); 522 } 523 animator.setDuration(duration); 524 if (runnable != null) { 525 animator.addListener(getEndListener(runnable)); 526 } 527 animator.start(); 528 } 529 } 530 isAnimatingAlpha()531 public boolean isAnimatingAlpha() { 532 return mAlphaAnimator != null; 533 } 534 getEndListener(final Runnable runnable)535 private Animator.AnimatorListener getEndListener(final Runnable runnable) { 536 return new AnimatorListenerAdapter() { 537 boolean mCancelled; 538 @Override 539 public void onAnimationCancel(Animator animation) { 540 mCancelled = true; 541 } 542 543 @Override 544 public void onAnimationEnd(Animator animation) { 545 if (!mCancelled) { 546 runnable.run(); 547 } 548 } 549 }; 550 } 551 getCircleRadius()552 public float getCircleRadius() { 553 return mCircleRadius; 554 } 555 556 @Override performClick()557 public boolean performClick() { 558 if (isClickable()) { 559 return super.performClick(); 560 } else { 561 return false; 562 } 563 } 564 setLaunchingAffordance(boolean launchingAffordance)565 public void setLaunchingAffordance(boolean launchingAffordance) { 566 mLaunchingAffordance = launchingAffordance; 567 } 568 } 569