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