KeyButtonView.java revision 42310965fa2c0d2c91bea0a76730a21f6dd308a2
1/* 2 * Copyright (C) 2008 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.policy; 18 19import android.animation.Animator; 20import android.animation.AnimatorSet; 21import android.animation.ObjectAnimator; 22import android.animation.TimeInterpolator; 23import android.app.ActivityManager; 24import android.content.Context; 25import android.content.res.TypedArray; 26import android.graphics.Canvas; 27import android.graphics.Paint; 28import android.graphics.RectF; 29import android.hardware.input.InputManager; 30import android.media.AudioManager; 31import android.os.Bundle; 32import android.os.SystemClock; 33import android.util.AttributeSet; 34import android.util.Log; 35import android.view.HapticFeedbackConstants; 36import android.view.InputDevice; 37import android.view.KeyCharacterMap; 38import android.view.KeyEvent; 39import android.view.MotionEvent; 40import android.view.SoundEffectConstants; 41import android.view.View; 42import android.view.ViewConfiguration; 43import android.view.accessibility.AccessibilityEvent; 44import android.view.accessibility.AccessibilityNodeInfo; 45import android.widget.ImageView; 46import java.lang.Math; 47 48import com.android.systemui.R; 49 50import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; 51import static android.view.accessibility.AccessibilityNodeInfo.ACTION_LONG_CLICK; 52 53public class KeyButtonView extends ImageView { 54 private static final String TAG = "StatusBar.KeyButtonView"; 55 private static final boolean DEBUG = false; 56 57 // TODO: Get rid of this 58 public static final float DEFAULT_QUIESCENT_ALPHA = 1f; 59 public static final float MAX_ALPHA = 0.15f; 60 public static final float GLOW_MAX_SCALE_FACTOR = 1.5f; 61 62 private long mDownTime; 63 private int mCode; 64 private int mTouchSlop; 65 private float mGlowAlpha = 0f; 66 private float mGlowScale = 1f; 67 private float mDrawingAlpha = 1f; 68 private float mQuiescentAlpha = DEFAULT_QUIESCENT_ALPHA; 69 private boolean mSupportsLongpress = true; 70 private AnimatorSet mPressedAnim; 71 private Animator mAnimateToQuiescent = new ObjectAnimator(); 72 private Paint mRipplePaint; 73 private final TimeInterpolator mInterpolator = (TimeInterpolator) new LogInterpolator(); 74 private AudioManager mAudioManager; 75 76 private final Runnable mCheckLongPress = new Runnable() { 77 public void run() { 78 if (isPressed()) { 79 // Log.d("KeyButtonView", "longpressed: " + this); 80 if (isLongClickable()) { 81 // Just an old-fashioned ImageView 82 performLongClick(); 83 } else { 84 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); 85 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 86 } 87 } 88 } 89 }; 90 91 public KeyButtonView(Context context, AttributeSet attrs) { 92 this(context, attrs, 0); 93 } 94 95 public KeyButtonView(Context context, AttributeSet attrs, int defStyle) { 96 super(context, attrs); 97 98 TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.KeyButtonView, 99 defStyle, 0); 100 101 mCode = a.getInteger(R.styleable.KeyButtonView_keyCode, 0); 102 103 mSupportsLongpress = a.getBoolean(R.styleable.KeyButtonView_keyRepeat, true); 104 105 106 setDrawingAlpha(mQuiescentAlpha); 107 108 a.recycle(); 109 110 setClickable(true); 111 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); 112 mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); 113 } 114 115 @Override 116 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 117 super.onInitializeAccessibilityNodeInfo(info); 118 if (mCode != 0) { 119 info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, null)); 120 if (mSupportsLongpress) { 121 info.addAction( 122 new AccessibilityNodeInfo.AccessibilityAction(ACTION_LONG_CLICK, null)); 123 } 124 } 125 } 126 127 @Override 128 public boolean performAccessibilityAction(int action, Bundle arguments) { 129 if (action == ACTION_CLICK && mCode != 0) { 130 sendEvent(KeyEvent.ACTION_DOWN, 0, SystemClock.uptimeMillis()); 131 sendEvent(KeyEvent.ACTION_UP, 0); 132 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 133 playSoundEffect(SoundEffectConstants.CLICK); 134 return true; 135 } else if (action == ACTION_LONG_CLICK && mCode != 0 && mSupportsLongpress) { 136 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.FLAG_LONG_PRESS); 137 sendEvent(KeyEvent.ACTION_UP, 0); 138 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); 139 return true; 140 } 141 return super.performAccessibilityAction(action, arguments); 142 } 143 144 private Paint getRipplePaint() { 145 if (mRipplePaint == null) { 146 mRipplePaint = new Paint(); 147 mRipplePaint.setAntiAlias(true); 148 mRipplePaint.setColor(0xffffffff); 149 } 150 return mRipplePaint; 151 } 152 153 @Override 154 protected void onDraw(Canvas canvas) { 155 final Paint p = getRipplePaint(); 156 p.setAlpha((int)(MAX_ALPHA * mDrawingAlpha * mGlowAlpha * 255)); 157 158 final float w = getWidth(); 159 final float h = getHeight(); 160 final boolean horizontal = w > h; 161 final float diameter = (horizontal ? w : h) * mGlowScale; 162 final float radius = diameter * .5f; 163 final float cx = w * .5f; 164 final float cy = h * .5f; 165 final float rx = horizontal ? radius : cx; 166 final float ry = horizontal ? cy : radius; 167 final float corner = horizontal ? cy : cx; 168 169 canvas.drawRoundRect(cx - rx, cy - ry, 170 cx + rx, cy + ry, 171 corner, corner, p); 172 173 super.onDraw(canvas); 174 } 175 176 public void setQuiescentAlpha(float alpha, boolean animate) { 177 mAnimateToQuiescent.cancel(); 178 alpha = Math.min(Math.max(alpha, 0), 1); 179 if (alpha == mQuiescentAlpha && alpha == mDrawingAlpha) return; 180 mQuiescentAlpha = alpha; 181 if (DEBUG) Log.d(TAG, "New quiescent alpha = " + mQuiescentAlpha); 182 if (animate) { 183 mAnimateToQuiescent = animateToQuiescent(); 184 mAnimateToQuiescent.start(); 185 } else { 186 setDrawingAlpha(mQuiescentAlpha); 187 } 188 } 189 190 private ObjectAnimator animateToQuiescent() { 191 return ObjectAnimator.ofFloat(this, "drawingAlpha", mQuiescentAlpha); 192 } 193 194 public float getQuiescentAlpha() { 195 return mQuiescentAlpha; 196 } 197 198 public float getDrawingAlpha() { 199 return mDrawingAlpha; 200 } 201 202 public void setDrawingAlpha(float x) { 203 setImageAlpha((int) (x * 255)); 204 mDrawingAlpha = x; 205 } 206 207 public float getGlowAlpha() { 208 return mGlowAlpha; 209 } 210 211 public void setGlowAlpha(float x) { 212 mGlowAlpha = x; 213 invalidate(); 214 } 215 216 public float getGlowScale() { 217 return mGlowScale; 218 } 219 220 public void setGlowScale(float x) { 221 mGlowScale = x; 222 final float w = getWidth(); 223 final float h = getHeight(); 224 if (GLOW_MAX_SCALE_FACTOR <= 1.0f) { 225 // this only works if we know the glow will never leave our bounds 226 invalidate(); 227 } else { 228 final float rx = (w * (GLOW_MAX_SCALE_FACTOR - 1.0f)) / 2.0f + 1.0f; 229 final float ry = (h * (GLOW_MAX_SCALE_FACTOR - 1.0f)) / 2.0f + 1.0f; 230 com.android.systemui.SwipeHelper.invalidateGlobalRegion( 231 this, 232 new RectF(getLeft() - rx, 233 getTop() - ry, 234 getRight() + rx, 235 getBottom() + ry)); 236 237 // also invalidate our immediate parent to help avoid situations where nearby glows 238 // interfere 239 ((View)getParent()).invalidate(); 240 } 241 } 242 243 public void setPressed(boolean pressed) { 244 if (pressed != isPressed()) { 245 if (mPressedAnim != null && mPressedAnim.isRunning()) { 246 mPressedAnim.cancel(); 247 } 248 final AnimatorSet as = mPressedAnim = new AnimatorSet(); 249 final ObjectAnimator scaleAnimator = ObjectAnimator.ofFloat(this, 250 "glowScale", GLOW_MAX_SCALE_FACTOR); 251 scaleAnimator.setInterpolator(mInterpolator); 252 if (pressed) { 253 mGlowScale = 0f; 254 if (mGlowAlpha < mQuiescentAlpha) 255 mGlowAlpha = mQuiescentAlpha; 256 setDrawingAlpha(1f); 257 as.playTogether( 258 ObjectAnimator.ofFloat(this, "glowAlpha", 1f), 259 scaleAnimator 260 ); 261 as.setDuration(500); 262 } else { 263 mAnimateToQuiescent.cancel(); 264 mAnimateToQuiescent = animateToQuiescent(); 265 as.playTogether( 266 ObjectAnimator.ofFloat(this, "glowAlpha", mGlowAlpha, mGlowAlpha * .2f, 0f), 267 scaleAnimator, 268 mAnimateToQuiescent 269 ); 270 as.setDuration(500); 271 } 272 as.start(); 273 } 274 super.setPressed(pressed); 275 } 276 277 public boolean onTouchEvent(MotionEvent ev) { 278 final int action = ev.getAction(); 279 int x, y; 280 281 switch (action) { 282 case MotionEvent.ACTION_DOWN: 283 //Log.d("KeyButtonView", "press"); 284 mDownTime = SystemClock.uptimeMillis(); 285 setPressed(true); 286 if (mCode != 0) { 287 sendEvent(KeyEvent.ACTION_DOWN, 0, mDownTime); 288 } else { 289 // Provide the same haptic feedback that the system offers for virtual keys. 290 performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY); 291 } 292 if (mSupportsLongpress) { 293 removeCallbacks(mCheckLongPress); 294 postDelayed(mCheckLongPress, ViewConfiguration.getLongPressTimeout()); 295 } 296 break; 297 case MotionEvent.ACTION_MOVE: 298 x = (int)ev.getX(); 299 y = (int)ev.getY(); 300 setPressed(x >= -mTouchSlop 301 && x < getWidth() + mTouchSlop 302 && y >= -mTouchSlop 303 && y < getHeight() + mTouchSlop); 304 break; 305 case MotionEvent.ACTION_CANCEL: 306 setPressed(false); 307 if (mCode != 0) { 308 sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); 309 } 310 if (mSupportsLongpress) { 311 removeCallbacks(mCheckLongPress); 312 } 313 break; 314 case MotionEvent.ACTION_UP: 315 final boolean doIt = isPressed(); 316 setPressed(false); 317 if (mCode != 0) { 318 if (doIt) { 319 sendEvent(KeyEvent.ACTION_UP, 0); 320 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 321 playSoundEffect(SoundEffectConstants.CLICK); 322 } else { 323 sendEvent(KeyEvent.ACTION_UP, KeyEvent.FLAG_CANCELED); 324 } 325 } else { 326 // no key code, just a regular ImageView 327 if (doIt) { 328 performClick(); 329 } 330 } 331 if (mSupportsLongpress) { 332 removeCallbacks(mCheckLongPress); 333 } 334 break; 335 } 336 337 return true; 338 } 339 340 public void playSoundEffect(int soundConstant) { 341 mAudioManager.playSoundEffect(soundConstant, ActivityManager.getCurrentUser()); 342 }; 343 344 public void sendEvent(int action, int flags) { 345 sendEvent(action, flags, SystemClock.uptimeMillis()); 346 } 347 348 void sendEvent(int action, int flags, long when) { 349 final int repeatCount = (flags & KeyEvent.FLAG_LONG_PRESS) != 0 ? 1 : 0; 350 final KeyEvent ev = new KeyEvent(mDownTime, when, action, mCode, repeatCount, 351 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 352 flags | KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, 353 InputDevice.SOURCE_KEYBOARD); 354 InputManager.getInstance().injectInputEvent(ev, 355 InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); 356 } 357 358 /** 359 * Interpolator with a smooth log deceleration 360 */ 361 private static final class LogInterpolator implements TimeInterpolator { 362 @Override 363 public float getInterpolation(float input) { 364 return 1 - (float) Math.pow(400, -input * 1.4); 365 } 366 } 367 368} 369 370 371