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.AnimationUtils; 37 import android.view.animation.Interpolator; 38 import android.widget.ImageView; 39 import com.android.systemui.R; 40 import com.android.systemui.statusbar.phone.KeyguardAffordanceHelper; 41 import com.android.systemui.statusbar.phone.PhoneStatusBar; 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 Interpolator mAppearInterpolator; 58 private final Interpolator mDisappearInterpolator; 59 private final int mInverseColor; 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 83 private CanvasProperty<Float> mHwCircleRadius; 84 private CanvasProperty<Float> mHwCenterX; 85 private CanvasProperty<Float> mHwCenterY; 86 private CanvasProperty<Paint> mHwCirclePaint; 87 88 private AnimatorListenerAdapter mClipEndListener = new AnimatorListenerAdapter() { 89 @Override 90 public void onAnimationEnd(Animator animation) { 91 mPreviewClipper = null; 92 } 93 }; 94 private AnimatorListenerAdapter mCircleEndListener = new AnimatorListenerAdapter() { 95 @Override 96 public void onAnimationEnd(Animator animation) { 97 mCircleAnimator = null; 98 } 99 }; 100 private AnimatorListenerAdapter mScaleEndListener = new AnimatorListenerAdapter() { 101 @Override 102 public void onAnimationEnd(Animator animation) { 103 mScaleAnimator = null; 104 } 105 }; 106 private AnimatorListenerAdapter mAlphaEndListener = new AnimatorListenerAdapter() { 107 @Override 108 public void onAnimationEnd(Animator animation) { 109 mAlphaAnimator = null; 110 } 111 }; 112 KeyguardAffordanceView(Context context)113 public KeyguardAffordanceView(Context context) { 114 this(context, null); 115 } 116 KeyguardAffordanceView(Context context, AttributeSet attrs)117 public KeyguardAffordanceView(Context context, AttributeSet attrs) { 118 this(context, attrs, 0); 119 } 120 KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr)121 public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr) { 122 this(context, attrs, defStyleAttr, 0); 123 } 124 KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)125 public KeyguardAffordanceView(Context context, AttributeSet attrs, int defStyleAttr, 126 int defStyleRes) { 127 super(context, attrs, defStyleAttr, defStyleRes); 128 mCirclePaint = new Paint(); 129 mCirclePaint.setAntiAlias(true); 130 mCircleColor = 0xffffffff; 131 mCirclePaint.setColor(mCircleColor); 132 133 mNormalColor = 0xffffffff; 134 mInverseColor = 0xff000000; 135 mMinBackgroundRadius = mContext.getResources().getDimensionPixelSize( 136 R.dimen.keyguard_affordance_min_background_radius); 137 mAppearInterpolator = AnimationUtils.loadInterpolator(mContext, 138 android.R.interpolator.linear_out_slow_in); 139 mDisappearInterpolator = AnimationUtils.loadInterpolator(mContext, 140 android.R.interpolator.fast_out_linear_in); 141 mColorInterpolator = new ArgbEvaluator(); 142 mFlingAnimationUtils = new FlingAnimationUtils(mContext, 0.3f); 143 } 144 145 @Override onLayout(boolean changed, int left, int top, int right, int bottom)146 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 147 super.onLayout(changed, left, top, right, bottom); 148 mCenterX = getWidth() / 2; 149 mCenterY = getHeight() / 2; 150 mMaxCircleSize = getMaxCircleSize(); 151 } 152 153 @Override onDraw(Canvas canvas)154 protected void onDraw(Canvas canvas) { 155 mSupportHardware = canvas.isHardwareAccelerated(); 156 drawBackgroundCircle(canvas); 157 canvas.save(); 158 canvas.scale(mImageScale, mImageScale, getWidth() / 2, getHeight() / 2); 159 super.onDraw(canvas); 160 canvas.restore(); 161 } 162 setPreviewView(View v)163 public void setPreviewView(View v) { 164 mPreviewView = v; 165 if (mPreviewView != null) { 166 mPreviewView.setVisibility(INVISIBLE); 167 } 168 } 169 updateIconColor()170 private void updateIconColor() { 171 Drawable drawable = getDrawable().mutate(); 172 float alpha = mCircleRadius / mMinBackgroundRadius; 173 alpha = Math.min(1.0f, alpha); 174 int color = (int) mColorInterpolator.evaluate(alpha, mNormalColor, mInverseColor); 175 drawable.setColorFilter(color, PorterDuff.Mode.SRC_ATOP); 176 } 177 drawBackgroundCircle(Canvas canvas)178 private void drawBackgroundCircle(Canvas canvas) { 179 if (mCircleRadius > 0) { 180 if (mFinishing && mSupportHardware) { 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 updateCircleColor()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 finishAnimation(float velocity, final Runnable mAnimationEndRunnable)205 public void finishAnimation(float velocity, final Runnable mAnimationEndRunnable) { 206 cancelAnimator(mCircleAnimator); 207 cancelAnimator(mPreviewClipper); 208 mFinishing = true; 209 mCircleStartRadius = mCircleRadius; 210 float maxCircleSize = getMaxCircleSize(); 211 Animator animatorToRadius; 212 if (mSupportHardware) { 213 initHwProperties(); 214 animatorToRadius = getRtAnimatorToRadius(maxCircleSize); 215 } else { 216 animatorToRadius = getAnimatorToRadius(maxCircleSize); 217 } 218 mFlingAnimationUtils.applyDismissing(animatorToRadius, mCircleRadius, maxCircleSize, 219 velocity, maxCircleSize); 220 animatorToRadius.addListener(new AnimatorListenerAdapter() { 221 @Override 222 public void onAnimationEnd(Animator animation) { 223 mAnimationEndRunnable.run(); 224 mFinishing = false; 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 startRtCircleFadeOut(long duration)244 private void startRtCircleFadeOut(long duration) { 245 RenderNodeAnimator animator = new RenderNodeAnimator(mHwCirclePaint, 246 RenderNodeAnimator.PAINT_ALPHA, 0); 247 animator.setDuration(duration); 248 animator.setInterpolator(PhoneStatusBar.ALPHA_OUT); 249 animator.setTarget(this); 250 animator.start(); 251 } 252 getRtAnimatorToRadius(float circleRadius)253 private Animator getRtAnimatorToRadius(float circleRadius) { 254 RenderNodeAnimator animator = new RenderNodeAnimator(mHwCircleRadius, circleRadius); 255 animator.setTarget(this); 256 return animator; 257 } 258 initHwProperties()259 private void initHwProperties() { 260 mHwCenterX = CanvasProperty.createFloat(mCenterX); 261 mHwCenterY = CanvasProperty.createFloat(mCenterY); 262 mHwCirclePaint = CanvasProperty.createPaint(mCirclePaint); 263 mHwCircleRadius = CanvasProperty.createFloat(mCircleRadius); 264 } 265 getMaxCircleSize()266 private float getMaxCircleSize() { 267 getLocationInWindow(mTempPoint); 268 float rootWidth = getRootView().getWidth(); 269 float width = mTempPoint[0] + mCenterX; 270 width = Math.max(rootWidth - width, width); 271 float height = mTempPoint[1] + mCenterY; 272 return (float) Math.hypot(width, height); 273 } 274 setCircleRadius(float circleRadius)275 public void setCircleRadius(float circleRadius) { 276 setCircleRadius(circleRadius, false, false); 277 } 278 setCircleRadius(float circleRadius, boolean slowAnimation)279 public void setCircleRadius(float circleRadius, boolean slowAnimation) { 280 setCircleRadius(circleRadius, slowAnimation, false); 281 } 282 setCircleRadiusWithoutAnimation(float circleRadius)283 public void setCircleRadiusWithoutAnimation(float circleRadius) { 284 cancelAnimator(mCircleAnimator); 285 setCircleRadius(circleRadius, false ,true); 286 } 287 setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation)288 private void setCircleRadius(float circleRadius, boolean slowAnimation, boolean noAnimation) { 289 290 // Check if we need a new animation 291 boolean radiusHidden = (mCircleAnimator != null && mCircleWillBeHidden) 292 || (mCircleAnimator == null && mCircleRadius == 0.0f); 293 boolean nowHidden = circleRadius == 0.0f; 294 boolean radiusNeedsAnimation = (radiusHidden != nowHidden) && !noAnimation; 295 if (!radiusNeedsAnimation) { 296 if (mCircleAnimator == null) { 297 mCircleRadius = circleRadius; 298 updateIconColor(); 299 invalidate(); 300 if (nowHidden) { 301 if (mPreviewView != null) { 302 mPreviewView.setVisibility(View.INVISIBLE); 303 } 304 } 305 } else if (!mCircleWillBeHidden) { 306 307 // We just update the end value 308 float diff = circleRadius - mMinBackgroundRadius; 309 PropertyValuesHolder[] values = mCircleAnimator.getValues(); 310 values[0].setFloatValues(mCircleStartValue + diff, circleRadius); 311 mCircleAnimator.setCurrentPlayTime(mCircleAnimator.getCurrentPlayTime()); 312 } 313 } else { 314 cancelAnimator(mCircleAnimator); 315 cancelAnimator(mPreviewClipper); 316 ValueAnimator animator = getAnimatorToRadius(circleRadius); 317 Interpolator interpolator = circleRadius == 0.0f 318 ? mDisappearInterpolator 319 : mAppearInterpolator; 320 animator.setInterpolator(interpolator); 321 long duration = 250; 322 if (!slowAnimation) { 323 float durationFactor = Math.abs(mCircleRadius - circleRadius) 324 / (float) mMinBackgroundRadius; 325 duration = (long) (CIRCLE_APPEAR_DURATION * durationFactor); 326 duration = Math.min(duration, CIRCLE_DISAPPEAR_MAX_DURATION); 327 } 328 animator.setDuration(duration); 329 animator.start(); 330 if (mPreviewView != null && mPreviewView.getVisibility() == View.VISIBLE) { 331 mPreviewView.setVisibility(View.VISIBLE); 332 mPreviewClipper = ViewAnimationUtils.createCircularReveal( 333 mPreviewView, getLeft() + mCenterX, getTop() + mCenterY, mCircleRadius, 334 circleRadius); 335 mPreviewClipper.setInterpolator(interpolator); 336 mPreviewClipper.setDuration(duration); 337 mPreviewClipper.addListener(mClipEndListener); 338 mPreviewClipper.addListener(new AnimatorListenerAdapter() { 339 @Override 340 public void onAnimationEnd(Animator animation) { 341 mPreviewView.setVisibility(View.INVISIBLE); 342 } 343 }); 344 mPreviewClipper.start(); 345 } 346 } 347 } 348 getAnimatorToRadius(float circleRadius)349 private ValueAnimator getAnimatorToRadius(float circleRadius) { 350 ValueAnimator animator = ValueAnimator.ofFloat(mCircleRadius, circleRadius); 351 mCircleAnimator = animator; 352 mCircleStartValue = mCircleRadius; 353 mCircleWillBeHidden = circleRadius == 0.0f; 354 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 355 @Override 356 public void onAnimationUpdate(ValueAnimator animation) { 357 mCircleRadius = (float) animation.getAnimatedValue(); 358 updateIconColor(); 359 invalidate(); 360 } 361 }); 362 animator.addListener(mCircleEndListener); 363 return animator; 364 } 365 cancelAnimator(Animator animator)366 private void cancelAnimator(Animator animator) { 367 if (animator != null) { 368 animator.cancel(); 369 } 370 } 371 setImageScale(float imageScale, boolean animate)372 public void setImageScale(float imageScale, boolean animate) { 373 setImageScale(imageScale, animate, -1, null); 374 } 375 376 /** 377 * Sets the scale of the containing image 378 * 379 * @param imageScale The new Scale. 380 * @param animate Should an animation be performed 381 * @param duration If animate, whats the duration? When -1 we take the default duration 382 * @param interpolator If animate, whats the interpolator? When null we take the default 383 * interpolator. 384 */ setImageScale(float imageScale, boolean animate, long duration, Interpolator interpolator)385 public void setImageScale(float imageScale, boolean animate, long duration, 386 Interpolator interpolator) { 387 cancelAnimator(mScaleAnimator); 388 if (!animate) { 389 mImageScale = imageScale; 390 invalidate(); 391 } else { 392 ValueAnimator animator = ValueAnimator.ofFloat(mImageScale, imageScale); 393 mScaleAnimator = animator; 394 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 395 @Override 396 public void onAnimationUpdate(ValueAnimator animation) { 397 mImageScale = (float) animation.getAnimatedValue(); 398 invalidate(); 399 } 400 }); 401 animator.addListener(mScaleEndListener); 402 if (interpolator == null) { 403 interpolator = imageScale == 0.0f 404 ? mDisappearInterpolator 405 : mAppearInterpolator; 406 } 407 animator.setInterpolator(interpolator); 408 if (duration == -1) { 409 float durationFactor = Math.abs(mImageScale - imageScale) 410 / (1.0f - MIN_ICON_SCALE_AMOUNT); 411 durationFactor = Math.min(1.0f, durationFactor); 412 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor); 413 } 414 animator.setDuration(duration); 415 animator.start(); 416 } 417 } 418 setRestingAlpha(float alpha)419 public void setRestingAlpha(float alpha) { 420 mRestingAlpha = alpha; 421 422 // TODO: Handle the case an animation is playing. 423 setImageAlpha(alpha, false); 424 } 425 getRestingAlpha()426 public float getRestingAlpha() { 427 return mRestingAlpha; 428 } 429 setImageAlpha(float alpha, boolean animate)430 public void setImageAlpha(float alpha, boolean animate) { 431 setImageAlpha(alpha, animate, -1, null, null); 432 } 433 434 /** 435 * Sets the alpha of the containing image 436 * 437 * @param alpha The new alpha. 438 * @param animate Should an animation be performed 439 * @param duration If animate, whats the duration? When -1 we take the default duration 440 * @param interpolator If animate, whats the interpolator? When null we take the default 441 * interpolator. 442 */ setImageAlpha(float alpha, boolean animate, long duration, Interpolator interpolator, Runnable runnable)443 public void setImageAlpha(float alpha, boolean animate, long duration, 444 Interpolator interpolator, Runnable runnable) { 445 cancelAnimator(mAlphaAnimator); 446 int endAlpha = (int) (alpha * 255); 447 final Drawable background = getBackground(); 448 if (!animate) { 449 if (background != null) background.mutate().setAlpha(endAlpha); 450 setImageAlpha(endAlpha); 451 } else { 452 int currentAlpha = getImageAlpha(); 453 ValueAnimator animator = ValueAnimator.ofInt(currentAlpha, endAlpha); 454 mAlphaAnimator = animator; 455 animator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 456 @Override 457 public void onAnimationUpdate(ValueAnimator animation) { 458 int alpha = (int) animation.getAnimatedValue(); 459 if (background != null) background.mutate().setAlpha(alpha); 460 setImageAlpha(alpha); 461 } 462 }); 463 animator.addListener(mAlphaEndListener); 464 if (interpolator == null) { 465 interpolator = alpha == 0.0f 466 ? mDisappearInterpolator 467 : mAppearInterpolator; 468 } 469 animator.setInterpolator(interpolator); 470 if (duration == -1) { 471 float durationFactor = Math.abs(currentAlpha - endAlpha) / 255f; 472 durationFactor = Math.min(1.0f, durationFactor); 473 duration = (long) (NORMAL_ANIMATION_DURATION * durationFactor); 474 } 475 animator.setDuration(duration); 476 if (runnable != null) { 477 animator.addListener(getEndListener(runnable)); 478 } 479 animator.start(); 480 } 481 } 482 getEndListener(final Runnable runnable)483 private Animator.AnimatorListener getEndListener(final Runnable runnable) { 484 return new AnimatorListenerAdapter() { 485 boolean mCancelled; 486 @Override 487 public void onAnimationCancel(Animator animation) { 488 mCancelled = true; 489 } 490 491 @Override 492 public void onAnimationEnd(Animator animation) { 493 if (!mCancelled) { 494 runnable.run(); 495 } 496 } 497 }; 498 } 499 getCircleRadius()500 public float getCircleRadius() { 501 return mCircleRadius; 502 } 503 504 @Override performClick()505 public boolean performClick() { 506 if (isClickable()) { 507 return super.performClick(); 508 } else { 509 return false; 510 } 511 } 512 } 513