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