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