1 /* 2 * Copyright (C) 2010 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 17 package com.android.internal.widget; 18 19 import java.util.ArrayList; 20 21 import android.animation.ValueAnimator; 22 import android.content.Context; 23 import android.content.res.Resources; 24 import android.graphics.Bitmap; 25 import android.graphics.BitmapFactory; 26 import android.graphics.Canvas; 27 import android.graphics.drawable.BitmapDrawable; 28 import android.media.AudioAttributes; 29 import android.os.UserHandle; 30 import android.os.Vibrator; 31 import android.provider.Settings; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.view.MotionEvent; 35 import android.view.View; 36 import android.view.accessibility.AccessibilityEvent; 37 import android.view.accessibility.AccessibilityManager; 38 39 import com.android.internal.R; 40 41 /** 42 * A special widget containing a center and outer ring. Moving the center ring to the outer ring 43 * causes an event that can be caught by implementing OnTriggerListener. 44 */ 45 public class WaveView extends View implements ValueAnimator.AnimatorUpdateListener { 46 private static final String TAG = "WaveView"; 47 private static final boolean DBG = false; 48 private static final int WAVE_COUNT = 20; // default wave count 49 private static final long VIBRATE_SHORT = 20; // msec 50 private static final long VIBRATE_LONG = 20; // msec 51 52 // Lock state machine states 53 private static final int STATE_RESET_LOCK = 0; 54 private static final int STATE_READY = 1; 55 private static final int STATE_START_ATTEMPT = 2; 56 private static final int STATE_ATTEMPTING = 3; 57 private static final int STATE_UNLOCK_ATTEMPT = 4; 58 private static final int STATE_UNLOCK_SUCCESS = 5; 59 60 // Animation properties. 61 private static final long DURATION = 300; // duration of transitional animations 62 private static final long FINAL_DURATION = 200; // duration of final animations when unlocking 63 private static final long RING_DELAY = 1300; // when to start fading animated rings 64 private static final long FINAL_DELAY = 200; // delay for unlock success animation 65 private static final long SHORT_DELAY = 100; // for starting one animation after another. 66 private static final long WAVE_DURATION = 2000; // amount of time for way to expand/decay 67 private static final long RESET_TIMEOUT = 3000; // elapsed time of inactivity before we reset 68 private static final long DELAY_INCREMENT = 15; // increment per wave while tracking motion 69 private static final long DELAY_INCREMENT2 = 12; // increment per wave while not tracking 70 private static final long WAVE_DELAY = WAVE_DURATION / WAVE_COUNT; // initial propagation delay 71 72 /** 73 * The scale by which to multiply the unlock handle width to compute the radius 74 * in which it can be grabbed when accessibility is disabled. 75 */ 76 private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED = 0.5f; 77 78 /** 79 * The scale by which to multiply the unlock handle width to compute the radius 80 * in which it can be grabbed when accessibility is enabled (more generous). 81 */ 82 private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.0f; 83 84 private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder() 85 .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) 86 .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION) 87 .build(); 88 89 private Vibrator mVibrator; 90 private OnTriggerListener mOnTriggerListener; 91 private ArrayList<DrawableHolder> mDrawables = new ArrayList<DrawableHolder>(3); 92 private ArrayList<DrawableHolder> mLightWaves = new ArrayList<DrawableHolder>(WAVE_COUNT); 93 private boolean mFingerDown = false; 94 private float mRingRadius = 182.0f; // Radius of bitmap ring. Used to snap halo to it 95 private int mSnapRadius = 136; // minimum threshold for drag unlock 96 private int mWaveCount = WAVE_COUNT; // number of waves 97 private long mWaveTimerDelay = WAVE_DELAY; 98 private int mCurrentWave = 0; 99 private float mLockCenterX; // center of widget as dictated by widget size 100 private float mLockCenterY; 101 private float mMouseX; // current mouse position as of last touch event 102 private float mMouseY; 103 private DrawableHolder mUnlockRing; 104 private DrawableHolder mUnlockDefault; 105 private DrawableHolder mUnlockHalo; 106 private int mLockState = STATE_RESET_LOCK; 107 private int mGrabbedState = OnTriggerListener.NO_HANDLE; 108 private boolean mWavesRunning; 109 private boolean mFinishWaves; 110 111 public WaveView(Context context) { 112 this(context, null); 113 } 114 115 public WaveView(Context context, AttributeSet attrs) { 116 super(context, attrs); 117 118 // TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveView); 119 // mOrientation = a.getInt(R.styleable.WaveView_orientation, HORIZONTAL); 120 // a.recycle(); 121 122 initDrawables(); 123 } 124 125 @Override 126 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 127 mLockCenterX = 0.5f * w; 128 mLockCenterY = 0.5f * h; 129 super.onSizeChanged(w, h, oldw, oldh); 130 } 131 132 @Override 133 protected int getSuggestedMinimumWidth() { 134 // View should be large enough to contain the unlock ring + halo 135 return mUnlockRing.getWidth() + mUnlockHalo.getWidth(); 136 } 137 138 @Override 139 protected int getSuggestedMinimumHeight() { 140 // View should be large enough to contain the unlock ring + halo 141 return mUnlockRing.getHeight() + mUnlockHalo.getHeight(); 142 } 143 144 @Override 145 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 146 int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); 147 int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); 148 int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec); 149 int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec); 150 int width; 151 int height; 152 153 if (widthSpecMode == MeasureSpec.AT_MOST) { 154 width = Math.min(widthSpecSize, getSuggestedMinimumWidth()); 155 } else if (widthSpecMode == MeasureSpec.EXACTLY) { 156 width = widthSpecSize; 157 } else { 158 width = getSuggestedMinimumWidth(); 159 } 160 161 if (heightSpecMode == MeasureSpec.AT_MOST) { 162 height = Math.min(heightSpecSize, getSuggestedMinimumWidth()); 163 } else if (heightSpecMode == MeasureSpec.EXACTLY) { 164 height = heightSpecSize; 165 } else { 166 height = getSuggestedMinimumHeight(); 167 } 168 169 setMeasuredDimension(width, height); 170 } 171 172 private void initDrawables() { 173 mUnlockRing = new DrawableHolder(createDrawable(R.drawable.unlock_ring)); 174 mUnlockRing.setX(mLockCenterX); 175 mUnlockRing.setY(mLockCenterY); 176 mUnlockRing.setScaleX(0.1f); 177 mUnlockRing.setScaleY(0.1f); 178 mUnlockRing.setAlpha(0.0f); 179 mDrawables.add(mUnlockRing); 180 181 mUnlockDefault = new DrawableHolder(createDrawable(R.drawable.unlock_default)); 182 mUnlockDefault.setX(mLockCenterX); 183 mUnlockDefault.setY(mLockCenterY); 184 mUnlockDefault.setScaleX(0.1f); 185 mUnlockDefault.setScaleY(0.1f); 186 mUnlockDefault.setAlpha(0.0f); 187 mDrawables.add(mUnlockDefault); 188 189 mUnlockHalo = new DrawableHolder(createDrawable(R.drawable.unlock_halo)); 190 mUnlockHalo.setX(mLockCenterX); 191 mUnlockHalo.setY(mLockCenterY); 192 mUnlockHalo.setScaleX(0.1f); 193 mUnlockHalo.setScaleY(0.1f); 194 mUnlockHalo.setAlpha(0.0f); 195 mDrawables.add(mUnlockHalo); 196 197 BitmapDrawable wave = createDrawable(R.drawable.unlock_wave); 198 for (int i = 0; i < mWaveCount; i++) { 199 DrawableHolder holder = new DrawableHolder(wave); 200 mLightWaves.add(holder); 201 holder.setAlpha(0.0f); 202 } 203 } 204 205 private void waveUpdateFrame(float mouseX, float mouseY, boolean fingerDown) { 206 double distX = mouseX - mLockCenterX; 207 double distY = mouseY - mLockCenterY; 208 int dragDistance = (int) Math.ceil(Math.hypot(distX, distY)); 209 double touchA = Math.atan2(distX, distY); 210 float ringX = (float) (mLockCenterX + mRingRadius * Math.sin(touchA)); 211 float ringY = (float) (mLockCenterY + mRingRadius * Math.cos(touchA)); 212 213 switch (mLockState) { 214 case STATE_RESET_LOCK: 215 if (DBG) Log.v(TAG, "State RESET_LOCK"); 216 mWaveTimerDelay = WAVE_DELAY; 217 for (int i = 0; i < mLightWaves.size(); i++) { 218 DrawableHolder holder = mLightWaves.get(i); 219 holder.addAnimTo(300, 0, "alpha", 0.0f, false); 220 } 221 for (int i = 0; i < mLightWaves.size(); i++) { 222 mLightWaves.get(i).startAnimations(this); 223 } 224 225 mUnlockRing.addAnimTo(DURATION, 0, "x", mLockCenterX, true); 226 mUnlockRing.addAnimTo(DURATION, 0, "y", mLockCenterY, true); 227 mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 0.1f, true); 228 mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 0.1f, true); 229 mUnlockRing.addAnimTo(DURATION, 0, "alpha", 0.0f, true); 230 231 mUnlockDefault.removeAnimationFor("x"); 232 mUnlockDefault.removeAnimationFor("y"); 233 mUnlockDefault.removeAnimationFor("scaleX"); 234 mUnlockDefault.removeAnimationFor("scaleY"); 235 mUnlockDefault.removeAnimationFor("alpha"); 236 mUnlockDefault.setX(mLockCenterX); 237 mUnlockDefault.setY(mLockCenterY); 238 mUnlockDefault.setScaleX(0.1f); 239 mUnlockDefault.setScaleY(0.1f); 240 mUnlockDefault.setAlpha(0.0f); 241 mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true); 242 mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true); 243 mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true); 244 245 mUnlockHalo.removeAnimationFor("x"); 246 mUnlockHalo.removeAnimationFor("y"); 247 mUnlockHalo.removeAnimationFor("scaleX"); 248 mUnlockHalo.removeAnimationFor("scaleY"); 249 mUnlockHalo.removeAnimationFor("alpha"); 250 mUnlockHalo.setX(mLockCenterX); 251 mUnlockHalo.setY(mLockCenterY); 252 mUnlockHalo.setScaleX(0.1f); 253 mUnlockHalo.setScaleY(0.1f); 254 mUnlockHalo.setAlpha(0.0f); 255 mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "x", mLockCenterX, true); 256 mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "y", mLockCenterY, true); 257 mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true); 258 mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true); 259 mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true); 260 261 removeCallbacks(mLockTimerActions); 262 263 mLockState = STATE_READY; 264 break; 265 266 case STATE_READY: 267 if (DBG) Log.v(TAG, "State READY"); 268 mWaveTimerDelay = WAVE_DELAY; 269 break; 270 271 case STATE_START_ATTEMPT: 272 if (DBG) Log.v(TAG, "State START_ATTEMPT"); 273 mUnlockDefault.removeAnimationFor("x"); 274 mUnlockDefault.removeAnimationFor("y"); 275 mUnlockDefault.removeAnimationFor("scaleX"); 276 mUnlockDefault.removeAnimationFor("scaleY"); 277 mUnlockDefault.removeAnimationFor("alpha"); 278 mUnlockDefault.setX(mLockCenterX + 182); 279 mUnlockDefault.setY(mLockCenterY); 280 mUnlockDefault.setScaleX(0.1f); 281 mUnlockDefault.setScaleY(0.1f); 282 mUnlockDefault.setAlpha(0.0f); 283 284 mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, false); 285 mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, false); 286 mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, false); 287 288 mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 1.0f, true); 289 mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 1.0f, true); 290 mUnlockRing.addAnimTo(DURATION, 0, "alpha", 1.0f, true); 291 292 mLockState = STATE_ATTEMPTING; 293 break; 294 295 case STATE_ATTEMPTING: 296 if (DBG) Log.v(TAG, "State ATTEMPTING (fingerDown = " + fingerDown + ")"); 297 if (dragDistance > mSnapRadius) { 298 mFinishWaves = true; // don't start any more waves. 299 if (fingerDown) { 300 mUnlockHalo.addAnimTo(0, 0, "x", ringX, true); 301 mUnlockHalo.addAnimTo(0, 0, "y", ringY, true); 302 mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true); 303 mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true); 304 mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true); 305 } else { 306 if (DBG) Log.v(TAG, "up detected, moving to STATE_UNLOCK_ATTEMPT"); 307 mLockState = STATE_UNLOCK_ATTEMPT; 308 } 309 } else { 310 // If waves have stopped, we need to kick them off again... 311 if (!mWavesRunning) { 312 mWavesRunning = true; 313 mFinishWaves = false; 314 // mWaveTimerDelay = WAVE_DELAY; 315 postDelayed(mAddWaveAction, mWaveTimerDelay); 316 } 317 mUnlockHalo.addAnimTo(0, 0, "x", mouseX, true); 318 mUnlockHalo.addAnimTo(0, 0, "y", mouseY, true); 319 mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true); 320 mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true); 321 mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true); 322 } 323 break; 324 325 case STATE_UNLOCK_ATTEMPT: 326 if (DBG) Log.v(TAG, "State UNLOCK_ATTEMPT"); 327 if (dragDistance > mSnapRadius) { 328 for (int n = 0; n < mLightWaves.size(); n++) { 329 DrawableHolder wave = mLightWaves.get(n); 330 long delay = 1000L*(6 + n - mCurrentWave)/10L; 331 wave.addAnimTo(FINAL_DURATION, delay, "x", ringX, true); 332 wave.addAnimTo(FINAL_DURATION, delay, "y", ringY, true); 333 wave.addAnimTo(FINAL_DURATION, delay, "scaleX", 0.1f, true); 334 wave.addAnimTo(FINAL_DURATION, delay, "scaleY", 0.1f, true); 335 wave.addAnimTo(FINAL_DURATION, delay, "alpha", 0.0f, true); 336 } 337 for (int i = 0; i < mLightWaves.size(); i++) { 338 mLightWaves.get(i).startAnimations(this); 339 } 340 341 mUnlockRing.addAnimTo(FINAL_DURATION, 0, "x", ringX, false); 342 mUnlockRing.addAnimTo(FINAL_DURATION, 0, "y", ringY, false); 343 mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleX", 0.1f, false); 344 mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleY", 0.1f, false); 345 mUnlockRing.addAnimTo(FINAL_DURATION, 0, "alpha", 0.0f, false); 346 347 mUnlockRing.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); 348 349 mUnlockDefault.removeAnimationFor("x"); 350 mUnlockDefault.removeAnimationFor("y"); 351 mUnlockDefault.removeAnimationFor("scaleX"); 352 mUnlockDefault.removeAnimationFor("scaleY"); 353 mUnlockDefault.removeAnimationFor("alpha"); 354 mUnlockDefault.setX(ringX); 355 mUnlockDefault.setY(ringY); 356 mUnlockDefault.setScaleX(0.1f); 357 mUnlockDefault.setScaleY(0.1f); 358 mUnlockDefault.setAlpha(0.0f); 359 360 mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "x", ringX, true); 361 mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "y", ringY, true); 362 mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleX", 1.0f, true); 363 mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleY", 1.0f, true); 364 mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "alpha", 1.0f, true); 365 366 mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false); 367 mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false); 368 mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); 369 370 mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "x", ringX, false); 371 mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "y", ringY, false); 372 373 mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false); 374 mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false); 375 mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false); 376 377 removeCallbacks(mLockTimerActions); 378 379 postDelayed(mLockTimerActions, RESET_TIMEOUT); 380 381 dispatchTriggerEvent(OnTriggerListener.CENTER_HANDLE); 382 mLockState = STATE_UNLOCK_SUCCESS; 383 } else { 384 mLockState = STATE_RESET_LOCK; 385 } 386 break; 387 388 case STATE_UNLOCK_SUCCESS: 389 if (DBG) Log.v(TAG, "State UNLOCK_SUCCESS"); 390 removeCallbacks(mAddWaveAction); 391 break; 392 393 default: 394 if (DBG) Log.v(TAG, "Unknown state " + mLockState); 395 break; 396 } 397 mUnlockDefault.startAnimations(this); 398 mUnlockHalo.startAnimations(this); 399 mUnlockRing.startAnimations(this); 400 } 401 402 BitmapDrawable createDrawable(int resId) { 403 Resources res = getResources(); 404 Bitmap bitmap = BitmapFactory.decodeResource(res, resId); 405 return new BitmapDrawable(res, bitmap); 406 } 407 408 @Override 409 protected void onDraw(Canvas canvas) { 410 waveUpdateFrame(mMouseX, mMouseY, mFingerDown); 411 for (int i = 0; i < mDrawables.size(); ++i) { 412 mDrawables.get(i).draw(canvas); 413 } 414 for (int i = 0; i < mLightWaves.size(); ++i) { 415 mLightWaves.get(i).draw(canvas); 416 } 417 } 418 419 private final Runnable mLockTimerActions = new Runnable() { 420 public void run() { 421 if (DBG) Log.v(TAG, "LockTimerActions"); 422 // reset lock after inactivity 423 if (mLockState == STATE_ATTEMPTING) { 424 if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK"); 425 mLockState = STATE_RESET_LOCK; 426 } 427 // for prototype, reset after successful unlock 428 if (mLockState == STATE_UNLOCK_SUCCESS) { 429 if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK after success"); 430 mLockState = STATE_RESET_LOCK; 431 } 432 invalidate(); 433 } 434 }; 435 436 private final Runnable mAddWaveAction = new Runnable() { 437 public void run() { 438 double distX = mMouseX - mLockCenterX; 439 double distY = mMouseY - mLockCenterY; 440 int dragDistance = (int) Math.ceil(Math.hypot(distX, distY)); 441 if (mLockState == STATE_ATTEMPTING && dragDistance < mSnapRadius 442 && mWaveTimerDelay >= WAVE_DELAY) { 443 mWaveTimerDelay = Math.min(WAVE_DURATION, mWaveTimerDelay + DELAY_INCREMENT); 444 445 DrawableHolder wave = mLightWaves.get(mCurrentWave); 446 wave.setAlpha(0.0f); 447 wave.setScaleX(0.2f); 448 wave.setScaleY(0.2f); 449 wave.setX(mMouseX); 450 wave.setY(mMouseY); 451 452 wave.addAnimTo(WAVE_DURATION, 0, "x", mLockCenterX, true); 453 wave.addAnimTo(WAVE_DURATION, 0, "y", mLockCenterY, true); 454 wave.addAnimTo(WAVE_DURATION*2/3, 0, "alpha", 1.0f, true); 455 wave.addAnimTo(WAVE_DURATION, 0, "scaleX", 1.0f, true); 456 wave.addAnimTo(WAVE_DURATION, 0, "scaleY", 1.0f, true); 457 458 wave.addAnimTo(1000, RING_DELAY, "alpha", 0.0f, false); 459 wave.startAnimations(WaveView.this); 460 461 mCurrentWave = (mCurrentWave+1) % mWaveCount; 462 if (DBG) Log.v(TAG, "WaveTimerDelay: start new wave in " + mWaveTimerDelay); 463 } else { 464 mWaveTimerDelay += DELAY_INCREMENT2; 465 } 466 if (mFinishWaves) { 467 // sentinel used to restart the waves after they've stopped 468 mWavesRunning = false; 469 } else { 470 postDelayed(mAddWaveAction, mWaveTimerDelay); 471 } 472 } 473 }; 474 475 @Override 476 public boolean onHoverEvent(MotionEvent event) { 477 if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) { 478 final int action = event.getAction(); 479 switch (action) { 480 case MotionEvent.ACTION_HOVER_ENTER: 481 event.setAction(MotionEvent.ACTION_DOWN); 482 break; 483 case MotionEvent.ACTION_HOVER_MOVE: 484 event.setAction(MotionEvent.ACTION_MOVE); 485 break; 486 case MotionEvent.ACTION_HOVER_EXIT: 487 event.setAction(MotionEvent.ACTION_UP); 488 break; 489 } 490 onTouchEvent(event); 491 event.setAction(action); 492 } 493 return super.onHoverEvent(event); 494 } 495 496 @Override 497 public boolean onTouchEvent(MotionEvent event) { 498 final int action = event.getAction(); 499 mMouseX = event.getX(); 500 mMouseY = event.getY(); 501 boolean handled = false; 502 switch (action) { 503 case MotionEvent.ACTION_DOWN: 504 removeCallbacks(mLockTimerActions); 505 mFingerDown = true; 506 tryTransitionToStartAttemptState(event); 507 handled = true; 508 break; 509 510 case MotionEvent.ACTION_MOVE: 511 tryTransitionToStartAttemptState(event); 512 handled = true; 513 break; 514 515 case MotionEvent.ACTION_UP: 516 if (DBG) Log.v(TAG, "ACTION_UP"); 517 mFingerDown = false; 518 postDelayed(mLockTimerActions, RESET_TIMEOUT); 519 setGrabbedState(OnTriggerListener.NO_HANDLE); 520 // Normally the state machine is driven by user interaction causing redraws. 521 // However, when there's no more user interaction and no running animations, 522 // the state machine stops advancing because onDraw() never gets called. 523 // The following ensures we advance to the next state in this case, 524 // either STATE_UNLOCK_ATTEMPT or STATE_RESET_LOCK. 525 waveUpdateFrame(mMouseX, mMouseY, mFingerDown); 526 handled = true; 527 break; 528 529 case MotionEvent.ACTION_CANCEL: 530 mFingerDown = false; 531 handled = true; 532 break; 533 } 534 invalidate(); 535 return handled ? true : super.onTouchEvent(event); 536 } 537 538 /** 539 * Tries to transition to start attempt state. 540 * 541 * @param event A motion event. 542 */ 543 private void tryTransitionToStartAttemptState(MotionEvent event) { 544 final float dx = event.getX() - mUnlockHalo.getX(); 545 final float dy = event.getY() - mUnlockHalo.getY(); 546 float dist = (float) Math.hypot(dx, dy); 547 if (dist <= getScaledGrabHandleRadius()) { 548 setGrabbedState(OnTriggerListener.CENTER_HANDLE); 549 if (mLockState == STATE_READY) { 550 mLockState = STATE_START_ATTEMPT; 551 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 552 announceUnlockHandle(); 553 } 554 } 555 } 556 } 557 558 /** 559 * @return The radius in which the handle is grabbed scaled based on 560 * whether accessibility is enabled. 561 */ 562 private float getScaledGrabHandleRadius() { 563 if (AccessibilityManager.getInstance(mContext).isEnabled()) { 564 return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mUnlockHalo.getWidth(); 565 } else { 566 return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED * mUnlockHalo.getWidth(); 567 } 568 } 569 570 /** 571 * Announces the unlock handle if accessibility is enabled. 572 */ 573 private void announceUnlockHandle() { 574 setContentDescription(mContext.getString(R.string.description_target_unlock_tablet)); 575 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED); 576 setContentDescription(null); 577 } 578 579 /** 580 * Triggers haptic feedback. 581 */ 582 private synchronized void vibrate(long duration) { 583 final boolean hapticEnabled = Settings.System.getIntForUser( 584 mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1, 585 UserHandle.USER_CURRENT) != 0; 586 if (hapticEnabled) { 587 if (mVibrator == null) { 588 mVibrator = (android.os.Vibrator) getContext() 589 .getSystemService(Context.VIBRATOR_SERVICE); 590 } 591 mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES); 592 } 593 } 594 595 /** 596 * Registers a callback to be invoked when the user triggers an event. 597 * 598 * @param listener the OnDialTriggerListener to attach to this view 599 */ 600 public void setOnTriggerListener(OnTriggerListener listener) { 601 mOnTriggerListener = listener; 602 } 603 604 /** 605 * Dispatches a trigger event to listener. Ignored if a listener is not set. 606 * @param whichHandle the handle that triggered the event. 607 */ 608 private void dispatchTriggerEvent(int whichHandle) { 609 vibrate(VIBRATE_LONG); 610 if (mOnTriggerListener != null) { 611 mOnTriggerListener.onTrigger(this, whichHandle); 612 } 613 } 614 615 /** 616 * Sets the current grabbed state, and dispatches a grabbed state change 617 * event to our listener. 618 */ 619 private void setGrabbedState(int newState) { 620 if (newState != mGrabbedState) { 621 mGrabbedState = newState; 622 if (mOnTriggerListener != null) { 623 mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState); 624 } 625 } 626 } 627 628 public interface OnTriggerListener { 629 /** 630 * Sent when the user releases the handle. 631 */ 632 public static final int NO_HANDLE = 0; 633 634 /** 635 * Sent when the user grabs the center handle 636 */ 637 public static final int CENTER_HANDLE = 10; 638 639 /** 640 * Called when the user drags the center ring beyond a threshold. 641 */ 642 void onTrigger(View v, int whichHandle); 643 644 /** 645 * Called when the "grabbed state" changes (i.e. when the user either grabs or releases 646 * one of the handles.) 647 * 648 * @param v the view that was triggered 649 * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #CENTER_HANDLE}, 650 */ 651 void onGrabbedStateChange(View v, int grabbedState); 652 } 653 654 public void onAnimationUpdate(ValueAnimator animation) { 655 invalidate(); 656 } 657 658 public void reset() { 659 if (DBG) Log.v(TAG, "reset() : resets state to STATE_RESET_LOCK"); 660 mLockState = STATE_RESET_LOCK; 661 invalidate(); 662 } 663 } 664