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