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