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