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