ActivatableNotificationView.java revision 4222d9a7fb87d73e1443ec1a2de9782b05741af6
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) (NotificationActivator.ANIMATION_LENGTH_MS 234 - mBackgroundAnimator.getCurrentPlayTime()); 235 mBackgroundAnimator.removeAllListeners(); 236 mBackgroundAnimator.cancel(); 237 } 238 mBackgroundNormal.setAlpha(startAlpha); 239 mBackgroundAnimator = 240 ObjectAnimator.ofInt(mBackgroundNormal, "alpha", startAlpha, endAlpha); 241 mBackgroundAnimator.setInterpolator(mFastOutSlowInInterpolator); 242 mBackgroundAnimator.setDuration(duration); 243 mBackgroundAnimator.addListener(new AnimatorListenerAdapter() { 244 @Override 245 public void onAnimationEnd(Animator animation) { 246 if (mDimmed) { 247 setBackgroundNormal(null); 248 } else { 249 setBackgroundDimmed(null); 250 } 251 mBackgroundAnimator = null; 252 } 253 }); 254 mBackgroundAnimator.start(); 255 } 256 257 private void updateBackgroundResource() { 258 if (mDimmed) { 259 setBackgroundDimmed(mDimmedBgResId); 260 setBackgroundNormal(null); 261 } else { 262 setBackgroundDimmed(null); 263 setBackgroundNormal(mBgResId); 264 } 265 } 266 267 /** 268 * Sets a background drawable for the normal state. As we need to change our bounds 269 * independently of layout, we need the notion of a background independently of the regular View 270 * background.. 271 */ 272 private void setBackgroundNormal(Drawable backgroundNormal) { 273 if (mBackgroundNormal != null) { 274 mBackgroundNormal.setCallback(null); 275 unscheduleDrawable(mBackgroundNormal); 276 } 277 mBackgroundNormal = backgroundNormal; 278 if (mBackgroundNormal != null) { 279 mBackgroundNormal.setCallback(this); 280 } 281 invalidate(); 282 } 283 284 private void setBackgroundDimmed(Drawable overlay) { 285 if (mBackgroundDimmed != null) { 286 mBackgroundDimmed.setCallback(null); 287 unscheduleDrawable(mBackgroundDimmed); 288 } 289 mBackgroundDimmed = overlay; 290 if (mBackgroundDimmed != null) { 291 mBackgroundDimmed.setCallback(this); 292 } 293 invalidate(); 294 } 295 296 private void setBackgroundNormal(int drawableResId) { 297 setBackgroundNormal(getResources().getDrawable(drawableResId)); 298 } 299 300 private void setBackgroundDimmed(int drawableResId) { 301 setBackgroundDimmed(getResources().getDrawable(drawableResId)); 302 } 303 304 @Override 305 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 306 super.onLayout(changed, left, top, right, bottom); 307 setPivotX(getWidth() / 2); 308 } 309 310 @Override 311 public void setActualHeight(int actualHeight) { 312 super.setActualHeight(actualHeight); 313 invalidate(); 314 setPivotY(actualHeight / 2); 315 } 316 317 @Override 318 public void setClipTopAmount(int clipTopAmount) { 319 super.setClipTopAmount(clipTopAmount); 320 invalidate(); 321 } 322 323 public void setOnActivatedListener(OnActivatedListener onActivatedListener) { 324 mOnActivatedListener = onActivatedListener; 325 } 326 327 public interface OnActivatedListener { 328 void onActivated(View view); 329 void onReset(View view); 330 } 331} 332