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.ValueAnimator; 23 import android.content.Context; 24 import android.graphics.Canvas; 25 import android.graphics.RectF; 26 import android.util.AttributeSet; 27 import android.view.MotionEvent; 28 import android.view.View; 29 import android.view.ViewAnimationUtils; 30 import android.view.ViewConfiguration; 31 import android.view.animation.AnimationUtils; 32 import android.view.animation.Interpolator; 33 import android.view.animation.LinearInterpolator; 34 import android.view.animation.PathInterpolator; 35 36 import com.android.systemui.R; 37 38 /** 39 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer} 40 * to implement dimming/activating on Keyguard for the double-tap gesture 41 */ 42 public abstract class ActivatableNotificationView extends ExpandableOutlineView { 43 44 private static final long DOUBLETAP_TIMEOUT_MS = 1200; 45 private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220; 46 private static final int ACTIVATE_ANIMATION_LENGTH = 220; 47 private static final int DARK_ANIMATION_LENGTH = 170; 48 49 /** 50 * The amount of width, which is kept in the end when performing a disappear animation (also 51 * the amount from which the horizontal appearing begins) 52 */ 53 private static final float HORIZONTAL_COLLAPSED_REST_PARTIAL = 0.05f; 54 55 /** 56 * At which point from [0,1] does the horizontal collapse animation end (or start when 57 * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated. 58 */ 59 private static final float HORIZONTAL_ANIMATION_END = 0.2f; 60 61 /** 62 * At which point from [0,1] does the alpha animation end (or start when 63 * expanding)? 1.0 meaning that it ends immediately and 0.0 that it is continuously animated. 64 */ 65 private static final float ALPHA_ANIMATION_END = 0.0f; 66 67 /** 68 * At which point from [0,1] does the horizontal collapse animation start (or start when 69 * expanding)? 1.0 meaning that it starts immediately and 0.0 that it is animated at all. 70 */ 71 private static final float HORIZONTAL_ANIMATION_START = 1.0f; 72 73 /** 74 * At which point from [0,1] does the vertical collapse animation start (or end when 75 * expanding) 1.0 meaning that it starts immediately and 0.0 that it is animated at all. 76 */ 77 private static final float VERTICAL_ANIMATION_START = 1.0f; 78 79 /** 80 * Scale for the background to animate from when exiting dark mode. 81 */ 82 private static final float DARK_EXIT_SCALE_START = 0.93f; 83 84 private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR 85 = new PathInterpolator(0.6f, 0, 0.5f, 1); 86 private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR 87 = new PathInterpolator(0, 0, 0.5f, 1); 88 private final int mTintedRippleColor; 89 private final int mLowPriorityRippleColor; 90 protected final int mNormalRippleColor; 91 92 private boolean mDimmed; 93 private boolean mDark; 94 95 private int mBgTint = 0; 96 97 /** 98 * Flag to indicate that the notification has been touched once and the second touch will 99 * click it. 100 */ 101 private boolean mActivated; 102 103 private float mDownX; 104 private float mDownY; 105 private final float mTouchSlop; 106 107 private OnActivatedListener mOnActivatedListener; 108 109 private final Interpolator mLinearOutSlowInInterpolator; 110 protected final Interpolator mFastOutSlowInInterpolator; 111 private final Interpolator mSlowOutFastInInterpolator; 112 private final Interpolator mSlowOutLinearInInterpolator; 113 private final Interpolator mLinearInterpolator; 114 private Interpolator mCurrentAppearInterpolator; 115 private Interpolator mCurrentAlphaInterpolator; 116 117 private NotificationBackgroundView mBackgroundNormal; 118 private NotificationBackgroundView mBackgroundDimmed; 119 private ObjectAnimator mBackgroundAnimator; 120 private RectF mAppearAnimationRect = new RectF(); 121 private float mAnimationTranslationY; 122 private boolean mDrawingAppearAnimation; 123 private ValueAnimator mAppearAnimator; 124 private float mAppearAnimationFraction = -1.0f; 125 private float mAppearAnimationTranslation; 126 private boolean mShowingLegacyBackground; 127 private final int mLegacyColor; 128 private final int mNormalColor; 129 private final int mLowPriorityColor; 130 private boolean mIsBelowSpeedBump; 131 ActivatableNotificationView(Context context, AttributeSet attrs)132 public ActivatableNotificationView(Context context, AttributeSet attrs) { 133 super(context, attrs); 134 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 135 mFastOutSlowInInterpolator = 136 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 137 mSlowOutFastInInterpolator = new PathInterpolator(0.8f, 0.0f, 0.6f, 1.0f); 138 mLinearOutSlowInInterpolator = 139 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); 140 mSlowOutLinearInInterpolator = new PathInterpolator(0.8f, 0.0f, 1.0f, 1.0f); 141 mLinearInterpolator = new LinearInterpolator(); 142 setClipChildren(false); 143 setClipToPadding(false); 144 mLegacyColor = context.getColor(R.color.notification_legacy_background_color); 145 mNormalColor = context.getColor(R.color.notification_material_background_color); 146 mLowPriorityColor = context.getColor( 147 R.color.notification_material_background_low_priority_color); 148 mTintedRippleColor = context.getColor( 149 R.color.notification_ripple_tinted_color); 150 mLowPriorityRippleColor = context.getColor( 151 R.color.notification_ripple_color_low_priority); 152 mNormalRippleColor = context.getColor( 153 R.color.notification_ripple_untinted_color); 154 } 155 156 @Override onFinishInflate()157 protected void onFinishInflate() { 158 super.onFinishInflate(); 159 mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal); 160 mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed); 161 mBackgroundNormal.setCustomBackground(R.drawable.notification_material_bg); 162 mBackgroundDimmed.setCustomBackground(R.drawable.notification_material_bg_dim); 163 updateBackground(); 164 updateBackgroundTint(); 165 } 166 167 private final Runnable mTapTimeoutRunnable = new Runnable() { 168 @Override 169 public void run() { 170 makeInactive(true /* animate */); 171 } 172 }; 173 174 @Override onTouchEvent(MotionEvent event)175 public boolean onTouchEvent(MotionEvent event) { 176 if (mDimmed) { 177 return handleTouchEventDimmed(event); 178 } else { 179 return super.onTouchEvent(event); 180 } 181 } 182 183 @Override drawableHotspotChanged(float x, float y)184 public void drawableHotspotChanged(float x, float y) { 185 if (!mDimmed){ 186 mBackgroundNormal.drawableHotspotChanged(x, y); 187 } 188 } 189 190 @Override drawableStateChanged()191 protected void drawableStateChanged() { 192 super.drawableStateChanged(); 193 if (mDimmed) { 194 mBackgroundDimmed.setState(getDrawableState()); 195 } else { 196 mBackgroundNormal.setState(getDrawableState()); 197 } 198 } 199 handleTouchEventDimmed(MotionEvent event)200 private boolean handleTouchEventDimmed(MotionEvent event) { 201 int action = event.getActionMasked(); 202 switch (action) { 203 case MotionEvent.ACTION_DOWN: 204 mDownX = event.getX(); 205 mDownY = event.getY(); 206 if (mDownY > getActualHeight()) { 207 return false; 208 } 209 break; 210 case MotionEvent.ACTION_MOVE: 211 if (!isWithinTouchSlop(event)) { 212 makeInactive(true /* animate */); 213 return false; 214 } 215 break; 216 case MotionEvent.ACTION_UP: 217 if (isWithinTouchSlop(event)) { 218 if (!mActivated) { 219 makeActive(); 220 postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS); 221 } else { 222 boolean performed = performClick(); 223 if (performed) { 224 removeCallbacks(mTapTimeoutRunnable); 225 } 226 } 227 } else { 228 makeInactive(true /* animate */); 229 } 230 break; 231 case MotionEvent.ACTION_CANCEL: 232 makeInactive(true /* animate */); 233 break; 234 default: 235 break; 236 } 237 return true; 238 } 239 makeActive()240 private void makeActive() { 241 startActivateAnimation(false /* reverse */); 242 mActivated = true; 243 if (mOnActivatedListener != null) { 244 mOnActivatedListener.onActivated(this); 245 } 246 } 247 startActivateAnimation(boolean reverse)248 private void startActivateAnimation(boolean reverse) { 249 if (!isAttachedToWindow()) { 250 return; 251 } 252 int widthHalf = mBackgroundNormal.getWidth()/2; 253 int heightHalf = mBackgroundNormal.getActualHeight()/2; 254 float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf); 255 Animator animator; 256 if (reverse) { 257 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 258 widthHalf, heightHalf, radius, 0); 259 } else { 260 animator = ViewAnimationUtils.createCircularReveal(mBackgroundNormal, 261 widthHalf, heightHalf, 0, radius); 262 } 263 mBackgroundNormal.setVisibility(View.VISIBLE); 264 Interpolator interpolator; 265 Interpolator alphaInterpolator; 266 if (!reverse) { 267 interpolator = mLinearOutSlowInInterpolator; 268 alphaInterpolator = mLinearOutSlowInInterpolator; 269 } else { 270 interpolator = ACTIVATE_INVERSE_INTERPOLATOR; 271 alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR; 272 } 273 animator.setInterpolator(interpolator); 274 animator.setDuration(ACTIVATE_ANIMATION_LENGTH); 275 if (reverse) { 276 mBackgroundNormal.setAlpha(1f); 277 animator.addListener(new AnimatorListenerAdapter() { 278 @Override 279 public void onAnimationEnd(Animator animation) { 280 if (mDimmed) { 281 mBackgroundNormal.setVisibility(View.INVISIBLE); 282 } 283 } 284 }); 285 animator.start(); 286 } else { 287 mBackgroundNormal.setAlpha(0.4f); 288 animator.start(); 289 } 290 mBackgroundNormal.animate() 291 .alpha(reverse ? 0f : 1f) 292 .setInterpolator(alphaInterpolator) 293 .setDuration(ACTIVATE_ANIMATION_LENGTH); 294 } 295 296 /** 297 * Cancels the hotspot and makes the notification inactive. 298 */ makeInactive(boolean animate)299 public void makeInactive(boolean animate) { 300 if (mActivated) { 301 if (mDimmed) { 302 if (animate) { 303 startActivateAnimation(true /* reverse */); 304 } else { 305 mBackgroundNormal.setVisibility(View.INVISIBLE); 306 } 307 } 308 mActivated = false; 309 } 310 if (mOnActivatedListener != null) { 311 mOnActivatedListener.onActivationReset(this); 312 } 313 removeCallbacks(mTapTimeoutRunnable); 314 } 315 isWithinTouchSlop(MotionEvent event)316 private boolean isWithinTouchSlop(MotionEvent event) { 317 return Math.abs(event.getX() - mDownX) < mTouchSlop 318 && Math.abs(event.getY() - mDownY) < mTouchSlop; 319 } 320 setDimmed(boolean dimmed, boolean fade)321 public void setDimmed(boolean dimmed, boolean fade) { 322 if (mDimmed != dimmed) { 323 mDimmed = dimmed; 324 if (fade) { 325 fadeDimmedBackground(); 326 } else { 327 updateBackground(); 328 } 329 } 330 } 331 setDark(boolean dark, boolean fade, long delay)332 public void setDark(boolean dark, boolean fade, long delay) { 333 super.setDark(dark, fade, delay); 334 if (mDark == dark) { 335 return; 336 } 337 mDark = dark; 338 if (!dark && fade) { 339 if (mActivated) { 340 mBackgroundDimmed.setVisibility(View.VISIBLE); 341 mBackgroundNormal.setVisibility(View.VISIBLE); 342 } else if (mDimmed) { 343 mBackgroundDimmed.setVisibility(View.VISIBLE); 344 mBackgroundNormal.setVisibility(View.INVISIBLE); 345 } else { 346 mBackgroundDimmed.setVisibility(View.INVISIBLE); 347 mBackgroundNormal.setVisibility(View.VISIBLE); 348 } 349 fadeInFromDark(delay); 350 } else { 351 updateBackground(); 352 } 353 setOutlineAlpha(dark ? 0f : 1f); 354 } 355 setShowingLegacyBackground(boolean showing)356 public void setShowingLegacyBackground(boolean showing) { 357 mShowingLegacyBackground = showing; 358 updateBackgroundTint(); 359 } 360 361 @Override setBelowSpeedBump(boolean below)362 public void setBelowSpeedBump(boolean below) { 363 super.setBelowSpeedBump(below); 364 if (below != mIsBelowSpeedBump) { 365 mIsBelowSpeedBump = below; 366 updateBackgroundTint(); 367 } 368 } 369 370 /** 371 * Sets the tint color of the background 372 */ setTintColor(int color)373 public void setTintColor(int color) { 374 mBgTint = color; 375 updateBackgroundTint(); 376 } 377 updateBackgroundTint()378 private void updateBackgroundTint() { 379 int color = getBgColor(); 380 int rippleColor = getRippleColor(); 381 if (color == mNormalColor) { 382 // We don't need to tint a normal notification 383 color = 0; 384 } 385 mBackgroundDimmed.setTint(color); 386 mBackgroundNormal.setTint(color); 387 mBackgroundDimmed.setRippleColor(rippleColor); 388 mBackgroundNormal.setRippleColor(rippleColor); 389 } 390 391 /** 392 * Fades in the background when exiting dark mode. 393 */ fadeInFromDark(long delay)394 private void fadeInFromDark(long delay) { 395 final View background = mDimmed ? mBackgroundDimmed : mBackgroundNormal; 396 background.setAlpha(0f); 397 background.setPivotX(mBackgroundDimmed.getWidth() / 2f); 398 background.setPivotY(getActualHeight() / 2f); 399 background.setScaleX(DARK_EXIT_SCALE_START); 400 background.setScaleY(DARK_EXIT_SCALE_START); 401 background.animate() 402 .alpha(1f) 403 .scaleX(1f) 404 .scaleY(1f) 405 .setDuration(DARK_ANIMATION_LENGTH) 406 .setStartDelay(delay) 407 .setInterpolator(mLinearOutSlowInInterpolator) 408 .setListener(new AnimatorListenerAdapter() { 409 @Override 410 public void onAnimationCancel(Animator animation) { 411 // Jump state if we are cancelled 412 background.setScaleX(1f); 413 background.setScaleY(1f); 414 background.setAlpha(1f); 415 } 416 }) 417 .start(); 418 } 419 420 /** 421 * Fades the background when the dimmed state changes. 422 */ fadeDimmedBackground()423 private void fadeDimmedBackground() { 424 mBackgroundDimmed.animate().cancel(); 425 mBackgroundNormal.animate().cancel(); 426 if (mDimmed) { 427 mBackgroundDimmed.setVisibility(View.VISIBLE); 428 } else { 429 mBackgroundNormal.setVisibility(View.VISIBLE); 430 } 431 float startAlpha = mDimmed ? 1f : 0; 432 float endAlpha = mDimmed ? 0 : 1f; 433 int duration = BACKGROUND_ANIMATION_LENGTH_MS; 434 // Check whether there is already a background animation running. 435 if (mBackgroundAnimator != null) { 436 startAlpha = (Float) mBackgroundAnimator.getAnimatedValue(); 437 duration = (int) mBackgroundAnimator.getCurrentPlayTime(); 438 mBackgroundAnimator.removeAllListeners(); 439 mBackgroundAnimator.cancel(); 440 if (duration <= 0) { 441 updateBackground(); 442 return; 443 } 444 } 445 mBackgroundNormal.setAlpha(startAlpha); 446 mBackgroundAnimator = 447 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha); 448 mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator); 449 mBackgroundAnimator.setDuration(duration); 450 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 451 @Override 452 public void onAnimationEnd(Animator animation) { 453 if (mDimmed) { 454 mBackgroundNormal.setVisibility(View.INVISIBLE); 455 } else { 456 mBackgroundDimmed.setVisibility(View.INVISIBLE); 457 } 458 mBackgroundAnimator = null; 459 } 460 }); 461 mBackgroundAnimator.start(); 462 } 463 updateBackground()464 private void updateBackground() { 465 cancelFadeAnimations(); 466 if (mDark) { 467 mBackgroundDimmed.setVisibility(View.INVISIBLE); 468 mBackgroundNormal.setVisibility(View.INVISIBLE); 469 } else if (mDimmed) { 470 mBackgroundDimmed.setVisibility(View.VISIBLE); 471 mBackgroundNormal.setVisibility(View.INVISIBLE); 472 } else { 473 mBackgroundDimmed.setVisibility(View.INVISIBLE); 474 mBackgroundNormal.setVisibility(View.VISIBLE); 475 mBackgroundNormal.setAlpha(1f); 476 removeCallbacks(mTapTimeoutRunnable); 477 } 478 } 479 cancelFadeAnimations()480 private void cancelFadeAnimations() { 481 if (mBackgroundAnimator != null) { 482 mBackgroundAnimator.cancel(); 483 } 484 mBackgroundDimmed.animate().cancel(); 485 mBackgroundNormal.animate().cancel(); 486 } 487 488 @Override onLayout(boolean changed, int left, int top, int right, int bottom)489 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 490 super.onLayout(changed, left, top, right, bottom); 491 setPivotX(getWidth() / 2); 492 } 493 494 @Override setActualHeight(int actualHeight, boolean notifyListeners)495 public void setActualHeight(int actualHeight, boolean notifyListeners) { 496 super.setActualHeight(actualHeight, notifyListeners); 497 setPivotY(actualHeight / 2); 498 mBackgroundNormal.setActualHeight(actualHeight); 499 mBackgroundDimmed.setActualHeight(actualHeight); 500 } 501 502 @Override setClipTopAmount(int clipTopAmount)503 public void setClipTopAmount(int clipTopAmount) { 504 super.setClipTopAmount(clipTopAmount); 505 mBackgroundNormal.setClipTopAmount(clipTopAmount); 506 mBackgroundDimmed.setClipTopAmount(clipTopAmount); 507 } 508 509 @Override performRemoveAnimation(long duration, float translationDirection, Runnable onFinishedRunnable)510 public void performRemoveAnimation(long duration, float translationDirection, 511 Runnable onFinishedRunnable) { 512 enableAppearDrawing(true); 513 if (mDrawingAppearAnimation) { 514 startAppearAnimation(false /* isAppearing */, translationDirection, 515 0, duration, onFinishedRunnable); 516 } else if (onFinishedRunnable != null) { 517 onFinishedRunnable.run(); 518 } 519 } 520 521 @Override performAddAnimation(long delay, long duration)522 public void performAddAnimation(long delay, long duration) { 523 enableAppearDrawing(true); 524 if (mDrawingAppearAnimation) { 525 startAppearAnimation(true /* isAppearing */, -1.0f, delay, duration, null); 526 } 527 } 528 startAppearAnimation(boolean isAppearing, float translationDirection, long delay, long duration, final Runnable onFinishedRunnable)529 private void startAppearAnimation(boolean isAppearing, float translationDirection, long delay, 530 long duration, final Runnable onFinishedRunnable) { 531 cancelAppearAnimation(); 532 mAnimationTranslationY = translationDirection * getActualHeight(); 533 if (mAppearAnimationFraction == -1.0f) { 534 // not initialized yet, we start anew 535 if (isAppearing) { 536 mAppearAnimationFraction = 0.0f; 537 mAppearAnimationTranslation = mAnimationTranslationY; 538 } else { 539 mAppearAnimationFraction = 1.0f; 540 mAppearAnimationTranslation = 0; 541 } 542 } 543 544 float targetValue; 545 if (isAppearing) { 546 mCurrentAppearInterpolator = mSlowOutFastInInterpolator; 547 mCurrentAlphaInterpolator = mLinearOutSlowInInterpolator; 548 targetValue = 1.0f; 549 } else { 550 mCurrentAppearInterpolator = mFastOutSlowInInterpolator; 551 mCurrentAlphaInterpolator = mSlowOutLinearInInterpolator; 552 targetValue = 0.0f; 553 } 554 mAppearAnimator = ValueAnimator.ofFloat(mAppearAnimationFraction, 555 targetValue); 556 mAppearAnimator.setInterpolator(mLinearInterpolator); 557 mAppearAnimator.setDuration( 558 (long) (duration * Math.abs(mAppearAnimationFraction - targetValue))); 559 mAppearAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { 560 @Override 561 public void onAnimationUpdate(ValueAnimator animation) { 562 mAppearAnimationFraction = (float) animation.getAnimatedValue(); 563 updateAppearAnimationAlpha(); 564 updateAppearRect(); 565 invalidate(); 566 } 567 }); 568 if (delay > 0) { 569 // we need to apply the initial state already to avoid drawn frames in the wrong state 570 updateAppearAnimationAlpha(); 571 updateAppearRect(); 572 mAppearAnimator.setStartDelay(delay); 573 } 574 mAppearAnimator.addListener(new AnimatorListenerAdapter() { 575 private boolean mWasCancelled; 576 577 @Override 578 public void onAnimationEnd(Animator animation) { 579 if (onFinishedRunnable != null) { 580 onFinishedRunnable.run(); 581 } 582 if (!mWasCancelled) { 583 mAppearAnimationFraction = -1; 584 setOutlineRect(null); 585 enableAppearDrawing(false); 586 } 587 } 588 589 @Override 590 public void onAnimationStart(Animator animation) { 591 mWasCancelled = false; 592 } 593 594 @Override 595 public void onAnimationCancel(Animator animation) { 596 mWasCancelled = true; 597 } 598 }); 599 mAppearAnimator.start(); 600 } 601 cancelAppearAnimation()602 private void cancelAppearAnimation() { 603 if (mAppearAnimator != null) { 604 mAppearAnimator.cancel(); 605 } 606 } 607 cancelAppearDrawing()608 public void cancelAppearDrawing() { 609 cancelAppearAnimation(); 610 enableAppearDrawing(false); 611 } 612 updateAppearRect()613 private void updateAppearRect() { 614 float inverseFraction = (1.0f - mAppearAnimationFraction); 615 float translationFraction = mCurrentAppearInterpolator.getInterpolation(inverseFraction); 616 float translateYTotalAmount = translationFraction * mAnimationTranslationY; 617 mAppearAnimationTranslation = translateYTotalAmount; 618 619 // handle width animation 620 float widthFraction = (inverseFraction - (1.0f - HORIZONTAL_ANIMATION_START)) 621 / (HORIZONTAL_ANIMATION_START - HORIZONTAL_ANIMATION_END); 622 widthFraction = Math.min(1.0f, Math.max(0.0f, widthFraction)); 623 widthFraction = mCurrentAppearInterpolator.getInterpolation(widthFraction); 624 float left = (getWidth() * (0.5f - HORIZONTAL_COLLAPSED_REST_PARTIAL / 2.0f) * 625 widthFraction); 626 float right = getWidth() - left; 627 628 // handle top animation 629 float heightFraction = (inverseFraction - (1.0f - VERTICAL_ANIMATION_START)) / 630 VERTICAL_ANIMATION_START; 631 heightFraction = Math.max(0.0f, heightFraction); 632 heightFraction = mCurrentAppearInterpolator.getInterpolation(heightFraction); 633 634 float top; 635 float bottom; 636 final int actualHeight = getActualHeight(); 637 if (mAnimationTranslationY > 0.0f) { 638 bottom = actualHeight - heightFraction * mAnimationTranslationY * 0.1f 639 - translateYTotalAmount; 640 top = bottom * heightFraction; 641 } else { 642 top = heightFraction * (actualHeight + mAnimationTranslationY) * 0.1f - 643 translateYTotalAmount; 644 bottom = actualHeight * (1 - heightFraction) + top * heightFraction; 645 } 646 mAppearAnimationRect.set(left, top, right, bottom); 647 setOutlineRect(left, top + mAppearAnimationTranslation, right, 648 bottom + mAppearAnimationTranslation); 649 } 650 updateAppearAnimationAlpha()651 private void updateAppearAnimationAlpha() { 652 float contentAlphaProgress = mAppearAnimationFraction; 653 contentAlphaProgress = contentAlphaProgress / (1.0f - ALPHA_ANIMATION_END); 654 contentAlphaProgress = Math.min(1.0f, contentAlphaProgress); 655 contentAlphaProgress = mCurrentAlphaInterpolator.getInterpolation(contentAlphaProgress); 656 setContentAlpha(contentAlphaProgress); 657 } 658 setContentAlpha(float contentAlpha)659 private void setContentAlpha(float contentAlpha) { 660 View contentView = getContentView(); 661 if (contentView.hasOverlappingRendering()) { 662 int layerType = contentAlpha == 0.0f || contentAlpha == 1.0f ? LAYER_TYPE_NONE 663 : LAYER_TYPE_HARDWARE; 664 int currentLayerType = contentView.getLayerType(); 665 if (currentLayerType != layerType) { 666 contentView.setLayerType(layerType, null); 667 } 668 } 669 contentView.setAlpha(contentAlpha); 670 } 671 getContentView()672 protected abstract View getContentView(); 673 getBgColor()674 private int getBgColor() { 675 if (mBgTint != 0) { 676 return mBgTint; 677 } else if (mShowingLegacyBackground) { 678 return mLegacyColor; 679 } else if (mIsBelowSpeedBump) { 680 return mLowPriorityColor; 681 } else { 682 return mNormalColor; 683 } 684 } 685 getRippleColor()686 protected int getRippleColor() { 687 if (mBgTint != 0) { 688 return mTintedRippleColor; 689 } else if (mShowingLegacyBackground) { 690 return mTintedRippleColor; 691 } else if (mIsBelowSpeedBump) { 692 return mLowPriorityRippleColor; 693 } else { 694 return mNormalRippleColor; 695 } 696 } 697 698 /** 699 * When we draw the appear animation, we render the view in a bitmap and render this bitmap 700 * as a shader of a rect. This call creates the Bitmap and switches the drawing mode, 701 * such that the normal drawing of the views does not happen anymore. 702 * 703 * @param enable Should it be enabled. 704 */ enableAppearDrawing(boolean enable)705 private void enableAppearDrawing(boolean enable) { 706 if (enable != mDrawingAppearAnimation) { 707 mDrawingAppearAnimation = enable; 708 if (!enable) { 709 setContentAlpha(1.0f); 710 } 711 invalidate(); 712 } 713 } 714 715 @Override dispatchDraw(Canvas canvas)716 protected void dispatchDraw(Canvas canvas) { 717 if (mDrawingAppearAnimation) { 718 canvas.save(); 719 canvas.translate(0, mAppearAnimationTranslation); 720 } 721 super.dispatchDraw(canvas); 722 if (mDrawingAppearAnimation) { 723 canvas.restore(); 724 } 725 } 726 setOnActivatedListener(OnActivatedListener onActivatedListener)727 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 728 mOnActivatedListener = onActivatedListener; 729 } 730 reset()731 public void reset() { 732 setTintColor(0); 733 setShowingLegacyBackground(false); 734 setBelowSpeedBump(false); 735 } 736 737 public interface OnActivatedListener { onActivated(ActivatableNotificationView view)738 void onActivated(ActivatableNotificationView view); onActivationReset(ActivatableNotificationView view)739 void onActivationReset(ActivatableNotificationView view); 740 } 741 } 742