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