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