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