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