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