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