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 ActivatableNotificationView(Context context, AttributeSet attrs)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 initDimens()209 private void initDimens() { 210 mHeadsUpAddStartLocation = getResources().getDimensionPixelSize( 211 com.android.internal.R.dimen.notification_content_margin_start); 212 } 213 214 @Override onDensityOrFontScaleChanged()215 public void onDensityOrFontScaleChanged() { 216 super.onDensityOrFontScaleChanged(); 217 initDimens(); 218 } 219 220 @Override onFinishInflate()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 */ initBackground()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 onInterceptTouchEvent(MotionEvent ev)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 isTouchExplorationEnabled()267 private boolean isTouchExplorationEnabled() { 268 return mAccessibilityManager.isTouchExplorationEnabled(); 269 } 270 disallowSingleClick(MotionEvent ev)271 protected boolean disallowSingleClick(MotionEvent ev) { 272 return false; 273 } 274 handleSlideBack()275 protected boolean handleSlideBack() { 276 return false; 277 } 278 279 @Override onTouchEvent(MotionEvent event)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 */ isInteractive()301 protected boolean isInteractive() { 302 return true; 303 } 304 305 @Override drawableHotspotChanged(float x, float y)306 public void drawableHotspotChanged(float x, float y) { 307 if (!mDimmed){ 308 mBackgroundNormal.drawableHotspotChanged(x, y); 309 } 310 } 311 312 @Override drawableStateChanged()313 protected void drawableStateChanged() { 314 super.drawableStateChanged(); 315 if (mDimmed) { 316 mBackgroundDimmed.setState(getDrawableState()); 317 } else { 318 mBackgroundNormal.setState(getDrawableState()); 319 } 320 } 321 setRippleAllowed(boolean allowed)322 public void setRippleAllowed(boolean allowed) { 323 mBackgroundNormal.setPressedAllowed(allowed); 324 } 325 handleTouchEventDimmed(MotionEvent event)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 performClick()335 public boolean performClick() { 336 if (!mNeedsDimming || isTouchExplorationEnabled()) { 337 return super.performClick(); 338 } 339 return false; 340 } 341 makeActive()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 startActivateAnimation(final boolean reverse)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 */ makeInactive(boolean animate)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 setDimmed(boolean dimmed, boolean fade)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 isDimmable()444 public boolean isDimmable() { 445 return true; 446 } 447 setDark(boolean dark, boolean fade, long delay)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 updateOutlineAlpha()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 setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount)476 public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) { 477 mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount; 478 updateOutlineAlpha(); 479 } 480 481 @Override setBelowSpeedBump(boolean below)482 public void setBelowSpeedBump(boolean below) { 483 super.setBelowSpeedBump(below); 484 if (below != mIsBelowSpeedBump) { 485 mIsBelowSpeedBump = below; 486 updateBackgroundTint(); 487 onBelowSpeedBumpChanged(); 488 } 489 } 490 onBelowSpeedBumpChanged()491 protected void onBelowSpeedBumpChanged() { 492 } 493 494 /** 495 * @return whether we are below the speed bump 496 */ isBelowSpeedBump()497 public boolean isBelowSpeedBump() { 498 return mIsBelowSpeedBump; 499 } 500 501 /** 502 * Sets the tint color of the background 503 */ setTintColor(int color)504 public void setTintColor(int color) { 505 setTintColor(color, false); 506 } 507 508 /** 509 * Sets the tint color of the background 510 */ setTintColor(int color, boolean animated)511 public void setTintColor(int color, boolean animated) { 512 if (color != mBgTint) { 513 mBgTint = color; 514 updateBackgroundTint(animated); 515 } 516 } 517 518 @Override setDistanceToTopRoundness(float distanceToTopRoundness)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 */ setOverrideTintColor(int color, float overrideAmount)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 updateBackgroundTint()553 protected void updateBackgroundTint() { 554 updateBackgroundTint(false /* animated */); 555 } 556 updateBackgroundTint(boolean animated)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 setBackgroundTintColor(int color)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 */ fadeInFromDark(long delay)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 */ fadeDimmedBackground()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 updateBackgroundAlpha(float transformationAmount)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 resetBackgroundAlpha()691 protected void resetBackgroundAlpha() { 692 updateBackgroundAlpha(0f /* transformationAmount */); 693 } 694 updateBackground()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 updateBackgroundClipping()720 protected void updateBackgroundClipping() { 721 mBackgroundNormal.setBottomAmountClips(!isChildInGroup()); 722 mBackgroundDimmed.setBottomAmountClips(!isChildInGroup()); 723 } 724 shouldHideBackground()725 protected boolean shouldHideBackground() { 726 return mDark; 727 } 728 cancelFadeAnimations()729 private void cancelFadeAnimations() { 730 if (mBackgroundAnimator != null) { 731 mBackgroundAnimator.cancel(); 732 } 733 mBackgroundDimmed.animate().cancel(); 734 mBackgroundNormal.animate().cancel(); 735 } 736 737 @Override onLayout(boolean changed, int left, int top, int right, int bottom)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 setActualHeight(int actualHeight, boolean notifyListeners)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 setClipTopAmount(int clipTopAmount)752 public void setClipTopAmount(int clipTopAmount) { 753 super.setClipTopAmount(clipTopAmount); 754 mBackgroundNormal.setClipTopAmount(clipTopAmount); 755 mBackgroundDimmed.setClipTopAmount(clipTopAmount); 756 } 757 758 @Override setClipBottomAmount(int clipBottomAmount)759 public void setClipBottomAmount(int clipBottomAmount) { 760 super.setClipBottomAmount(clipBottomAmount); 761 mBackgroundNormal.setClipBottomAmount(clipBottomAmount); 762 mBackgroundDimmed.setClipBottomAmount(clipBottomAmount); 763 } 764 765 @Override performRemoveAnimation(long duration, long delay, float translationDirection, boolean isHeadsUpAnimation, float endLocation, Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)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 performAddAnimation(long delay, long duration, boolean isHeadsUpAppear)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 startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable, AnimatorListenerAdapter animationListener)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 onAppearAnimationFinished(boolean wasAppearing)868 protected void onAppearAnimationFinished(boolean wasAppearing) { 869 } 870 cancelAppearAnimation()871 private void cancelAppearAnimation() { 872 if (mAppearAnimator != null) { 873 mAppearAnimator.cancel(); 874 mAppearAnimator = null; 875 } 876 } 877 cancelAppearDrawing()878 public void cancelAppearDrawing() { 879 cancelAppearAnimation(); 880 enableAppearDrawing(false); 881 } 882 updateAppearRect()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 updateAppearAnimationAlpha()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 setContentAlpha(float contentAlpha)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 applyRoundness()955 protected void applyRoundness() { 956 super.applyRoundness(); 957 applyBackgroundRoundness(getCurrentBackgroundRadiusTop(), 958 getCurrentBackgroundRadiusBottom()); 959 } 960 applyBackgroundRoundness(float topRadius, float bottomRadius)961 protected void applyBackgroundRoundness(float topRadius, float bottomRadius) { 962 mBackgroundDimmed.setRoundness(topRadius, bottomRadius); 963 mBackgroundNormal.setRoundness(topRadius, bottomRadius); 964 } 965 966 @Override setBackgroundTop(int backgroundTop)967 protected void setBackgroundTop(int backgroundTop) { 968 mBackgroundDimmed.setBackgroundTop(backgroundTop); 969 mBackgroundNormal.setBackgroundTop(backgroundTop); 970 } 971 getContentView()972 protected abstract View getContentView(); 973 calculateBgColor()974 public int calculateBgColor() { 975 return calculateBgColor(true /* withTint */, true /* withOverRide */); 976 } 977 978 @Override childNeedsClipping(View child)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 */ calculateBgColor(boolean withTint, boolean withOverRide)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 getRippleColor()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 */ enableAppearDrawing(boolean enable)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 isDrawingAppearAnimation()1033 public boolean isDrawingAppearAnimation() { 1034 return mDrawingAppearAnimation; 1035 } 1036 1037 @Override dispatchDraw(Canvas canvas)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 setOnActivatedListener(OnActivatedListener onActivatedListener)1049 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 1050 mOnActivatedListener = onActivatedListener; 1051 } 1052 hasSameBgColor(ActivatableNotificationView otherView)1053 public boolean hasSameBgColor(ActivatableNotificationView otherView) { 1054 return calculateBgColor() == otherView.calculateBgColor(); 1055 } 1056 1057 @Override getShadowAlpha()1058 public float getShadowAlpha() { 1059 return mShadowAlpha; 1060 } 1061 1062 @Override setShadowAlpha(float shadowAlpha)1063 public void setShadowAlpha(float shadowAlpha) { 1064 if (shadowAlpha != mShadowAlpha) { 1065 mShadowAlpha = shadowAlpha; 1066 updateOutlineAlpha(); 1067 } 1068 } 1069 1070 @Override setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, int outlineTranslation)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 getBackgroundColorWithoutTint()1082 public int getBackgroundColorWithoutTint() { 1083 return calculateBgColor(false /* withTint */, false /* withOverride */); 1084 } 1085 isPinned()1086 public boolean isPinned() { 1087 return false; 1088 } 1089 isHeadsUpAnimatingAway()1090 public boolean isHeadsUpAnimatingAway() { 1091 return false; 1092 } 1093 1094 public interface OnActivatedListener { onActivated(ActivatableNotificationView view)1095 void onActivated(ActivatableNotificationView view); onActivationReset(ActivatableNotificationView view)1096 void onActivationReset(ActivatableNotificationView view); 1097 } 1098 } 1099