WaveView.java revision 6033c0817427386cd3e95a992d1f34dad4188f96
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
17package com.android.internal.widget;
18
19import java.util.ArrayList;
20
21import android.animation.ValueAnimator;
22import android.content.Context;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.BitmapFactory;
26import android.graphics.Canvas;
27import android.graphics.drawable.BitmapDrawable;
28import android.os.Vibrator;
29import android.text.TextUtils;
30import android.util.AttributeSet;
31import android.util.Log;
32import android.view.MotionEvent;
33import android.view.View;
34import android.view.accessibility.AccessibilityEvent;
35import android.view.accessibility.AccessibilityManager;
36
37import 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 */
43public 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