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