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