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