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.ObjectAnimator; 22 import android.animation.ValueAnimator; 23 import android.content.Context; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapShader; 26 import android.graphics.Canvas; 27 import android.graphics.Color; 28 import android.graphics.Paint; 29 import android.graphics.PorterDuff; 30 import android.graphics.PorterDuffColorFilter; 31 import android.graphics.RectF; 32 import android.graphics.Shader; 33 import android.util.AttributeSet; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.ViewAnimationUtils; 37 import android.view.ViewConfiguration; 38 import android.view.animation.AnimationUtils; 39 import android.view.animation.Interpolator; 40 import android.view.animation.LinearInterpolator; 41 import android.view.animation.PathInterpolator; 42 43 import com.android.systemui.R; 44 45 /** 46 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer} 47 * to implement dimming/activating on Keyguard for the double-tap gesture 48 */ 49 public abstract class ActivatableNotificationView extends ExpandableOutlineView { 50 51 private static final long DOUBLETAP_TIMEOUT_MS = 1200; 52 private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220; 53 private static final int ACTIVATE_ANIMATION_LENGTH = 220; 54 private static final int DARK_ANIMATION_LENGTH = 170; 55 56 /** 57 * The amount of width, which is kept in the end when performing a disappear animation (also 58 * the amount from which the horizontal appearing begins) 59 */ 60 private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f; 61 62 /** 63 * At which point from [0,1] does the horizontal collapse animation end (or start when 64 * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated. 65 */ 66 private static final float HORIZONTAL_ANIMATION_END = 0.2f; 67 68 /** 69 * At which point from [0,1] does the alpha animation end (or start when 70 * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated. 71 */ 72 private static final float ALPHA_ANIMATION_END = 0.0f; 73 74 /** 75 * At which point from [0,1] does the horizontal collapse animation start (or start when 76 * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all. 77 */ 78 private static final float HORIZONTAL_ANIMATION_START = 1.0f; 79 80 /** 81 * At which point from [0,1] does the vertical collapse animation start (or end when 82 * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all. 83 */ 84 private static final float VERTICAL_ANIMATION_START = 1.0f; 85 86 /** 87 * Scale for the background to animate from when exiting dark mode. 88 */ 89 private static final float DARK_EXIT_SCALE_START = 0.93f; 90 91 private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR 92 = new PathInterpolator(0.6f, 0, 0.5f, 1); 93 private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR 94 = new PathInterpolator(0, 0, 0.5f, 1); 95 private final int mTintedRippleColor; 96 private final int mLowPriorityRippleColor; 97 private final int mNormalRippleColor; 98 99 private boolean mDimmed; 100 private boolean mDark; 101 102 private int mBgTint = 0; 103 private final int mRoundedRectCornerRadius; 104 105 /** 106 * Flag to indicate that the notification has been touched once and the second touch will 107 * click it. 108 */ 109 private boolean mActivated; 110 111 private float mDownX; 112 private float mDownY; 113 private final float mTouchSlop; 114 115 private OnActivatedListener mOnActivatedListener; 116 117 private final Interpolator mLinearOutSlowInInterpolator; 118 private final Interpolator mFastOutSlowInInterpolator; 119 private final Interpolator mSlowOutFastInInterpolator; 120 private final Interpolator mSlowOutLinearInInterpolator; 121 private final Interpolator mLinearInterpolator; 122 private Interpolator mCurrentAppearInterpolator; 123 private Interpolator mCurrentAlphaInterpolator; 124 125 private NotificationBackgroundView mBackgroundNormal; 126 private NotificationBackgroundView mBackgroundDimmed; 127 private ObjectAnimator mBackgroundAnimator; 128 private RectF mAppearAnimationRect = new RectF(); 129 private PorterDuffColorFilter mAppearAnimationFilter; 130 private float mAnimationTranslationY; 131 private boolean mDrawingAppearAnimation; 132 private Paint mAppearPaint = new Paint(); 133 private ValueAnimator mAppearAnimator; 134 private float mAppearAnimationFraction = -1.0f; 135 private float mAppearAnimationTranslation; 136 private boolean mShowingLegacyBackground; 137 private final int mLegacyColor; 138 private final int mNormalColor; 139 private final int mLowPriorityColor; 140 private boolean mIsBelowSpeedBump; 141 142 public ActivatableNotificationView(Context context, AttributeSet attrs) { 143 super(context, attrs); 144 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 145 mFastOutSlowInInterpolator = 146 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 147 mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f); 148 mLinearOutSlowInInterpolator = 149 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); 150 mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f); 151 mLinearInterpolator = new LinearInterpolator(); 152 setClipChildren(false); 153 setClipToPadding(false); 154 mAppearAnimationFilter = new PorterDuffColorFilter(0, PorterDuff.Mode.SRC_ATOP); 155 mRoundedRectCornerRadius = getResources().getDimensionPixelSize( 156 R.dimen.notification_material_rounded_rect_radius); 157 mLegacyColor = getResources().getColor(R.color.notification_legacy_background_color); 158 mNormalColor = getResources().getColor(R.color.notification_material_background_color); 159 mLowPriorityColor = getResources().getColor( 160 R.color.notification_material_background_low_priority_color); 161 mTintedRippleColor = context.getResources().getColor( 162 R.color.notification_ripple_tinted_color); 163 mLowPriorityRippleColor = context.getResources().getColor( 164 R.color.notification_ripple_color_low_priority); 165 mNormalRippleColor = context.getResources().getColor( 166 R.color.notification_ripple_untinted_color); 167 } 168 169 @Override 170 protected void onFinishInflate() { 171 super.onFinishInflate(); 172 mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal); 173 mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed); 174 mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg); 175 mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim); 176 updateBackground(); 177 updateBackgroundTint(); 178 } 179 180 private final Runnable mTapTimeoutRunnable = new Runnable() { 181 @Override 182 public void run() { 183 makeInactive(true /* animate */); 184 } 185 }; 186 187 @Override 188 public boolean onTouchEvent(MotionEvent event) { 189 if (mDimmed) { 190 return handleTouchEventDimmed(event); 191 } else { 192 return super.onTouchEvent(event); 193 } 194 } 195 196 @Override 197 public void drawableHotspotChanged(float x, float y) { 198 if (!mDimmed){ 199 mBackgroundNormal.drawableHotspotChanged(x, y); 200 } 201 } 202 203 @Override 204 protected void drawableStateChanged() { 205 super.drawableStateChanged(); 206 if (mDimmed) { 207 mBackgroundDimmed.setState(getDrawableState()); 208 } else { 209 mBackgroundNormal.setState(getDrawableState()); 210 } 211 } 212 213 private boolean handleTouchEventDimmed(MotionEvent event) { 214 int action = event.getActionMasked(); 215 switch (action) { 216 case MotionEvent.ACTION_DOWN: 217 mDownX = event.getX(); 218 mDownY = event.getY(); 219 if (mDownY > getActualHeight()) { 220 return false; 221 } 222 break; 223 case MotionEvent.ACTION_MOVE: 224 if (!isWithinTouchSlop(event)) { 225 makeInactive(true /* animate */); 226 return false; 227 } 228 break; 229 case MotionEvent.ACTION_UP: 230 if (isWithinTouchSlop(event)) { 231 if (!mActivated) { 232 makeActive(); 233 postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS); 234 } else { 235 boolean performed = performClick(); 236 if (performed) { 237 removeCallbacks(mTapTimeoutRunnable); 238 } 239 } 240 } else { 241 makeInactive(true /* animate */); 242 } 243 break; 244 case MotionEvent.ACTION_CANCEL: 245 makeInactive(true /* animate */); 246 break; 247 default: 248 break; 249 } 250 return true; 251 } 252 253 private void makeActive() { 254 startActivateAnimation(false /* reverse */); 255 mActivated = true; 256 if (mOnActivatedListener != null) { 257 mOnActivatedListener.onActivated(this); 258 } 259 } 260 261 private void startActivateAnimation(boolean reverse) { 262 if (!isAttachedToWindow()) { 263 return; 264 } 265 int widthHalf = mBackgroundNormal.getWidth()/2; 266 int heightHalf = mBackgroundNormal.getActualHeight()/2; 267 float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf); 268 Animator animator; 269 if (reverse) { 270 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 271 widthHalf, heightHalf, radius, 0); 272 } else { 273 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 274 widthHalf, heightHalf, 0, radius); 275 } 276 mBackgroundNormal.setVisibility(View.VISIBLE); 277 Interpolator interpolator; 278 Interpolator alphaInterpolator; 279 if (!reverse) { 280 interpolator = mLinearOutSlowInInterpolator; 281 alphaInterpolator = mLinearOutSlowInInterpolator; 282 } else { 283 interpolator = ACTIVATE_INVERSE_INTERPOLATOR; 284 alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR; 285 } 286 animator.setInterpolator(interpolator); 287 animator.setDuration(ACTIVATE_ANIMATION_LENGTH); 288 if (reverse) { 289 mBackgroundNormal.setAlpha(1f); 290 animator.addListener(new AnimatorListenerAdapter() { 291 @Override 292 public void onAnimationEnd(Animator animation) { 293 if (mDimmed) { 294 mBackgroundNormal.setVisibility(View.INVISIBLE); 295 } 296 } 297 }); 298 animator.start(); 299 } else { 300 mBackgroundNormal.setAlpha(0.4f); 301 animator.start(); 302 } 303 mBackgroundNormal.animate() 304 .alpha(reverse ? 0f : 1f) 305 .setInterpolator(alphaInterpolator) 306 .setDuration(ACTIVATE_ANIMATION_LENGTH); 307 } 308 309 /** 310 * Cancels the hotspot and makes the notification inactive. 311 */ 312 public void makeInactive(boolean animate) { 313 if (mActivated) { 314 if (mDimmed) { 315 if (animate) { 316 startActivateAnimation(true /* reverse */); 317 } else { 318 mBackgroundNormal.setVisibility(View.INVISIBLE); 319 } 320 } 321 mActivated = false; 322 } 323 if (mOnActivatedListener != null) { 324 mOnActivatedListener.onActivationReset(this); 325 } 326 removeCallbacks(mTapTimeoutRunnable); 327 } 328 329 private boolean isWithinTouchSlop(MotionEvent event) { 330 return Math.abs(event.getX() - mDownX) < mTouchSlop 331 && Math.abs(event.getY() - mDownY) < mTouchSlop; 332 } 333 334 public void setDimmed(boolean dimmed, boolean fade) { 335 if (mDimmed != dimmed) { 336 mDimmed = dimmed; 337 if (fade) { 338 fadeDimmedBackground(); 339 } else { 340 updateBackground(); 341 } 342 } 343 } 344 345 public void setDark(boolean dark, boolean fade, long delay) { 346 super.setDark(dark, fade, delay); 347 if (mDark == dark) { 348 return; 349 } 350 mDark = dark; 351 if (!dark && fade) { 352 if (mActivated) { 353 mBackgroundDimmed.setVisibility(View.VISIBLE); 354 mBackgroundNormal.setVisibility(View.VISIBLE); 355 } else if (mDimmed) { 356 mBackgroundDimmed.setVisibility(View.VISIBLE); 357 mBackgroundNormal.setVisibility(View.INVISIBLE); 358 } else { 359 mBackgroundDimmed.setVisibility(View.INVISIBLE); 360 mBackgroundNormal.setVisibility(View.VISIBLE); 361 } 362 fadeInFromDark(delay); 363 } else { 364 updateBackground(); 365 } 366 } 367 368 public void setShowingLegacyBackground(boolean showing) { 369 mShowingLegacyBackground = showing; 370 updateBackgroundTint(); 371 } 372 373 @Override 374 public void setBelowSpeedBump(boolean below) { 375 super.setBelowSpeedBump(below); 376 if (below != mIsBelowSpeedBump) { 377 mIsBelowSpeedBump = below; 378 updateBackgroundTint(); 379 } 380 } 381 382 /** 383 * Sets the tint color of the background 384 */ 385 public void setTintColor(int color) { 386 mBgTint = color; 387 updateBackgroundTint(); 388 } 389 390 private void updateBackgroundTint() { 391 int color = getBackgroundColor(); 392 int rippleColor = getRippleColor(); 393 if (color == mNormalColor) { 394 // We don't need to tint a normal notification 395 color = 0; 396 } 397 mBackgroundDimmed.setTint(color); 398 mBackgroundNormal.setTint(color); 399 mBackgroundDimmed.setRippleColor(rippleColor); 400 mBackgroundNormal.setRippleColor(rippleColor); 401 } 402 403 /** 404 * Fades in the background when exiting dark mode. 405 */ 406 private void fadeInFromDark(long delay) { 407 final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal; 408 background.setAlpha(0f); 409 background.setPivotX(mBackgroundDimmed.getWidth() / 2f); 410 background.setPivotY(getActualHeight() / 2f); 411 background.setScaleX(DARK_EXIT_SCALE_START); 412 background.setScaleY(DARK_EXIT_SCALE_START); 413 background.animate() 414 .alpha(1f) 415 .scaleX(1f) 416 .scaleY(1f) 417 .setDuration(DARK_ANIMATION_LENGTH) 418 .setStartDelay(delay) 419 .setInterpolator(mLinearOutSlowInInterpolator) 420 .setListener(new AnimatorListenerAdapter() { 421 @Override 422 public void onAnimationCancel(Animator animation) { 423 // Jump state if we are cancelled 424 background.setScaleX(1f); 425 background.setScaleY(1f); 426 background.setAlpha(1f); 427 } 428 }) 429 .start(); 430 } 431 432 /** 433 * Fades the background when the dimmed state changes. 434 */ 435 private void fadeDimmedBackground() { 436 mBackgroundDimmed.animate().cancel(); 437 mBackgroundNormal.animate().cancel(); 438 if (mDimmed) { 439 mBackgroundDimmed.setVisibility(View.VISIBLE); 440 } else { 441 mBackgroundNormal.setVisibility(View.VISIBLE); 442 } 443 float startAlpha = mDimmed ? 1f : 0; 444 float endAlpha = mDimmed ? 0 : 1f; 445 int duration = BACKGROUND_ANIMATION_LENGTH_MS; 446 // Check whether there is already a background animation running. 447 if (mBackgroundAnimator != null) { 448 startAlpha = (Float) mBackgroundAnimator.getAnimatedValue(); 449 duration = (int) mBackgroundAnimator.getCurrentPlayTime(); 450 mBackgroundAnimator.removeAllListeners(); 451 mBackgroundAnimator.cancel(); 452 if (duration <= 0) { 453 updateBackground(); 454 return; 455 } 456 } 457 mBackgroundNormal.setAlpha(startAlpha); 458 mBackgroundAnimator = 459 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha); 460 mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator); 461 mBackgroundAnimator.setDuration(duration); 462 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 463 @Override 464 public void onAnimationEnd(Animator animation) { 465 if (mDimmed) { 466 mBackgroundNormal.setVisibility(View.INVISIBLE); 467 } else { 468 mBackgroundDimmed.setVisibility(View.INVISIBLE); 469 } 470 mBackgroundAnimator = null; 471 } 472 }); 473 mBackgroundAnimator.start(); 474 } 475 476 private void updateBackground() { 477 cancelFadeAnimations(); 478 if (mDark) { 479 mBackgroundDimmed.setVisibility(View.INVISIBLE); 480 mBackgroundNormal.setVisibility(View.INVISIBLE); 481 } else if (mDimmed) { 482 mBackgroundDimmed.setVisibility(View.VISIBLE); 483 mBackgroundNormal.setVisibility(View.INVISIBLE); 484 } else { 485 mBackgroundDimmed.setVisibility(View.INVISIBLE); 486 mBackgroundNormal.setVisibility(View.VISIBLE); 487 mBackgroundNormal.setAlpha(1f); 488 removeCallbacks(mTapTimeoutRunnable); 489 } 490 } 491 492 private void cancelFadeAnimations() { 493 if (mBackgroundAnimator != null) { 494 mBackgroundAnimator.cancel(); 495 } 496 mBackgroundDimmed.animate().cancel(); 497 mBackgroundNormal.animate().cancel(); 498 } 499 500 @Override 501 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 502 super.onLayout(changed, left, top, right, bottom); 503 setPivotX(getWidth() / 2); 504 } 505 506 @Override 507 public void setActualHeight(int actualHeight, boolean notifyListeners) { 508 super.setActualHeight(actualHeight, notifyListeners); 509 setPivotY(actualHeight / 2); 510 mBackgroundNormal.setActualHeight(actualHeight); 511 mBackgroundDimmed.setActualHeight(actualHeight); 512 } 513 514 @Override 515 public void setClipTopAmount(int clipTopAmount) { 516 super.setClipTopAmount(clipTopAmount); 517 mBackgroundNormal.setClipTopAmount(clipTopAmount); 518 mBackgroundDimmed.setClipTopAmount(clipTopAmount); 519 } 520 521 @Override 522 public void performRemoveAnimation(long duration, float translationDirection, 523 Runnable onFinishedRunnable) { 524 enableAppearDrawing(true); 525 if (mDrawingAppearAnimation) { 526 startAppearAnimation(false /* isAppearing */, translationDirection, 527 0, duration, onFinishedRunnable); 528 } else if (onFinishedRunnable != null) { 529 onFinishedRunnable.run(); 530 } 531 } 532 533 @Override 534 public void performAddAnimation(long delay, long duration) { 535 enableAppearDrawing(true); 536 if (mDrawingAppearAnimation) { 537 startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null); 538 } 539 } 540 541 private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, 542 long duration, final Runnable onFinishedRunnable) { 543 if (mAppearAnimator != null) { 544 mAppearAnimator.cancel(); 545 } 546 mAnimationTranslationY = translationDirection * getActualHeight(); 547 if (mAppearAnimationFraction == -1.0f) { 548 // not initialized yet, we start anew 549 if (isAppearing) { 550 mAppearAnimationFraction = 0.0f; 551 mAppearAnimationTranslation = mAnimationTranslationY; 552 } else { 553 mAppearAnimationFraction = 1.0f; 554 mAppearAnimationTranslation = 0; 555 } 556 } 557 558 float targetValue; 559 if (isAppearing) { 560 mCurrentAppearInterpolator = mSlowOutFastInInterpolator; 561 mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator; 562 targetValue = 1.0f; 563 } else { 564 mCurrentAppearInterpolator = mFastOutSlowInInterpolator; 565 mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator; 566 targetValue = 0.0f; 567 } 568 mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, 569 targetValue); 570 mAppearAnimator.setInterpolator(mLinearInterpolator); 571 mAppearAnimator.setDuration( 572 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); 573 mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 574 @Override 575 public void onAnimationUpdate(ValueAnimator animation) { 576 mAppearAnimationFraction = (float) animation.getAnimatedValue(); 577 updateAppearAnimationAlpha(); 578 updateAppearRect(); 579 invalidate(); 580 } 581 }); 582 if (delay > 0) { 583 // we need to apply the initial state already to avoid drawn frames in the wrong state 584 updateAppearAnimationAlpha(); 585 updateAppearRect(); 586 mAppearAnimator.setStartDelay(delay); 587 } 588 mAppearAnimator.addListener(new AnimatorListenerAdapter() { 589 private boolean mWasCancelled; 590 591 @Override 592 public void onAnimationEnd(Animator animation) { 593 if (onFinishedRunnable != null) { 594 onFinishedRunnable.run(); 595 } 596 if (!mWasCancelled) { 597 mAppearAnimationFraction = -1; 598 setOutlineRect(null); 599 enableAppearDrawing(false); 600 } 601 } 602 603 @Override 604 public void onAnimationStart(Animator animation) { 605 mWasCancelled = false; 606 } 607 608 @Override 609 public void onAnimationCancel(Animator animation) { 610 mWasCancelled = true; 611 } 612 }); 613 mAppearAnimator.start(); 614 } 615 616 private void updateAppearRect() { 617 float inverseFraction = (1.0f - mAppearAnimationFraction); 618 float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction); 619 float translateYTotalAmount = translationFraction * mAnimationTranslationY; 620 mAppearAnimationTranslation = translateYTotalAmount; 621 622 // handle width animation 623 float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START)) 624 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END); 625 widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction)); 626 widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction); 627 float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) * 628 widthFraction); 629 float right = getWidth() - left; 630 631 // handle top animation 632 float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) / 633 VERTICAL_ANIMATION_START; 634 heightFraction = Math.max(0.0f, heightFraction); 635 heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction); 636 637 float top; 638 float bottom; 639 final int actualHeight = getActualHeight(); 640 if (mAnimationTranslationY > 0.0f) { 641 bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f 642 - translateYTotalAmount; 643 top = bottom * heightFraction; 644 } else { 645 top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f - 646 translateYTotalAmount; 647 bottom = actualHeight * (1 - heightFraction) + top * heightFraction; 648 } 649 mAppearAnimationRect.set(left, top, right, bottom); 650 setOutlineRect(left, top + mAppearAnimationTranslation, right, 651 bottom + mAppearAnimationTranslation); 652 } 653 654 private void updateAppearAnimationAlpha() { 655 int backgroundColor = getBackgroundColor(); 656 if (backgroundColor != -1) { 657 float contentAlphaProgress = mAppearAnimationFraction; 658 contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END); 659 contentAlphaProgress = Math.min(1.0f, contentAlphaProgress); 660 contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress); 661 int sourceColor = Color.argb((int) (255 * (1.0f - contentAlphaProgress)), 662 Color.red(backgroundColor), Color.green(backgroundColor), 663 Color.blue(backgroundColor)); 664 mAppearAnimationFilter.setColor(sourceColor); 665 mAppearPaint.setColorFilter(mAppearAnimationFilter); 666 } 667 } 668 669 private int getBackgroundColor() { 670 if (mBgTint != 0) { 671 return mBgTint; 672 } else if (mShowingLegacyBackground) { 673 return mLegacyColor; 674 } else if (mIsBelowSpeedBump) { 675 return mLowPriorityColor; 676 } else { 677 return mNormalColor; 678 } 679 } 680 681 private int getRippleColor() { 682 if (mBgTint != 0) { 683 return mTintedRippleColor; 684 } else if (mShowingLegacyBackground) { 685 return mTintedRippleColor; 686 } else if (mIsBelowSpeedBump) { 687 return mLowPriorityRippleColor; 688 } else { 689 return mNormalRippleColor; 690 } 691 } 692 693 /** 694 * When we draw the appear animation, we render the view in a bitmap and render this bitmap 695 * as a shader of a rect. This call creates the Bitmap and switches the drawing mode, 696 * such that the normal drawing of the views does not happen anymore. 697 * 698 * @param enable Should it be enabled. 699 */ 700 private void enableAppearDrawing(boolean enable) { 701 if (enable != mDrawingAppearAnimation) { 702 if (enable) { 703 if (getWidth() == 0 || getActualHeight() == 0) { 704 // TODO: This should not happen, but it can during expansion. Needs 705 // investigation 706 return; 707 } 708 Bitmap bitmap = Bitmap.createBitmap(getWidth(), getActualHeight(), 709 Bitmap.Config.ARGB_8888); 710 Canvas canvas = new Canvas(bitmap); 711 draw(canvas); 712 mAppearPaint.setShader(new BitmapShader(bitmap, Shader.TileMode.CLAMP, 713 Shader.TileMode.CLAMP)); 714 } else { 715 mAppearPaint.setShader(null); 716 } 717 mDrawingAppearAnimation = enable; 718 invalidate(); 719 } 720 } 721 722 @Override 723 protected void dispatchDraw(Canvas canvas) { 724 if (!mDrawingAppearAnimation) { 725 super.dispatchDraw(canvas); 726 } else { 727 drawAppearRect(canvas); 728 } 729 } 730 731 private void drawAppearRect(Canvas canvas) { 732 canvas.save(); 733 canvas.translate(0, mAppearAnimationTranslation); 734 canvas.drawRoundRect(mAppearAnimationRect, mRoundedRectCornerRadius, 735 mRoundedRectCornerRadius, mAppearPaint); 736 canvas.restore(); 737 } 738 739 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 740 mOnActivatedListener = onActivatedListener; 741 } 742 743 public void reset() { 744 setTintColor(0); 745 setShowingLegacyBackground(false); 746 setBelowSpeedBump(false); 747 } 748 749 public interface OnActivatedListener { 750 void onActivated(ActivatableNotificationView view); 751 void onActivationReset(ActivatableNotificationView view); 752 } 753 } 754