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