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