ActivatableNotificationView.java revision c9c00ae2fa5fb787e9f12705f8cd8de445ecde4b
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.util.AttributeSet; 25import android.view.MotionEvent; 26import android.view.View; 27import android.view.ViewConfiguration; 28import android.view.animation.AnimationUtils; 29import android.view.animation.Interpolator; 30import android.view.animation.PathInterpolator; 31 32import com.android.systemui.R; 33 34/** 35 * Base class for both {@link ExpandableNotificationRow} and {@link NotificationOverflowContainer} 36 * to implement dimming/activating on Keyguard for the double-tap gesture 37 */ 38public abstract class ActivatableNotificationView extends ExpandableOutlineView { 39 40 private static final long DOUBLETAP_TIMEOUT_MS = 1200; 41 private static final int BACKGROUND_ANIMATION_LENGTH_MS = 220; 42 private static final int ACTIVATE_ANIMATION_LENGTH = 220; 43 44 private static final Interpolator ACTIVATE_INVERSE_INTERPOLATOR 45 = new PathInterpolator(0.6f, 0, 0.5f, 1); 46 private static final Interpolator ACTIVATE_INVERSE_ALPHA_INTERPOLATOR 47 = new PathInterpolator(0, 0, 0.5f, 1); 48 private final int mMaxNotificationHeight; 49 50 private boolean mDimmed; 51 52 private int mBgResId = com.android.internal.R.drawable.notification_quantum_bg; 53 private int mDimmedBgResId = com.android.internal.R.drawable.notification_quantum_bg_dim; 54 55 private int mBgTint = 0; 56 private int mDimmedBgTint = 0; 57 58 /** 59 * Flag to indicate that the notification has been touched once and the second touch will 60 * click it. 61 */ 62 private boolean mActivated; 63 64 private float mDownX; 65 private float mDownY; 66 private final float mTouchSlop; 67 68 private OnActivatedListener mOnActivatedListener; 69 70 private Interpolator mLinearOutSlowInInterpolator; 71 private Interpolator mFastOutSlowInInterpolator; 72 73 private NotificationBackgroundView mBackgroundNormal; 74 private NotificationBackgroundView mBackgroundDimmed; 75 private ObjectAnimator mBackgroundAnimator; 76 77 public ActivatableNotificationView(Context context, AttributeSet attrs) { 78 super(context, attrs); 79 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 80 mFastOutSlowInInterpolator = 81 AnimationUtils.loadInterpolator(context, android.R.interpolator.fast_out_slow_in); 82 mLinearOutSlowInInterpolator = 83 AnimationUtils.loadInterpolator(context, android.R.interpolator.linear_out_slow_in); 84 mMaxNotificationHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); 85 setClipChildren(false); 86 setClipToPadding(false); 87 } 88 89 @Override 90 protected void onFinishInflate() { 91 super.onFinishInflate(); 92 mBackgroundNormal = (NotificationBackgroundView) findViewById(R.id.backgroundNormal); 93 mBackgroundDimmed = (NotificationBackgroundView) findViewById(R.id.backgroundDimmed); 94 updateBackgroundResource(); 95 } 96 97 private final Runnable mTapTimeoutRunnable = new Runnable() { 98 @Override 99 public void run() { 100 makeInactive(); 101 } 102 }; 103 104 @Override 105 public boolean onTouchEvent(MotionEvent event) { 106 if (mDimmed) { 107 return handleTouchEventDimmed(event); 108 } else { 109 return super.onTouchEvent(event); 110 } 111 } 112 113 private boolean handleTouchEventDimmed(MotionEvent event) { 114 int action = event.getActionMasked(); 115 switch (action) { 116 case MotionEvent.ACTION_DOWN: 117 mDownX = event.getX(); 118 mDownY = event.getY(); 119 if (mDownY > getActualHeight()) { 120 return false; 121 } 122 break; 123 case MotionEvent.ACTION_MOVE: 124 if (!isWithinTouchSlop(event)) { 125 makeInactive(); 126 return false; 127 } 128 break; 129 case MotionEvent.ACTION_UP: 130 if (isWithinTouchSlop(event)) { 131 if (!mActivated) { 132 makeActive(); 133 postDelayed(mTapTimeoutRunnable, DOUBLETAP_TIMEOUT_MS); 134 } else { 135 performClick(); 136 } 137 } else { 138 makeInactive(); 139 } 140 break; 141 case MotionEvent.ACTION_CANCEL: 142 makeInactive(); 143 break; 144 default: 145 break; 146 } 147 return true; 148 } 149 150 private void makeActive() { 151 startActivateAnimation(false /* reverse */); 152 mActivated = true; 153 if (mOnActivatedListener != null) { 154 mOnActivatedListener.onActivated(this); 155 } 156 } 157 158 private void startActivateAnimation(boolean reverse) { 159 int widthHalf = mBackgroundNormal.getWidth()/2; 160 int heightHalf = mBackgroundNormal.getActualHeight()/2; 161 float radius = (float) Math.sqrt(widthHalf*widthHalf + heightHalf*heightHalf); 162 ValueAnimator animator = 163 mBackgroundNormal.createRevealAnimator(widthHalf, heightHalf, 0, radius); 164 mBackgroundNormal.setVisibility(View.VISIBLE); 165 Interpolator interpolator; 166 Interpolator alphaInterpolator; 167 if (!reverse) { 168 interpolator = mLinearOutSlowInInterpolator; 169 alphaInterpolator = mLinearOutSlowInInterpolator; 170 } else { 171 interpolator = ACTIVATE_INVERSE_INTERPOLATOR; 172 alphaInterpolator = ACTIVATE_INVERSE_ALPHA_INTERPOLATOR; 173 } 174 animator.setInterpolator(interpolator); 175 animator.setDuration(ACTIVATE_ANIMATION_LENGTH); 176 if (reverse) { 177 mBackgroundNormal.setAlpha(1f); 178 animator.addListener(new AnimatorListenerAdapter() { 179 @Override 180 public void onAnimationEnd(Animator animation) { 181 mBackgroundNormal.setVisibility(View.INVISIBLE); 182 } 183 }); 184 animator.reverse(); 185 } else { 186 mBackgroundNormal.setAlpha(0.4f); 187 animator.start(); 188 } 189 mBackgroundNormal.animate() 190 .alpha(reverse ? 0f : 1f) 191 .setInterpolator(alphaInterpolator) 192 .setDuration(ACTIVATE_ANIMATION_LENGTH); 193 } 194 195 /** 196 * Cancels the hotspot and makes the notification inactive. 197 */ 198 private void makeInactive() { 199 if (mActivated) { 200 if (mDimmed) { 201 startActivateAnimation(true /* reverse */); 202 } 203 mActivated = false; 204 } 205 if (mOnActivatedListener != null) { 206 mOnActivatedListener.onActivationReset(this); 207 } 208 removeCallbacks(mTapTimeoutRunnable); 209 } 210 211 private boolean isWithinTouchSlop(MotionEvent event) { 212 return Math.abs(event.getX() - mDownX) < mTouchSlop 213 && Math.abs(event.getY() - mDownY) < mTouchSlop; 214 } 215 216 public void setDimmed(boolean dimmed, boolean fade) { 217 if (mDimmed != dimmed) { 218 mDimmed = dimmed; 219 if (fade) { 220 fadeBackgroundResource(); 221 } else { 222 updateBackgroundResource(); 223 } 224 } 225 } 226 227 /** 228 * Sets the resource id for the background of this notification. 229 * 230 * @param bgResId The background resource to use in normal state. 231 * @param dimmedBgResId The background resource to use in dimmed state. 232 */ 233 public void setBackgroundResourceIds(int bgResId, int bgTint, int dimmedBgResId, int dimmedTint) { 234 mBgResId = bgResId; 235 mBgTint = bgTint; 236 mDimmedBgResId = dimmedBgResId; 237 mDimmedBgTint = dimmedTint; 238 updateBackgroundResource(); 239 } 240 241 public void setBackgroundResourceIds(int bgResId, int dimmedBgResId) { 242 setBackgroundResourceIds(bgResId, 0, dimmedBgResId, 0); 243 } 244 245 private void fadeBackgroundResource() { 246 if (mDimmed) { 247 mBackgroundDimmed.setVisibility(View.VISIBLE); 248 } else { 249 mBackgroundNormal.setVisibility(View.VISIBLE); 250 } 251 float startAlpha = mDimmed ? 1f : 0; 252 float endAlpha = mDimmed ? 0 : 1f; 253 int duration = BACKGROUND_ANIMATION_LENGTH_MS; 254 // Check whether there is already a background animation running. 255 if (mBackgroundAnimator != null) { 256 startAlpha = (Float) mBackgroundAnimator.getAnimatedValue(); 257 duration = (int) mBackgroundAnimator.getCurrentPlayTime(); 258 mBackgroundAnimator.removeAllListeners(); 259 mBackgroundAnimator.cancel(); 260 if (duration <= 0) { 261 updateBackgroundResource(); 262 return; 263 } 264 } 265 mBackgroundNormal.setAlpha(startAlpha); 266 mBackgroundAnimator = 267 ObjectAnimator.ofFloat(mBackgroundNormal, View.ALPHA, startAlpha, endAlpha); 268 mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator); 269 mBackgroundAnimator.setDuration(duration); 270 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 271 @Override 272 public void onAnimationEnd(Animator animation) { 273 if (mDimmed) { 274 mBackgroundNormal.setVisibility(View.INVISIBLE); 275 } else { 276 mBackgroundDimmed.setVisibility(View.INVISIBLE); 277 } 278 mBackgroundAnimator = null; 279 } 280 }); 281 mBackgroundAnimator.start(); 282 } 283 284 private void updateBackgroundResource() { 285 if (mDimmed) { 286 mBackgroundDimmed.setVisibility(View.VISIBLE); 287 mBackgroundDimmed.setCustomBackground(mDimmedBgResId, mDimmedBgTint); 288 mBackgroundNormal.setVisibility(View.INVISIBLE); 289 } else { 290 mBackgroundDimmed.setVisibility(View.INVISIBLE); 291 mBackgroundNormal.setVisibility(View.VISIBLE); 292 mBackgroundNormal.setCustomBackground(mBgResId, mBgTint); 293 mBackgroundNormal.setAlpha(1f); 294 } 295 } 296 297 @Override 298 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 299 int newHeightSpec = MeasureSpec.makeMeasureSpec(mMaxNotificationHeight, 300 MeasureSpec.AT_MOST); 301 int maxChildHeight = 0; 302 int childCount = getChildCount(); 303 for (int i = 0; i < childCount; i++) { 304 View child = getChildAt(i); 305 if (child != mBackgroundDimmed && child != mBackgroundNormal) { 306 child.measure(widthMeasureSpec, newHeightSpec); 307 int childHeight = child.getMeasuredHeight(); 308 maxChildHeight = Math.max(maxChildHeight, childHeight); 309 } 310 } 311 newHeightSpec = MeasureSpec.makeMeasureSpec(maxChildHeight, MeasureSpec.EXACTLY); 312 mBackgroundDimmed.measure(widthMeasureSpec, newHeightSpec); 313 mBackgroundNormal.measure(widthMeasureSpec, newHeightSpec); 314 int width = MeasureSpec.getSize(widthMeasureSpec); 315 setMeasuredDimension(width, maxChildHeight); 316 } 317 318 @Override 319 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 320 super.onLayout(changed, left, top, right, bottom); 321 setPivotX(getWidth() / 2); 322 } 323 324 @Override 325 public void setActualHeight(int actualHeight, boolean notifyListeners) { 326 super.setActualHeight(actualHeight, notifyListeners); 327 setPivotY(actualHeight / 2); 328 mBackgroundNormal.setActualHeight(actualHeight); 329 mBackgroundDimmed.setActualHeight(actualHeight); 330 } 331 332 @Override 333 public void setClipTopAmount(int clipTopAmount) { 334 super.setClipTopAmount(clipTopAmount); 335 mBackgroundNormal.setClipTopAmount(clipTopAmount); 336 mBackgroundDimmed.setClipTopAmount(clipTopAmount); 337 } 338 339 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 340 mOnActivatedListener = onActivatedListener; 341 } 342 343 public interface OnActivatedListener { 344 void onActivated(View view); 345 void onActivationReset(View view); 346 } 347} 348