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