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