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