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.TimeAnimator; 23 import android.animation.ValueAnimator; 24 import android.content.Context; 25 import android.graphics.Canvas; 26 import android.graphics.Color; 27 import android.graphics.RectF; 28 import android.util.AttributeSet; 29 import android.util.MathUtils; 30 import android.view.MotionEvent; 31 import android.view.View; 32 import android.view.ViewAnimationUtils; 33 import android.view.accessibility.AccessibilityManager; 34 import android.view.animation.Interpolator; 35 import android.view.animation.PathInterpolator; 36 37 import com.android.systemui.Interpolators; 38 import com.android.systemui.R; 39 import com.android.systemui.classifier.FalsingManager; 40 import com.android.systemui.statusbar.notification.FakeShadowView; 41 import com.android.systemui.statusbar.notification.NotificationUtils; 42 import com.android.systemui.statusbar.phone.DoubleTapHelper; 43 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 44 import com.android.systemui.statusbar.stack.StackStateAnimator; 45 46 /** 47 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationShelf} 48 * to implement dimming/activating on Keyguard for the double-tap gesture 49 */ 50 public abstract class ActivatableNotificationView extends ExpandableOutlineView { 51 52 private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220; 53 private static final int ACTIVATE_ANIMATION_LENGTH = 220; 54 private static final long DARK_ANIMATION_LENGTH = StackStateAnimator.ANIMATION_DURATION_WAKEUP; 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 /** 92 * A sentinel value when no color should be used. Can be used with {@link #setTintColor(int)} 93 * or {@link #setOverrideTintColor(int, float)}. 94 */ 95 protected static final int NO_COLOR = 0; 96 97 private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR 98 = new PathInterpolator(0.6f, 0, 0.5f, 1); 99 private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR 100 = new PathInterpolator(0, 0, 0.5f, 1); 101 private final int mTintedRippleColor; 102 protected final int mNormalRippleColor; 103 private final AccessibilityManager mAccessibilityManager; 104 private final DoubleTapHelper mDoubleTapHelper; 105 106 private boolean mDimmed; 107 private boolean mDark; 108 109 protected int mBgTint = NO_COLOR; 110 private float mBgAlpha = 1f; 111 112 /** 113 * Flag to indicate that the notification has been touched once and the second touch will 114 * click it. 115 */ 116 private boolean mActivated; 117 118 private OnActivatedListener mOnActivatedListener; 119 120 private final Interpolator mSlowOutFastInInterpolator; 121 private final Interpolator mSlowOutLinearInInterpolator; 122 private Interpolator mCurrentAppearInterpolator; 123 private Interpolator mCurrentAlphaInterpolator; 124 125 protected NotificationBackgroundView mBackgroundNormal; 126 private NotificationBackgroundView mBackgroundDimmed; 127 private ObjectAnimator mBackgroundAnimator; 128 private RectF mAppearAnimationRect = new RectF(); 129 private float mAnimationTranslationY; 130 private boolean mDrawingAppearAnimation; 131 private ValueAnimator mAppearAnimator; 132 private ValueAnimator mBackgroundColorAnimator; 133 private float mAppearAnimationFraction = -1.0f; 134 private float mAppearAnimationTranslation; 135 private final int mNormalColor; 136 private boolean mIsBelowSpeedBump; 137 private FalsingManager mFalsingManager; 138 139 private float mNormalBackgroundVisibilityAmount; 140 private ValueAnimator mFadeInFromDarkAnimator; 141 private float mDimmedBackgroundFadeInAmount = -1; 142 private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater 143 = new ValueAnimator.AnimatorUpdateListener() { 144 @Override 145 public void onAnimationUpdate(ValueAnimator animation) { 146 setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha()); 147 mDimmedBackgroundFadeInAmount = mBackgroundDimmed.getAlpha(); 148 } 149 }; 150 private AnimatorListenerAdapter mFadeInEndListener = new AnimatorListenerAdapter() { 151 @Override 152 public void onAnimationEnd(Animator animation) { 153 super.onAnimationEnd(animation); 154 mFadeInFromDarkAnimator = null; 155 mDimmedBackgroundFadeInAmount = -1; 156 updateBackground(); 157 } 158 }; 159 private ValueAnimator.AnimatorUpdateListener mUpdateOutlineListener 160 = new ValueAnimator.AnimatorUpdateListener() { 161 @Override 162 public void onAnimationUpdate(ValueAnimator animation) { 163 updateOutlineAlpha(); 164 } 165 }; 166 private float mShadowAlpha = 1.0f; 167 private FakeShadowView mFakeShadow; 168 private int mCurrentBackgroundTint; 169 private int mTargetTint; 170 private int mStartTint; 171 private int mOverrideTint; 172 private float mOverrideAmount; 173 private boolean mShadowHidden; 174 /** 175 * Similar to mDimmed but is also true if it's not dimmable but should be 176 */ 177 private boolean mNeedsDimming; 178 private int mDimmedAlpha; 179 private boolean mBlockNextTouch; 180 private boolean mIsHeadsUpAnimation; 181 private int mHeadsUpAddStartLocation; 182 private float mHeadsUpLocation; 183 private boolean mIsAppearing; 184 185 public ActivatableNotificationView(Context context, AttributeSet attrs) { 186 super(context, attrs); 187 mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f); 188 mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f); 189 setClipChildren(false); 190 setClipToPadding(false); 191 mNormalColor = context.getColor(R.color.notification_material_background_color); 192 mTintedRippleColor = context.getColor( 193 R.color.notification_ripple_tinted_color); 194 mNormalRippleColor = context.getColor( 195 R.color.notification_ripple_untinted_color); 196 mFalsingManager = FalsingManager.getInstance(context); 197 mAccessibilityManager = AccessibilityManager.getInstance(mContext); 198 199 mDoubleTapHelper = new DoubleTapHelper(this, (active) -> { 200 if (active) { 201 makeActive(); 202 } else { 203 makeInactive(true /* animate */); 204 } 205 }, super::performClick, this::handleSlideBack, mFalsingManager::onNotificationDoubleTap); 206 initDimens(); 207 } 208 209 private void initDimens() { 210 mHeadsUpAddStartLocation = getResources().getDimensionPixelSize( 211 com.android.internal.R.dimen.notification_content_margin_start); 212 } 213 214 @Override 215 public void onDensityOrFontScaleChanged() { 216 super.onDensityOrFontScaleChanged(); 217 initDimens(); 218 } 219 220 @Override 221 protected void onFinishInflate() { 222 super.onFinishInflate(); 223 mBackgroundNormal = findViewById(R.id.backgroundNormal); 224 mFakeShadow = findViewById(R.id.fake_shadow); 225 mShadowHidden = mFakeShadow.getVisibility() != VISIBLE; 226 mBackgroundDimmed = findViewById(R.id.backgroundDimmed); 227 mDimmedAlpha = Color.alpha(mContext.getColor( 228 R.color.notification_material_background_dimmed_color)); 229 initBackground(); 230 updateBackground(); 231 updateBackgroundTint(); 232 updateOutlineAlpha(); 233 } 234 235 /** 236 * Sets the custom backgrounds on {@link #mBackgroundNormal} and {@link #mBackgroundDimmed}. 237 * This method can also be used to reload the backgrounds on both of those views, which can 238 * be useful in a configuration change. 239 */ 240 protected void initBackground() { 241 mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg); 242 mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim); 243 } 244 245 private final Runnable mTapTimeoutRunnable = new Runnable() { 246 @Override 247 public void run() { 248 makeInactive(true /* animate */); 249 } 250 }; 251 252 @Override 253 public boolean onInterceptTouchEvent(MotionEvent ev) { 254 if (mNeedsDimming && ev.getActionMasked() == MotionEvent.ACTION_DOWN 255 && disallowSingleClick(ev) && !isTouchExplorationEnabled()) { 256 if (!mActivated) { 257 return true; 258 } else if (!mDoubleTapHelper.isWithinDoubleTapSlop(ev)) { 259 mBlockNextTouch = true; 260 makeInactive(true /* animate */); 261 return true; 262 } 263 } 264 return super.onInterceptTouchEvent(ev); 265 } 266 267 private boolean isTouchExplorationEnabled() { 268 return mAccessibilityManager.isTouchExplorationEnabled(); 269 } 270 271 protected boolean disallowSingleClick(MotionEvent ev) { 272 return false; 273 } 274 275 protected boolean handleSlideBack() { 276 return false; 277 } 278 279 @Override 280 public boolean onTouchEvent(MotionEvent event) { 281 boolean result; 282 if (mBlockNextTouch) { 283 mBlockNextTouch = false; 284 return false; 285 } 286 if (mNeedsDimming && !isTouchExplorationEnabled() && isInteractive()) { 287 boolean wasActivated = mActivated; 288 result = handleTouchEventDimmed(event); 289 if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) { 290 removeCallbacks(mTapTimeoutRunnable); 291 } 292 } else { 293 result = super.onTouchEvent(event); 294 } 295 return result; 296 } 297 298 /** 299 * @return whether this view is interactive and can be double tapped 300 */ 301 protected boolean isInteractive() { 302 return true; 303 } 304 305 @Override 306 public void drawableHotspotChanged(float x, float y) { 307 if (!mDimmed){ 308 mBackgroundNormal.drawableHotspotChanged(x, y); 309 } 310 } 311 312 @Override 313 protected void drawableStateChanged() { 314 super.drawableStateChanged(); 315 if (mDimmed) { 316 mBackgroundDimmed.setState(getDrawableState()); 317 } else { 318 mBackgroundNormal.setState(getDrawableState()); 319 } 320 } 321 322 public void setRippleAllowed(boolean allowed) { 323 mBackgroundNormal.setPressedAllowed(allowed); 324 } 325 326 private boolean handleTouchEventDimmed(MotionEvent event) { 327 if (mNeedsDimming && !mDimmed) { 328 // We're actually dimmed, but our content isn't dimmable, let's ensure we have a ripple 329 super.onTouchEvent(event); 330 } 331 return mDoubleTapHelper.onTouchEvent(event, getActualHeight()); 332 } 333 334 @Override 335 public boolean performClick() { 336 if (!mNeedsDimming || isTouchExplorationEnabled()) { 337 return super.performClick(); 338 } 339 return false; 340 } 341 342 private void makeActive() { 343 mFalsingManager.onNotificationActive(); 344 startActivateAnimation(false /* reverse */); 345 mActivated = true; 346 if (mOnActivatedListener != null) { 347 mOnActivatedListener.onActivated(this); 348 } 349 } 350 351 private void startActivateAnimation(final boolean reverse) { 352 if (!isAttachedToWindow()) { 353 return; 354 } 355 if (!isDimmable()) { 356 return; 357 } 358 int widthHalf = mBackgroundNormal.getWidth()/2; 359 int heightHalf = mBackgroundNormal.getActualHeight()/2; 360 float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf); 361 Animator animator; 362 if (reverse) { 363 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 364 widthHalf, heightHalf, radius, 0); 365 } else { 366 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 367 widthHalf, heightHalf, 0, radius); 368 } 369 mBackgroundNormal.setVisibility(View.VISIBLE); 370 Interpolator interpolator; 371 Interpolator alphaInterpolator; 372 if (!reverse) { 373 interpolator = Interpolators.LINEAR_OUT_SLOW_IN; 374 alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; 375 } else { 376 interpolator = ACTIVATE_INVERSE_INTERPOLATOR; 377 alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR; 378 } 379 animator.setInterpolator(interpolator); 380 animator.setDuration(ACTIVATE_ANIMATION_LENGTH); 381 if (reverse) { 382 mBackgroundNormal.setAlpha(1f); 383 animator.addListener(new AnimatorListenerAdapter() { 384 @Override 385 public void onAnimationEnd(Animator animation) { 386 updateBackground(); 387 } 388 }); 389 animator.start(); 390 } else { 391 mBackgroundNormal.setAlpha(0.4f); 392 animator.start(); 393 } 394 mBackgroundNormal.animate() 395 .alpha(reverse ? 0f : 1f) 396 .setInterpolator(alphaInterpolator) 397 .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 398 @Override 399 public void onAnimationUpdate(ValueAnimator animation) { 400 float animatedFraction = animation.getAnimatedFraction(); 401 if (reverse) { 402 animatedFraction = 1.0f - animatedFraction; 403 } 404 setNormalBackgroundVisibilityAmount(animatedFraction); 405 } 406 }) 407 .setDuration(ACTIVATE_ANIMATION_LENGTH); 408 } 409 410 /** 411 * Cancels the hotspot and makes the notification inactive. 412 */ 413 public void makeInactive(boolean animate) { 414 if (mActivated) { 415 mActivated = false; 416 if (mDimmed) { 417 if (animate) { 418 startActivateAnimation(true /* reverse */); 419 } else { 420 updateBackground(); 421 } 422 } 423 } 424 if (mOnActivatedListener != null) { 425 mOnActivatedListener.onActivationReset(this); 426 } 427 removeCallbacks(mTapTimeoutRunnable); 428 } 429 430 public void setDimmed(boolean dimmed, boolean fade) { 431 mNeedsDimming = dimmed; 432 dimmed &= isDimmable(); 433 if (mDimmed != dimmed) { 434 mDimmed = dimmed; 435 resetBackgroundAlpha(); 436 if (fade) { 437 fadeDimmedBackground(); 438 } else { 439 updateBackground(); 440 } 441 } 442 } 443 444 public boolean isDimmable() { 445 return true; 446 } 447 448 public void setDark(boolean dark, boolean fade, long delay) { 449 super.setDark(dark, fade, delay); 450 if (mDark == dark) { 451 return; 452 } 453 mDark = dark; 454 updateBackground(); 455 updateBackgroundTint(false); 456 if (!dark && fade && !shouldHideBackground()) { 457 fadeInFromDark(delay); 458 } 459 updateOutlineAlpha(); 460 } 461 462 private void updateOutlineAlpha() { 463 if (mDark) { 464 setOutlineAlpha(0f); 465 return; 466 } 467 float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED; 468 alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount); 469 alpha *= mShadowAlpha; 470 if (mFadeInFromDarkAnimator != null) { 471 alpha *= mFadeInFromDarkAnimator.getAnimatedFraction(); 472 } 473 setOutlineAlpha(alpha); 474 } 475 476 public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) { 477 mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount; 478 updateOutlineAlpha(); 479 } 480 481 @Override 482 public void setBelowSpeedBump(boolean below) { 483 super.setBelowSpeedBump(below); 484 if (below != mIsBelowSpeedBump) { 485 mIsBelowSpeedBump = below; 486 updateBackgroundTint(); 487 onBelowSpeedBumpChanged(); 488 } 489 } 490 491 protected void onBelowSpeedBumpChanged() { 492 } 493 494 /** 495 * @return whether we are below the speed bump 496 */ 497 public boolean isBelowSpeedBump() { 498 return mIsBelowSpeedBump; 499 } 500 501 /** 502 * Sets the tint color of the background 503 */ 504 public void setTintColor(int color) { 505 setTintColor(color, false); 506 } 507 508 /** 509 * Sets the tint color of the background 510 */ 511 public void setTintColor(int color, boolean animated) { 512 if (color != mBgTint) { 513 mBgTint = color; 514 updateBackgroundTint(animated); 515 } 516 } 517 518 @Override 519 public void setDistanceToTopRoundness(float distanceToTopRoundness) { 520 super.setDistanceToTopRoundness(distanceToTopRoundness); 521 mBackgroundNormal.setDistanceToTopRoundness(distanceToTopRoundness); 522 mBackgroundDimmed.setDistanceToTopRoundness(distanceToTopRoundness); 523 } 524 525 /** 526 * Set an override tint color that is used for the background. 527 * 528 * @param color the color that should be used to tint the background. 529 * This can be {@link #NO_COLOR} if the tint should be normally computed. 530 * @param overrideAmount a value from 0 to 1 how much the override tint should be used. The 531 * background color will then be the interpolation between this and the 532 * regular background color, where 1 means the overrideTintColor is fully 533 * used and the background color not at all. 534 */ 535 public void setOverrideTintColor(int color, float overrideAmount) { 536 if (mDark) { 537 color = NO_COLOR; 538 overrideAmount = 0; 539 } 540 mOverrideTint = color; 541 mOverrideAmount = overrideAmount; 542 int newColor = calculateBgColor(); 543 setBackgroundTintColor(newColor); 544 if (!isDimmable() && mNeedsDimming) { 545 mBackgroundNormal.setDrawableAlpha((int) NotificationUtils.interpolate(255, 546 mDimmedAlpha, 547 overrideAmount)); 548 } else { 549 mBackgroundNormal.setDrawableAlpha(255); 550 } 551 } 552 553 protected void updateBackgroundTint() { 554 updateBackgroundTint(false /* animated */); 555 } 556 557 private void updateBackgroundTint(boolean animated) { 558 if (mBackgroundColorAnimator != null) { 559 mBackgroundColorAnimator.cancel(); 560 } 561 int rippleColor = getRippleColor(); 562 mBackgroundDimmed.setRippleColor(rippleColor); 563 mBackgroundNormal.setRippleColor(rippleColor); 564 int color = calculateBgColor(); 565 if (!animated) { 566 setBackgroundTintColor(color); 567 } else if (color != mCurrentBackgroundTint) { 568 mStartTint = mCurrentBackgroundTint; 569 mTargetTint = color; 570 mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); 571 mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 572 @Override 573 public void onAnimationUpdate(ValueAnimator animation) { 574 int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint, 575 animation.getAnimatedFraction()); 576 setBackgroundTintColor(newColor); 577 } 578 }); 579 mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 580 mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR); 581 mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() { 582 @Override 583 public void onAnimationEnd(Animator animation) { 584 mBackgroundColorAnimator = null; 585 } 586 }); 587 mBackgroundColorAnimator.start(); 588 } 589 } 590 591 protected void setBackgroundTintColor(int color) { 592 if (color != mCurrentBackgroundTint) { 593 mCurrentBackgroundTint = color; 594 if (color == mNormalColor) { 595 // We don't need to tint a normal notification 596 color = 0; 597 } 598 mBackgroundDimmed.setTint(color); 599 mBackgroundNormal.setTint(color); 600 } 601 } 602 603 /** 604 * Fades in the background when exiting dark mode. 605 */ 606 private void fadeInFromDark(long delay) { 607 final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal; 608 background.setAlpha(0f); 609 mBackgroundVisibilityUpdater.onAnimationUpdate(null); 610 background.animate() 611 .alpha(1f) 612 .setDuration(DARK_ANIMATION_LENGTH) 613 .setStartDelay(delay) 614 .setInterpolator(Interpolators.ALPHA_IN) 615 .setListener(new AnimatorListenerAdapter() { 616 @Override 617 public void onAnimationCancel(Animator animation) { 618 // Jump state if we are cancelled 619 background.setAlpha(1f); 620 } 621 }) 622 .setUpdateListener(mBackgroundVisibilityUpdater) 623 .start(); 624 mFadeInFromDarkAnimator = TimeAnimator.ofFloat(0.0f, 1.0f); 625 mFadeInFromDarkAnimator.setDuration(DARK_ANIMATION_LENGTH); 626 mFadeInFromDarkAnimator.setStartDelay(delay); 627 mFadeInFromDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 628 mFadeInFromDarkAnimator.addListener(mFadeInEndListener); 629 mFadeInFromDarkAnimator.addUpdateListener(mUpdateOutlineListener); 630 mFadeInFromDarkAnimator.start(); 631 } 632 633 /** 634 * Fades the background when the dimmed state changes. 635 */ 636 private void fadeDimmedBackground() { 637 mBackgroundDimmed.animate().cancel(); 638 mBackgroundNormal.animate().cancel(); 639 if (mActivated) { 640 updateBackground(); 641 return; 642 } 643 if (!shouldHideBackground()) { 644 if (mDimmed) { 645 mBackgroundDimmed.setVisibility(View.VISIBLE); 646 } else { 647 mBackgroundNormal.setVisibility(View.VISIBLE); 648 } 649 } 650 float startAlpha = mDimmed ? 1f : 0; 651 float endAlpha = mDimmed ? 0 : 1f; 652 int duration = BACKGROUND_ANIMATION_LENGTH_MS; 653 // Check whether there is already a background animation running. 654 if (mBackgroundAnimator != null) { 655 startAlpha = (Float) mBackgroundAnimator.getAnimatedValue(); 656 duration = (int) mBackgroundAnimator.getCurrentPlayTime(); 657 mBackgroundAnimator.removeAllListeners(); 658 mBackgroundAnimator.cancel(); 659 if (duration <= 0) { 660 updateBackground(); 661 return; 662 } 663 } 664 mBackgroundNormal.setAlpha(startAlpha); 665 mBackgroundAnimator = 666 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha); 667 mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 668 mBackgroundAnimator.setDuration(duration); 669 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 670 @Override 671 public void onAnimationEnd(Animator animation) { 672 updateBackground(); 673 mBackgroundAnimator = null; 674 if (mFadeInFromDarkAnimator == null) { 675 mDimmedBackgroundFadeInAmount = -1; 676 } 677 } 678 }); 679 mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater); 680 mBackgroundAnimator.start(); 681 } 682 683 protected void updateBackgroundAlpha(float transformationAmount) { 684 mBgAlpha = isChildInGroup() && mDimmed ? transformationAmount : 1f; 685 if (mDimmedBackgroundFadeInAmount != -1) { 686 mBgAlpha *= mDimmedBackgroundFadeInAmount; 687 } 688 mBackgroundDimmed.setAlpha(mBgAlpha); 689 } 690 691 protected void resetBackgroundAlpha() { 692 updateBackgroundAlpha(0f /* transformationAmount */); 693 } 694 695 protected void updateBackground() { 696 cancelFadeAnimations(); 697 if (shouldHideBackground()) { 698 mBackgroundDimmed.setVisibility(INVISIBLE); 699 mBackgroundNormal.setVisibility(mActivated ? VISIBLE : INVISIBLE); 700 } else if (mDimmed) { 701 // When groups are animating to the expanded state from the lockscreen, show the 702 // normal background instead of the dimmed background 703 final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup(); 704 mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE); 705 mBackgroundNormal.setVisibility((mActivated || dontShowDimmed) 706 ? View.VISIBLE 707 : View.INVISIBLE); 708 } else { 709 mBackgroundDimmed.setVisibility(View.INVISIBLE); 710 mBackgroundNormal.setVisibility(View.VISIBLE); 711 mBackgroundNormal.setAlpha(1f); 712 removeCallbacks(mTapTimeoutRunnable); 713 // make in inactive to avoid it sticking around active 714 makeInactive(false /* animate */); 715 } 716 setNormalBackgroundVisibilityAmount( 717 mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f); 718 } 719 720 protected void updateBackgroundClipping() { 721 mBackgroundNormal.setBottomAmountClips(!isChildInGroup()); 722 mBackgroundDimmed.setBottomAmountClips(!isChildInGroup()); 723 } 724 725 protected boolean shouldHideBackground() { 726 return mDark; 727 } 728 729 private void cancelFadeAnimations() { 730 if (mBackgroundAnimator != null) { 731 mBackgroundAnimator.cancel(); 732 } 733 mBackgroundDimmed.animate().cancel(); 734 mBackgroundNormal.animate().cancel(); 735 } 736 737 @Override 738 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 739 super.onLayout(changed, left, top, right, bottom); 740 setPivotX(getWidth() / 2); 741 } 742 743 @Override 744 public void setActualHeight(int actualHeight, boolean notifyListeners) { 745 super.setActualHeight(actualHeight, notifyListeners); 746 setPivotY(actualHeight / 2); 747 mBackgroundNormal.setActualHeight(actualHeight); 748 mBackgroundDimmed.setActualHeight(actualHeight); 749 } 750 751 @Override 752 public void setClipTopAmount(int clipTopAmount) { 753 super.setClipTopAmount(clipTopAmount); 754 mBackgroundNormal.setClipTopAmount(clipTopAmount); 755 mBackgroundDimmed.setClipTopAmount(clipTopAmount); 756 } 757 758 @Override 759 public void setClipBottomAmount(int clipBottomAmount) { 760 super.setClipBottomAmount(clipBottomAmount); 761 mBackgroundNormal.setClipBottomAmount(clipBottomAmount); 762 mBackgroundDimmed.setClipBottomAmount(clipBottomAmount); 763 } 764 765 @Override 766 public void performRemoveAnimation(long duration, long delay, 767 float translationDirection, boolean isHeadsUpAnimation, float endLocation, 768 Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener) { 769 enableAppearDrawing(true); 770 mIsHeadsUpAnimation = isHeadsUpAnimation; 771 mHeadsUpLocation = endLocation; 772 if (mDrawingAppearAnimation) { 773 startAppearAnimation(false /* isAppearing */, translationDirection, 774 delay, duration, onFinishedRunnable, animationListener); 775 } else if (onFinishedRunnable != null) { 776 onFinishedRunnable.run(); 777 } 778 } 779 780 @Override 781 public void performAddAnimation(long delay, long duration, boolean isHeadsUpAppear) { 782 enableAppearDrawing(true); 783 mIsHeadsUpAnimation = isHeadsUpAppear; 784 mHeadsUpLocation = mHeadsUpAddStartLocation; 785 if (mDrawingAppearAnimation) { 786 startAppearAnimation(true /* isAppearing */, isHeadsUpAppear ? 0.0f : -1.0f, delay, 787 duration, null, null); 788 } 789 } 790 791 private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, 792 long duration, final Runnable onFinishedRunnable, 793 AnimatorListenerAdapter animationListener) { 794 cancelAppearAnimation(); 795 mAnimationTranslationY = translationDirection * getActualHeight(); 796 if (mAppearAnimationFraction == -1.0f) { 797 // not initialized yet, we start anew 798 if (isAppearing) { 799 mAppearAnimationFraction = 0.0f; 800 mAppearAnimationTranslation = mAnimationTranslationY; 801 } else { 802 mAppearAnimationFraction = 1.0f; 803 mAppearAnimationTranslation = 0; 804 } 805 } 806 mIsAppearing = isAppearing; 807 808 float targetValue; 809 if (isAppearing) { 810 mCurrentAppearInterpolator = mSlowOutFastInInterpolator; 811 mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; 812 targetValue = 1.0f; 813 } else { 814 mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN; 815 mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator; 816 targetValue = 0.0f; 817 } 818 mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, 819 targetValue); 820 mAppearAnimator.setInterpolator(Interpolators.LINEAR); 821 mAppearAnimator.setDuration( 822 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); 823 mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 824 @Override 825 public void onAnimationUpdate(ValueAnimator animation) { 826 mAppearAnimationFraction = (float) animation.getAnimatedValue(); 827 updateAppearAnimationAlpha(); 828 updateAppearRect(); 829 invalidate(); 830 } 831 }); 832 if (animationListener != null) { 833 mAppearAnimator.addListener(animationListener); 834 } 835 if (delay > 0) { 836 // we need to apply the initial state already to avoid drawn frames in the wrong state 837 updateAppearAnimationAlpha(); 838 updateAppearRect(); 839 mAppearAnimator.setStartDelay(delay); 840 } 841 mAppearAnimator.addListener(new AnimatorListenerAdapter() { 842 private boolean mWasCancelled; 843 844 @Override 845 public void onAnimationEnd(Animator animation) { 846 if (onFinishedRunnable != null) { 847 onFinishedRunnable.run(); 848 } 849 if (!mWasCancelled) { 850 enableAppearDrawing(false); 851 onAppearAnimationFinished(isAppearing); 852 } 853 } 854 855 @Override 856 public void onAnimationStart(Animator animation) { 857 mWasCancelled = false; 858 } 859 860 @Override 861 public void onAnimationCancel(Animator animation) { 862 mWasCancelled = true; 863 } 864 }); 865 mAppearAnimator.start(); 866 } 867 868 protected void onAppearAnimationFinished(boolean wasAppearing) { 869 } 870 871 private void cancelAppearAnimation() { 872 if (mAppearAnimator != null) { 873 mAppearAnimator.cancel(); 874 mAppearAnimator = null; 875 } 876 } 877 878 public void cancelAppearDrawing() { 879 cancelAppearAnimation(); 880 enableAppearDrawing(false); 881 } 882 883 private void updateAppearRect() { 884 float inverseFraction = (1.0f - mAppearAnimationFraction); 885 float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction); 886 float translateYTotalAmount = translationFraction * mAnimationTranslationY; 887 mAppearAnimationTranslation = translateYTotalAmount; 888 889 // handle width animation 890 float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START)) 891 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END); 892 widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction)); 893 widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction); 894 float startWidthFraction = HORIZONTAL_COLLAPSED_REST_PARTIAL; 895 if (mIsHeadsUpAnimation && !mIsAppearing) { 896 startWidthFraction = 0; 897 } 898 float width = MathUtils.lerp(startWidthFraction, 1.0f, 1.0f - widthFraction) 899 * getWidth(); 900 float left; 901 float right; 902 if (mIsHeadsUpAnimation) { 903 left = MathUtils.lerp(mHeadsUpLocation, 0, 1.0f - widthFraction); 904 right = left + width; 905 } else { 906 left = getWidth() * 0.5f - width / 2.0f; 907 right = getWidth() - left; 908 } 909 910 // handle top animation 911 float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) / 912 VERTICAL_ANIMATION_START; 913 heightFraction = Math.max(0.0f, heightFraction); 914 heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction); 915 916 float top; 917 float bottom; 918 final int actualHeight = getActualHeight(); 919 if (mAnimationTranslationY > 0.0f) { 920 bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f 921 - translateYTotalAmount; 922 top = bottom * heightFraction; 923 } else { 924 top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f - 925 translateYTotalAmount; 926 bottom = actualHeight * (1 - heightFraction) + top * heightFraction; 927 } 928 mAppearAnimationRect.set(left, top, right, bottom); 929 setOutlineRect(left, top + mAppearAnimationTranslation, right, 930 bottom + mAppearAnimationTranslation); 931 } 932 933 private void updateAppearAnimationAlpha() { 934 float contentAlphaProgress = mAppearAnimationFraction; 935 contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END); 936 contentAlphaProgress = Math.min(1.0f, contentAlphaProgress); 937 contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress); 938 setContentAlpha(contentAlphaProgress); 939 } 940 941 private void setContentAlpha(float contentAlpha) { 942 View contentView = getContentView(); 943 if (contentView.hasOverlappingRendering()) { 944 int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE 945 : LAYER_TYPE_HARDWARE; 946 int currentLayerType = contentView.getLayerType(); 947 if (currentLayerType != layerType) { 948 contentView.setLayerType(layerType, null); 949 } 950 } 951 contentView.setAlpha(contentAlpha); 952 } 953 954 @Override 955 protected void applyRoundness() { 956 super.applyRoundness(); 957 applyBackgroundRoundness(getCurrentBackgroundRadiusTop(), 958 getCurrentBackgroundRadiusBottom()); 959 } 960 961 protected void applyBackgroundRoundness(float topRadius, float bottomRadius) { 962 mBackgroundDimmed.setRoundness(topRadius, bottomRadius); 963 mBackgroundNormal.setRoundness(topRadius, bottomRadius); 964 } 965 966 @Override 967 protected void setBackgroundTop(int backgroundTop) { 968 mBackgroundDimmed.setBackgroundTop(backgroundTop); 969 mBackgroundNormal.setBackgroundTop(backgroundTop); 970 } 971 972 protected abstract View getContentView(); 973 974 public int calculateBgColor() { 975 return calculateBgColor(true /* withTint */, true /* withOverRide */); 976 } 977 978 @Override 979 protected boolean childNeedsClipping(View child) { 980 if (child instanceof NotificationBackgroundView && isClippingNeeded()) { 981 return true; 982 } 983 return super.childNeedsClipping(child); 984 } 985 986 /** 987 * @param withTint should a possible tint be factored in? 988 * @param withOverRide should the value be interpolated with {@link #mOverrideTint} 989 * @return the calculated background color 990 */ 991 private int calculateBgColor(boolean withTint, boolean withOverRide) { 992 if (withTint && mDark) { 993 return getContext().getColor(R.color.notification_material_background_dark_color); 994 } 995 if (withOverRide && mOverrideTint != NO_COLOR) { 996 int defaultTint = calculateBgColor(withTint, false); 997 return NotificationUtils.interpolateColors(defaultTint, mOverrideTint, mOverrideAmount); 998 } 999 if (withTint && mBgTint != NO_COLOR) { 1000 return mBgTint; 1001 } else { 1002 return mNormalColor; 1003 } 1004 } 1005 1006 protected int getRippleColor() { 1007 if (mBgTint != 0) { 1008 return mTintedRippleColor; 1009 } else { 1010 return mNormalRippleColor; 1011 } 1012 } 1013 1014 /** 1015 * When we draw the appear animation, we render the view in a bitmap and render this bitmap 1016 * as a shader of a rect. This call creates the Bitmap and switches the drawing mode, 1017 * such that the normal drawing of the views does not happen anymore. 1018 * 1019 * @param enable Should it be enabled. 1020 */ 1021 private void enableAppearDrawing(boolean enable) { 1022 if (enable != mDrawingAppearAnimation) { 1023 mDrawingAppearAnimation = enable; 1024 if (!enable) { 1025 setContentAlpha(1.0f); 1026 mAppearAnimationFraction = -1; 1027 setOutlineRect(null); 1028 } 1029 invalidate(); 1030 } 1031 } 1032 1033 public boolean isDrawingAppearAnimation() { 1034 return mDrawingAppearAnimation; 1035 } 1036 1037 @Override 1038 protected void dispatchDraw(Canvas canvas) { 1039 if (mDrawingAppearAnimation) { 1040 canvas.save(); 1041 canvas.translate(0, mAppearAnimationTranslation); 1042 } 1043 super.dispatchDraw(canvas); 1044 if (mDrawingAppearAnimation) { 1045 canvas.restore(); 1046 } 1047 } 1048 1049 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 1050 mOnActivatedListener = onActivatedListener; 1051 } 1052 1053 public boolean hasSameBgColor(ActivatableNotificationView otherView) { 1054 return calculateBgColor() == otherView.calculateBgColor(); 1055 } 1056 1057 @Override 1058 public float getShadowAlpha() { 1059 return mShadowAlpha; 1060 } 1061 1062 @Override 1063 public void setShadowAlpha(float shadowAlpha) { 1064 if (shadowAlpha != mShadowAlpha) { 1065 mShadowAlpha = shadowAlpha; 1066 updateOutlineAlpha(); 1067 } 1068 } 1069 1070 @Override 1071 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 1072 int outlineTranslation) { 1073 boolean hiddenBefore = mShadowHidden; 1074 mShadowHidden = shadowIntensity == 0.0f; 1075 if (!mShadowHidden || !hiddenBefore) { 1076 mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ() 1077 + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd, 1078 outlineTranslation); 1079 } 1080 } 1081 1082 public int getBackgroundColorWithoutTint() { 1083 return calculateBgColor(false /* withTint */, false /* withOverride */); 1084 } 1085 1086 public boolean isPinned() { 1087 return false; 1088 } 1089 1090 public boolean isHeadsUpAnimatingAway() { 1091 return false; 1092 } 1093 1094 public interface OnActivatedListener { 1095 void onActivated(ActivatableNotificationView view); 1096 void onActivationReset(ActivatableNotificationView view); 1097 } 1098 } 1099