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.RectF; 27 import android.util.AttributeSet; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.ViewAnimationUtils; 31 import android.view.ViewConfiguration; 32 import android.view.animation.Interpolator; 33 import android.view.animation.PathInterpolator; 34 35 import com.android.systemui.Interpolators; 36 import com.android.systemui.R; 37 import com.android.systemui.classifier.FalsingManager; 38 import com.android.systemui.statusbar.notification.FakeShadowView; 39 import com.android.systemui.statusbar.notification.NotificationUtils; 40 import com.android.systemui.statusbar.stack.NotificationStackScrollLayout; 41 import com.android.systemui.statusbar.stack.StackStateAnimator; 42 43 /** 44 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer} 45 * to implement dimming/activating on Keyguard for the double-tap gesture 46 */ 47 public abstract class ActivatableNotificationView extends ExpandableOutlineView { 48 49 private static final long DOUBLETAP_TIMEOUT_MS = 1200; 50 private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220; 51 private static final int ACTIVATE_ANIMATION_LENGTH = 220; 52 private static final int DARK_ANIMATION_LENGTH = 170; 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 * Scale for the background to animate from when exiting dark mode. 86 */ 87 private static final float DARK_EXIT_SCALE_START = 0.93f; 88 89 private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR 90 = new PathInterpolator(0.6f, 0, 0.5f, 1); 91 private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR 92 = new PathInterpolator(0, 0, 0.5f, 1); 93 private final int mTintedRippleColor; 94 private final int mLowPriorityRippleColor; 95 protected final int mNormalRippleColor; 96 97 private boolean mDimmed; 98 private boolean mDark; 99 100 private int mBgTint = 0; 101 private float mBgAlpha = 1f; 102 103 /** 104 * Flag to indicate that the notification has been touched once and the second touch will 105 * click it. 106 */ 107 private boolean mActivated; 108 109 private float mDownX; 110 private float mDownY; 111 private final float mTouchSlop; 112 113 private OnActivatedListener mOnActivatedListener; 114 115 private final Interpolator mSlowOutFastInInterpolator; 116 private final Interpolator mSlowOutLinearInInterpolator; 117 private Interpolator mCurrentAppearInterpolator; 118 private Interpolator mCurrentAlphaInterpolator; 119 120 private NotificationBackgroundView mBackgroundNormal; 121 private NotificationBackgroundView mBackgroundDimmed; 122 private ObjectAnimator mBackgroundAnimator; 123 private RectF mAppearAnimationRect = new RectF(); 124 private float mAnimationTranslationY; 125 private boolean mDrawingAppearAnimation; 126 private ValueAnimator mAppearAnimator; 127 private ValueAnimator mBackgroundColorAnimator; 128 private float mAppearAnimationFraction = -1.0f; 129 private float mAppearAnimationTranslation; 130 private boolean mShowingLegacyBackground; 131 private final int mLegacyColor; 132 private final int mNormalColor; 133 private final int mLowPriorityColor; 134 private boolean mIsBelowSpeedBump; 135 private FalsingManager mFalsingManager; 136 private boolean mTrackTouch; 137 138 private float mNormalBackgroundVisibilityAmount; 139 private ValueAnimator mFadeInFromDarkAnimator; 140 private ValueAnimator.AnimatorUpdateListener mBackgroundVisibilityUpdater 141 = new ValueAnimator.AnimatorUpdateListener() { 142 @Override 143 public void onAnimationUpdate(ValueAnimator animation) { 144 setNormalBackgroundVisibilityAmount(mBackgroundNormal.getAlpha()); 145 } 146 }; 147 private AnimatorListenerAdapter mFadeInEndListener = new AnimatorListenerAdapter() { 148 @Override 149 public void onAnimationEnd(Animator animation) { 150 super.onAnimationEnd(animation); 151 mFadeInFromDarkAnimator = null; 152 updateBackground(); 153 } 154 }; 155 private ValueAnimator.AnimatorUpdateListener mUpdateOutlineListener 156 = new ValueAnimator.AnimatorUpdateListener() { 157 @Override 158 public void onAnimationUpdate(ValueAnimator animation) { 159 updateOutlineAlpha(); 160 } 161 }; 162 private float mShadowAlpha = 1.0f; 163 private FakeShadowView mFakeShadow; 164 private int mCurrentBackgroundTint; 165 private int mTargetTint; 166 private int mStartTint; 167 168 public ActivatableNotificationView(Context context, AttributeSet attrs) { 169 super(context, attrs); 170 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 171 mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f); 172 mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f); 173 setClipChildren(false); 174 setClipToPadding(false); 175 mLegacyColor = context.getColor(R.color.notification_legacy_background_color); 176 mNormalColor = context.getColor(R.color.notification_material_background_color); 177 mLowPriorityColor = context.getColor( 178 R.color.notification_material_background_low_priority_color); 179 mTintedRippleColor = context.getColor( 180 R.color.notification_ripple_tinted_color); 181 mLowPriorityRippleColor = context.getColor( 182 R.color.notification_ripple_color_low_priority); 183 mNormalRippleColor = context.getColor( 184 R.color.notification_ripple_untinted_color); 185 mFalsingManager = FalsingManager.getInstance(context); 186 } 187 188 @Override 189 protected void onFinishInflate() { 190 super.onFinishInflate(); 191 mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal); 192 mFakeShadow = (FakeShadowView) findViewById(R.id.fake_shadow); 193 mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed); 194 mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg); 195 mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim); 196 updateBackground(); 197 updateBackgroundTint(); 198 updateOutlineAlpha(); 199 } 200 201 private final Runnable mTapTimeoutRunnable = new Runnable() { 202 @Override 203 public void run() { 204 makeInactive(true /* animate */); 205 } 206 }; 207 208 @Override 209 public boolean onInterceptTouchEvent(MotionEvent ev) { 210 if (mDimmed && !mActivated 211 && ev.getActionMasked() == MotionEvent.ACTION_DOWN && disallowSingleClick(ev)) { 212 return true; 213 } 214 return super.onInterceptTouchEvent(ev); 215 } 216 217 protected boolean disallowSingleClick(MotionEvent ev) { 218 return false; 219 } 220 221 protected boolean handleSlideBack() { 222 return false; 223 } 224 225 @Override 226 public boolean onTouchEvent(MotionEvent event) { 227 boolean result; 228 if (mDimmed) { 229 boolean wasActivated = mActivated; 230 result = handleTouchEventDimmed(event); 231 if (wasActivated && result && event.getAction() == MotionEvent.ACTION_UP) { 232 mFalsingManager.onNotificationDoubleTap(); 233 removeCallbacks(mTapTimeoutRunnable); 234 } 235 } else { 236 result = super.onTouchEvent(event); 237 } 238 return result; 239 } 240 241 @Override 242 public void drawableHotspotChanged(float x, float y) { 243 if (!mDimmed){ 244 mBackgroundNormal.drawableHotspotChanged(x, y); 245 } 246 } 247 248 @Override 249 protected void drawableStateChanged() { 250 super.drawableStateChanged(); 251 if (mDimmed) { 252 mBackgroundDimmed.setState(getDrawableState()); 253 } else { 254 mBackgroundNormal.setState(getDrawableState()); 255 } 256 } 257 258 private boolean handleTouchEventDimmed(MotionEvent event) { 259 int action = event.getActionMasked(); 260 switch (action) { 261 case MotionEvent.ACTION_DOWN: 262 mDownX = event.getX(); 263 mDownY = event.getY(); 264 mTrackTouch = true; 265 if (mDownY > getActualHeight()) { 266 mTrackTouch = false; 267 } 268 break; 269 case MotionEvent.ACTION_MOVE: 270 if (!isWithinTouchSlop(event)) { 271 makeInactive(true /* animate */); 272 mTrackTouch = false; 273 } 274 break; 275 case MotionEvent.ACTION_UP: 276 if (isWithinTouchSlop(event)) { 277 if (handleSlideBack()) { 278 return true; 279 } 280 if (!mActivated) { 281 makeActive(); 282 postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS); 283 } else { 284 if (!performClick()) { 285 return false; 286 } 287 } 288 } else { 289 makeInactive(true /* animate */); 290 mTrackTouch = false; 291 } 292 break; 293 case MotionEvent.ACTION_CANCEL: 294 makeInactive(true /* animate */); 295 mTrackTouch = false; 296 break; 297 default: 298 break; 299 } 300 return mTrackTouch; 301 } 302 303 private void makeActive() { 304 mFalsingManager.onNotificationActive(); 305 startActivateAnimation(false /* reverse */); 306 mActivated = true; 307 if (mOnActivatedListener != null) { 308 mOnActivatedListener.onActivated(this); 309 } 310 } 311 312 private void startActivateAnimation(final boolean reverse) { 313 if (!isAttachedToWindow()) { 314 return; 315 } 316 int widthHalf = mBackgroundNormal.getWidth()/2; 317 int heightHalf = mBackgroundNormal.getActualHeight()/2; 318 float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf); 319 Animator animator; 320 if (reverse) { 321 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 322 widthHalf, heightHalf, radius, 0); 323 } else { 324 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 325 widthHalf, heightHalf, 0, radius); 326 } 327 mBackgroundNormal.setVisibility(View.VISIBLE); 328 Interpolator interpolator; 329 Interpolator alphaInterpolator; 330 if (!reverse) { 331 interpolator = Interpolators.LINEAR_OUT_SLOW_IN; 332 alphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; 333 } else { 334 interpolator = ACTIVATE_INVERSE_INTERPOLATOR; 335 alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR; 336 } 337 animator.setInterpolator(interpolator); 338 animator.setDuration(ACTIVATE_ANIMATION_LENGTH); 339 if (reverse) { 340 mBackgroundNormal.setAlpha(1f); 341 animator.addListener(new AnimatorListenerAdapter() { 342 @Override 343 public void onAnimationEnd(Animator animation) { 344 updateBackground(); 345 } 346 }); 347 animator.start(); 348 } else { 349 mBackgroundNormal.setAlpha(0.4f); 350 animator.start(); 351 } 352 mBackgroundNormal.animate() 353 .alpha(reverse ? 0f : 1f) 354 .setInterpolator(alphaInterpolator) 355 .setUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 356 @Override 357 public void onAnimationUpdate(ValueAnimator animation) { 358 float animatedFraction = animation.getAnimatedFraction(); 359 if (reverse) { 360 animatedFraction = 1.0f - animatedFraction; 361 } 362 setNormalBackgroundVisibilityAmount(animatedFraction); 363 } 364 }) 365 .setDuration(ACTIVATE_ANIMATION_LENGTH); 366 } 367 368 /** 369 * Cancels the hotspot and makes the notification inactive. 370 */ 371 public void makeInactive(boolean animate) { 372 if (mActivated) { 373 mActivated = false; 374 if (mDimmed) { 375 if (animate) { 376 startActivateAnimation(true /* reverse */); 377 } else { 378 updateBackground(); 379 } 380 } 381 } 382 if (mOnActivatedListener != null) { 383 mOnActivatedListener.onActivationReset(this); 384 } 385 removeCallbacks(mTapTimeoutRunnable); 386 } 387 388 private boolean isWithinTouchSlop(MotionEvent event) { 389 return Math.abs(event.getX() - mDownX) < mTouchSlop 390 && Math.abs(event.getY() - mDownY) < mTouchSlop; 391 } 392 393 public void setDimmed(boolean dimmed, boolean fade) { 394 if (mDimmed != dimmed) { 395 mDimmed = dimmed; 396 resetBackgroundAlpha(); 397 if (fade) { 398 fadeDimmedBackground(); 399 } else { 400 updateBackground(); 401 } 402 } 403 } 404 405 public void setDark(boolean dark, boolean fade, long delay) { 406 super.setDark(dark, fade, delay); 407 if (mDark == dark) { 408 return; 409 } 410 mDark = dark; 411 updateBackground(); 412 if (!dark && fade && !shouldHideBackground()) { 413 fadeInFromDark(delay); 414 } 415 updateOutlineAlpha(); 416 } 417 418 private void updateOutlineAlpha() { 419 if (mDark) { 420 setOutlineAlpha(0f); 421 return; 422 } 423 float alpha = NotificationStackScrollLayout.BACKGROUND_ALPHA_DIMMED; 424 alpha = (alpha + (1.0f - alpha) * mNormalBackgroundVisibilityAmount); 425 alpha *= mShadowAlpha; 426 if (mFadeInFromDarkAnimator != null) { 427 alpha *= mFadeInFromDarkAnimator.getAnimatedFraction(); 428 } 429 setOutlineAlpha(alpha); 430 } 431 432 public void setNormalBackgroundVisibilityAmount(float normalBackgroundVisibilityAmount) { 433 mNormalBackgroundVisibilityAmount = normalBackgroundVisibilityAmount; 434 updateOutlineAlpha(); 435 } 436 437 public void setShowingLegacyBackground(boolean showing) { 438 mShowingLegacyBackground = showing; 439 updateBackgroundTint(); 440 } 441 442 @Override 443 public void setBelowSpeedBump(boolean below) { 444 super.setBelowSpeedBump(below); 445 if (below != mIsBelowSpeedBump) { 446 mIsBelowSpeedBump = below; 447 updateBackgroundTint(); 448 } 449 } 450 451 /** 452 * Sets the tint color of the background 453 */ 454 public void setTintColor(int color) { 455 setTintColor(color, false); 456 } 457 458 /** 459 * Sets the tint color of the background 460 */ 461 public void setTintColor(int color, boolean animated) { 462 mBgTint = color; 463 updateBackgroundTint(animated); 464 } 465 466 protected void updateBackgroundTint() { 467 updateBackgroundTint(false /* animated */); 468 } 469 470 private void updateBackgroundTint(boolean animated) { 471 if (mBackgroundColorAnimator != null) { 472 mBackgroundColorAnimator.cancel(); 473 } 474 int rippleColor = getRippleColor(); 475 mBackgroundDimmed.setRippleColor(rippleColor); 476 mBackgroundNormal.setRippleColor(rippleColor); 477 int color = calculateBgColor(); 478 if (!animated) { 479 setBackgroundTintColor(color); 480 } else if (color != mCurrentBackgroundTint) { 481 mStartTint = mCurrentBackgroundTint; 482 mTargetTint = color; 483 mBackgroundColorAnimator = ValueAnimator.ofFloat(0.0f, 1.0f); 484 mBackgroundColorAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 485 @Override 486 public void onAnimationUpdate(ValueAnimator animation) { 487 int newColor = NotificationUtils.interpolateColors(mStartTint, mTargetTint, 488 animation.getAnimatedFraction()); 489 setBackgroundTintColor(newColor); 490 } 491 }); 492 mBackgroundColorAnimator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 493 mBackgroundColorAnimator.setInterpolator(Interpolators.LINEAR); 494 mBackgroundColorAnimator.addListener(new AnimatorListenerAdapter() { 495 @Override 496 public void onAnimationEnd(Animator animation) { 497 mBackgroundColorAnimator = null; 498 } 499 }); 500 mBackgroundColorAnimator.start(); 501 } 502 } 503 504 private void setBackgroundTintColor(int color) { 505 mCurrentBackgroundTint = color; 506 if (color == mNormalColor) { 507 // We don't need to tint a normal notification 508 color = 0; 509 } 510 mBackgroundDimmed.setTint(color); 511 mBackgroundNormal.setTint(color); 512 } 513 514 /** 515 * Fades in the background when exiting dark mode. 516 */ 517 private void fadeInFromDark(long delay) { 518 final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal; 519 background.setAlpha(0f); 520 mBackgroundVisibilityUpdater.onAnimationUpdate(null); 521 background.setPivotX(mBackgroundDimmed.getWidth() / 2f); 522 background.setPivotY(getActualHeight() / 2f); 523 background.setScaleX(DARK_EXIT_SCALE_START); 524 background.setScaleY(DARK_EXIT_SCALE_START); 525 background.animate() 526 .alpha(1f) 527 .scaleX(1f) 528 .scaleY(1f) 529 .setDuration(DARK_ANIMATION_LENGTH) 530 .setStartDelay(delay) 531 .setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN) 532 .setListener(new AnimatorListenerAdapter() { 533 @Override 534 public void onAnimationCancel(Animator animation) { 535 // Jump state if we are cancelled 536 background.setScaleX(1f); 537 background.setScaleY(1f); 538 background.setAlpha(1f); 539 } 540 }) 541 .setUpdateListener(mBackgroundVisibilityUpdater) 542 .start(); 543 mFadeInFromDarkAnimator = TimeAnimator.ofFloat(0.0f, 1.0f); 544 mFadeInFromDarkAnimator.setDuration(DARK_ANIMATION_LENGTH); 545 mFadeInFromDarkAnimator.setStartDelay(delay); 546 mFadeInFromDarkAnimator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 547 mFadeInFromDarkAnimator.addListener(mFadeInEndListener); 548 mFadeInFromDarkAnimator.addUpdateListener(mUpdateOutlineListener); 549 mFadeInFromDarkAnimator.start(); 550 } 551 552 /** 553 * Fades the background when the dimmed state changes. 554 */ 555 private void fadeDimmedBackground() { 556 mBackgroundDimmed.animate().cancel(); 557 mBackgroundNormal.animate().cancel(); 558 if (mActivated) { 559 updateBackground(); 560 return; 561 } 562 if (!shouldHideBackground()) { 563 if (mDimmed) { 564 mBackgroundDimmed.setVisibility(View.VISIBLE); 565 } else { 566 mBackgroundNormal.setVisibility(View.VISIBLE); 567 } 568 } 569 float startAlpha = mDimmed ? 1f : 0; 570 float endAlpha = mDimmed ? 0 : 1f; 571 int duration = BACKGROUND_ANIMATION_LENGTH_MS; 572 // Check whether there is already a background animation running. 573 if (mBackgroundAnimator != null) { 574 startAlpha = (Float) mBackgroundAnimator.getAnimatedValue(); 575 duration = (int) mBackgroundAnimator.getCurrentPlayTime(); 576 mBackgroundAnimator.removeAllListeners(); 577 mBackgroundAnimator.cancel(); 578 if (duration <= 0) { 579 updateBackground(); 580 return; 581 } 582 } 583 mBackgroundNormal.setAlpha(startAlpha); 584 mBackgroundAnimator = 585 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha); 586 mBackgroundAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); 587 mBackgroundAnimator.setDuration(duration); 588 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 589 @Override 590 public void onAnimationEnd(Animator animation) { 591 updateBackground(); 592 mBackgroundAnimator = null; 593 } 594 }); 595 mBackgroundAnimator.addUpdateListener(mBackgroundVisibilityUpdater); 596 mBackgroundAnimator.start(); 597 } 598 599 protected void updateBackgroundAlpha(float transformationAmount) { 600 mBgAlpha = isChildInGroup() && mDimmed ? transformationAmount : 1f; 601 mBackgroundDimmed.setAlpha(mBgAlpha); 602 } 603 604 protected void resetBackgroundAlpha() { 605 updateBackgroundAlpha(0f /* transformationAmount */); 606 } 607 608 protected void updateBackground() { 609 cancelFadeAnimations(); 610 if (shouldHideBackground()) { 611 mBackgroundDimmed.setVisibility(View.INVISIBLE); 612 mBackgroundNormal.setVisibility(View.INVISIBLE); 613 } else if (mDimmed) { 614 // When groups are animating to the expanded state from the lockscreen, show the 615 // normal background instead of the dimmed background 616 final boolean dontShowDimmed = isGroupExpansionChanging() && isChildInGroup(); 617 mBackgroundDimmed.setVisibility(dontShowDimmed ? View.INVISIBLE : View.VISIBLE); 618 mBackgroundNormal.setVisibility((mActivated || dontShowDimmed) 619 ? View.VISIBLE 620 : View.INVISIBLE); 621 } else { 622 mBackgroundDimmed.setVisibility(View.INVISIBLE); 623 mBackgroundNormal.setVisibility(View.VISIBLE); 624 mBackgroundNormal.setAlpha(1f); 625 removeCallbacks(mTapTimeoutRunnable); 626 // make in inactive to avoid it sticking around active 627 makeInactive(false /* animate */); 628 } 629 setNormalBackgroundVisibilityAmount( 630 mBackgroundNormal.getVisibility() == View.VISIBLE ? 1.0f : 0.0f); 631 } 632 633 protected boolean shouldHideBackground() { 634 return mDark; 635 } 636 637 private void cancelFadeAnimations() { 638 if (mBackgroundAnimator != null) { 639 mBackgroundAnimator.cancel(); 640 } 641 mBackgroundDimmed.animate().cancel(); 642 mBackgroundNormal.animate().cancel(); 643 } 644 645 @Override 646 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 647 super.onLayout(changed, left, top, right, bottom); 648 setPivotX(getWidth() / 2); 649 } 650 651 @Override 652 public void setActualHeight(int actualHeight, boolean notifyListeners) { 653 super.setActualHeight(actualHeight, notifyListeners); 654 setPivotY(actualHeight / 2); 655 mBackgroundNormal.setActualHeight(actualHeight); 656 mBackgroundDimmed.setActualHeight(actualHeight); 657 } 658 659 @Override 660 public void setClipTopAmount(int clipTopAmount) { 661 super.setClipTopAmount(clipTopAmount); 662 mBackgroundNormal.setClipTopAmount(clipTopAmount); 663 mBackgroundDimmed.setClipTopAmount(clipTopAmount); 664 } 665 666 @Override 667 public void performRemoveAnimation(long duration, float translationDirection, 668 Runnable onFinishedRunnable) { 669 enableAppearDrawing(true); 670 if (mDrawingAppearAnimation) { 671 startAppearAnimation(false /* isAppearing */, translationDirection, 672 0, duration, onFinishedRunnable); 673 } else if (onFinishedRunnable != null) { 674 onFinishedRunnable.run(); 675 } 676 } 677 678 @Override 679 public void performAddAnimation(long delay, long duration) { 680 enableAppearDrawing(true); 681 if (mDrawingAppearAnimation) { 682 startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null); 683 } 684 } 685 686 private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, 687 long duration, final Runnable onFinishedRunnable) { 688 cancelAppearAnimation(); 689 mAnimationTranslationY = translationDirection * getActualHeight(); 690 if (mAppearAnimationFraction == -1.0f) { 691 // not initialized yet, we start anew 692 if (isAppearing) { 693 mAppearAnimationFraction = 0.0f; 694 mAppearAnimationTranslation = mAnimationTranslationY; 695 } else { 696 mAppearAnimationFraction = 1.0f; 697 mAppearAnimationTranslation = 0; 698 } 699 } 700 701 float targetValue; 702 if (isAppearing) { 703 mCurrentAppearInterpolator = mSlowOutFastInInterpolator; 704 mCurrentAlphaInterpolator = Interpolators.LINEAR_OUT_SLOW_IN; 705 targetValue = 1.0f; 706 } else { 707 mCurrentAppearInterpolator = Interpolators.FAST_OUT_SLOW_IN; 708 mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator; 709 targetValue = 0.0f; 710 } 711 mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, 712 targetValue); 713 mAppearAnimator.setInterpolator(Interpolators.LINEAR); 714 mAppearAnimator.setDuration( 715 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); 716 mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 717 @Override 718 public void onAnimationUpdate(ValueAnimator animation) { 719 mAppearAnimationFraction = (float) animation.getAnimatedValue(); 720 updateAppearAnimationAlpha(); 721 updateAppearRect(); 722 invalidate(); 723 } 724 }); 725 if (delay > 0) { 726 // we need to apply the initial state already to avoid drawn frames in the wrong state 727 updateAppearAnimationAlpha(); 728 updateAppearRect(); 729 mAppearAnimator.setStartDelay(delay); 730 } 731 mAppearAnimator.addListener(new AnimatorListenerAdapter() { 732 private boolean mWasCancelled; 733 734 @Override 735 public void onAnimationEnd(Animator animation) { 736 if (onFinishedRunnable != null) { 737 onFinishedRunnable.run(); 738 } 739 if (!mWasCancelled) { 740 enableAppearDrawing(false); 741 } 742 } 743 744 @Override 745 public void onAnimationStart(Animator animation) { 746 mWasCancelled = false; 747 } 748 749 @Override 750 public void onAnimationCancel(Animator animation) { 751 mWasCancelled = true; 752 } 753 }); 754 mAppearAnimator.start(); 755 } 756 757 private void cancelAppearAnimation() { 758 if (mAppearAnimator != null) { 759 mAppearAnimator.cancel(); 760 mAppearAnimator = null; 761 } 762 } 763 764 public void cancelAppearDrawing() { 765 cancelAppearAnimation(); 766 enableAppearDrawing(false); 767 } 768 769 private void updateAppearRect() { 770 float inverseFraction = (1.0f - mAppearAnimationFraction); 771 float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction); 772 float translateYTotalAmount = translationFraction * mAnimationTranslationY; 773 mAppearAnimationTranslation = translateYTotalAmount; 774 775 // handle width animation 776 float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START)) 777 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END); 778 widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction)); 779 widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction); 780 float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) * 781 widthFraction); 782 float right = getWidth() - left; 783 784 // handle top animation 785 float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) / 786 VERTICAL_ANIMATION_START; 787 heightFraction = Math.max(0.0f, heightFraction); 788 heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction); 789 790 float top; 791 float bottom; 792 final int actualHeight = getActualHeight(); 793 if (mAnimationTranslationY > 0.0f) { 794 bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f 795 - translateYTotalAmount; 796 top = bottom * heightFraction; 797 } else { 798 top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f - 799 translateYTotalAmount; 800 bottom = actualHeight * (1 - heightFraction) + top * heightFraction; 801 } 802 mAppearAnimationRect.set(left, top, right, bottom); 803 setOutlineRect(left, top + mAppearAnimationTranslation, right, 804 bottom + mAppearAnimationTranslation); 805 } 806 807 private void updateAppearAnimationAlpha() { 808 float contentAlphaProgress = mAppearAnimationFraction; 809 contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END); 810 contentAlphaProgress = Math.min(1.0f, contentAlphaProgress); 811 contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress); 812 setContentAlpha(contentAlphaProgress); 813 } 814 815 private void setContentAlpha(float contentAlpha) { 816 View contentView = getContentView(); 817 if (contentView.hasOverlappingRendering()) { 818 int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE 819 : LAYER_TYPE_HARDWARE; 820 int currentLayerType = contentView.getLayerType(); 821 if (currentLayerType != layerType) { 822 contentView.setLayerType(layerType, null); 823 } 824 } 825 contentView.setAlpha(contentAlpha); 826 } 827 828 protected abstract View getContentView(); 829 830 public int calculateBgColor() { 831 return calculateBgColor(true /* withTint */); 832 } 833 834 private int calculateBgColor(boolean withTint) { 835 if (withTint && mBgTint != 0) { 836 return mBgTint; 837 } else if (mShowingLegacyBackground) { 838 return mLegacyColor; 839 } else if (mIsBelowSpeedBump) { 840 return mLowPriorityColor; 841 } else { 842 return mNormalColor; 843 } 844 } 845 846 protected int getRippleColor() { 847 if (mBgTint != 0) { 848 return mTintedRippleColor; 849 } else if (mShowingLegacyBackground) { 850 return mTintedRippleColor; 851 } else if (mIsBelowSpeedBump) { 852 return mLowPriorityRippleColor; 853 } else { 854 return mNormalRippleColor; 855 } 856 } 857 858 /** 859 * When we draw the appear animation, we render the view in a bitmap and render this bitmap 860 * as a shader of a rect. This call creates the Bitmap and switches the drawing mode, 861 * such that the normal drawing of the views does not happen anymore. 862 * 863 * @param enable Should it be enabled. 864 */ 865 private void enableAppearDrawing(boolean enable) { 866 if (enable != mDrawingAppearAnimation) { 867 mDrawingAppearAnimation = enable; 868 if (!enable) { 869 setContentAlpha(1.0f); 870 mAppearAnimationFraction = -1; 871 setOutlineRect(null); 872 } 873 invalidate(); 874 } 875 } 876 877 @Override 878 protected void dispatchDraw(Canvas canvas) { 879 if (mDrawingAppearAnimation) { 880 canvas.save(); 881 canvas.translate(0, mAppearAnimationTranslation); 882 } 883 super.dispatchDraw(canvas); 884 if (mDrawingAppearAnimation) { 885 canvas.restore(); 886 } 887 } 888 889 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 890 mOnActivatedListener = onActivatedListener; 891 } 892 893 public void reset() { 894 setTintColor(0); 895 resetBackgroundAlpha(); 896 setShowingLegacyBackground(false); 897 setBelowSpeedBump(false); 898 } 899 900 public boolean hasSameBgColor(ActivatableNotificationView otherView) { 901 return calculateBgColor() == otherView.calculateBgColor(); 902 } 903 904 @Override 905 public float getShadowAlpha() { 906 return mShadowAlpha; 907 } 908 909 @Override 910 public void setShadowAlpha(float shadowAlpha) { 911 if (shadowAlpha != mShadowAlpha) { 912 mShadowAlpha = shadowAlpha; 913 updateOutlineAlpha(); 914 } 915 } 916 917 @Override 918 public void setFakeShadowIntensity(float shadowIntensity, float outlineAlpha, int shadowYEnd, 919 int outlineTranslation) { 920 mFakeShadow.setFakeShadowTranslationZ(shadowIntensity * (getTranslationZ() 921 + FakeShadowView.SHADOW_SIBLING_TRESHOLD), outlineAlpha, shadowYEnd, 922 outlineTranslation); 923 } 924 925 public int getBackgroundColorWithoutTint() { 926 return calculateBgColor(false /* withTint */); 927 } 928 929 public interface OnActivatedListener { 930 void onActivated(ActivatableNotificationView view); 931 void onActivationReset(ActivatableNotificationView view); 932 } 933 } 934