FileDocCategorySizeDatePackage
GlowPadView.javaAPI DocAndroid 5.1 API52448Thu Mar 12 22:22:10 GMT 2015com.android.internal.widget.multiwaveview

GlowPadView

public class GlowPadView extends android.view.View
A re-usable widget containing a center, outer ring and wave animation.

Fields Summary
private static final String
TAG
private static final boolean
DEBUG
private static final int
STATE_IDLE
private static final int
STATE_START
private static final int
STATE_FIRST_TOUCH
private static final int
STATE_TRACKING
private static final int
STATE_SNAP
private static final int
STATE_FINISH
private static final float
SNAP_MARGIN_DEFAULT
private static final int
WAVE_ANIMATION_DURATION
private static final int
RETURN_TO_HOME_DELAY
private static final int
RETURN_TO_HOME_DURATION
private static final int
HIDE_ANIMATION_DELAY
private static final int
HIDE_ANIMATION_DURATION
private static final int
SHOW_ANIMATION_DURATION
private static final int
SHOW_ANIMATION_DELAY
private static final int
INITIAL_SHOW_HANDLE_DURATION
private static final int
REVEAL_GLOW_DELAY
private static final int
REVEAL_GLOW_DURATION
private static final float
TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED
private static final float
TARGET_SCALE_EXPANDED
private static final float
TARGET_SCALE_COLLAPSED
private static final float
RING_SCALE_EXPANDED
private static final float
RING_SCALE_COLLAPSED
private static final android.media.AudioAttributes
VIBRATION_ATTRIBUTES
private ArrayList
mTargetDrawables
private AnimationBundle
mWaveAnimations
private AnimationBundle
mTargetAnimations
private AnimationBundle
mGlowAnimations
private ArrayList
mTargetDescriptions
private ArrayList
mDirectionDescriptions
private OnTriggerListener
mOnTriggerListener
private TargetDrawable
mHandleDrawable
private TargetDrawable
mOuterRing
private android.os.Vibrator
mVibrator
private int
mFeedbackCount
private int
mVibrationDuration
private int
mGrabbedState
private int
mActiveTarget
private float
mGlowRadius
private float
mWaveCenterX
private float
mWaveCenterY
private int
mMaxTargetHeight
private int
mMaxTargetWidth
private float
mRingScaleFactor
private boolean
mAllowScaling
private float
mOuterRadius
private float
mSnapMargin
private float
mFirstItemOffset
private boolean
mMagneticTargets
private boolean
mDragging
private int
mNewTargetResources
private android.animation.Animator.AnimatorListener
mResetListener
private android.animation.Animator.AnimatorListener
mResetListenerWithPing
private android.animation.ValueAnimator.AnimatorUpdateListener
mUpdateListener
private boolean
mAnimatingTargets
private android.animation.Animator.AnimatorListener
mTargetUpdateListener
private int
mTargetResourceId
private int
mTargetDescriptionsResourceId
private int
mDirectionDescriptionsResourceId
private boolean
mAlwaysTrackFinger
private int
mHorizontalInset
private int
mVerticalInset
private int
mGravity
private boolean
mInitialLayout
private Tweener
mBackgroundAnimator
private PointCloud
mPointCloud
private float
mInnerRadius
private int
mPointerId
Constructors Summary
public GlowPadView(android.content.Context context)


       
        this(context, null);
    
public GlowPadView(android.content.Context context, android.util.AttributeSet attrs)

        super(context, attrs);
        Resources res = context.getResources();

        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView);
        mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius);
        mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius);
        mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin);
        mFirstItemOffset = (float) Math.toRadians(
                a.getFloat(R.styleable.GlowPadView_firstItemOffset,
                        (float) Math.toDegrees(mFirstItemOffset)));
        mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration,
                mVibrationDuration);
        mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount,
                mFeedbackCount);
        mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false);
        TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
        mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0);
        mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
        mOuterRing = new TargetDrawable(res,
                getResourceId(a, R.styleable.GlowPadView_outerRingDrawable));

        mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);
        mMagneticTargets = a.getBoolean(R.styleable.GlowPadView_magneticTargets, mMagneticTargets);

        int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
        Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null;
        mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);

        mPointCloud = new PointCloud(pointDrawable);
        mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
        mPointCloud.glowManager.setRadius(mGlowRadius);

        TypedValue outValue = new TypedValue();

        // Read array of target drawables
        if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
            internalSetTargetResources(outValue.resourceId);
        }
        if (mTargetDrawables == null || mTargetDrawables.size() == 0) {
            throw new IllegalStateException("Must specify at least one target drawable");
        }

        // Read array of target descriptions
        if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) {
            final int resourceId = outValue.resourceId;
            if (resourceId == 0) {
                throw new IllegalStateException("Must specify target descriptions");
            }
            setTargetDescriptionsResourceId(resourceId);
        }

        // Read array of direction descriptions
        if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) {
            final int resourceId = outValue.resourceId;
            if (resourceId == 0) {
                throw new IllegalStateException("Must specify direction descriptions");
            }
            setDirectionDescriptionsResourceId(resourceId);
        }

        mGravity = a.getInt(R.styleable.GlowPadView_gravity, Gravity.TOP);

        a.recycle();

        setVibrateEnabled(mVibrationDuration > 0);

        assignDefaultsIfNeeded();
    
Methods Summary
private voidannounceTargets()

        StringBuilder utterance = new StringBuilder();
        final int targetCount = mTargetDrawables.size();
        for (int i = 0; i < targetCount; i++) {
            String targetDescription = getTargetDescription(i);
            String directionDescription = getDirectionDescription(i);
            if (!TextUtils.isEmpty(targetDescription)
                    && !TextUtils.isEmpty(directionDescription)) {
                String text = String.format(directionDescription, targetDescription);
                utterance.append(text);
            }
        }
        if (utterance.length() > 0) {
            announceForAccessibility(utterance.toString());
        }
    
private voidassignDefaultsIfNeeded()

        if (mOuterRadius == 0.0f) {
            mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f;
        }
        if (mSnapMargin == 0.0f) {
            mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                    SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
        }
        if (mInnerRadius == 0.0f) {
            mInnerRadius = mHandleDrawable.getWidth() / 10.0f;
        }
    
private voidcomputeInsets(int dx, int dy)

        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);

        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.LEFT:
                mHorizontalInset = 0;
                break;
            case Gravity.RIGHT:
                mHorizontalInset = dx;
                break;
            case Gravity.CENTER_HORIZONTAL:
            default:
                mHorizontalInset = dx / 2;
                break;
        }
        switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.TOP:
                mVerticalInset = 0;
                break;
            case Gravity.BOTTOM:
                mVerticalInset = dy;
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                mVerticalInset = dy / 2;
                break;
        }
    
private floatcomputeScaleFactor(int desiredWidth, int desiredHeight, int actualWidth, int actualHeight)
Given the desired width and height of the ring and the allocated width and height, compute how much we need to scale the ring.


        // Return unity if scaling is not allowed.
        if (!mAllowScaling) return 1f;

        final int layoutDirection = getLayoutDirection();
        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);

        float scaleX = 1f;
        float scaleY = 1f;

        // We use the gravity as a cue for whether we want to scale on a particular axis.
        // We only scale to fit horizontally if we're not pinned to the left or right. Likewise,
        // we only scale to fit vertically if we're not pinned to the top or bottom. In these
        // cases, we want the ring to hang off the side or top/bottom, respectively.
        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
            case Gravity.LEFT:
            case Gravity.RIGHT:
                break;
            case Gravity.CENTER_HORIZONTAL:
            default:
                if (desiredWidth > actualWidth) {
                    scaleX = (1f * actualWidth - mMaxTargetWidth) /
                            (desiredWidth - mMaxTargetWidth);
                }
                break;
        }
        switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
            case Gravity.TOP:
            case Gravity.BOTTOM:
                break;
            case Gravity.CENTER_VERTICAL:
            default:
                if (desiredHeight > actualHeight) {
                    scaleY = (1f * actualHeight - mMaxTargetHeight) /
                            (desiredHeight - mMaxTargetHeight);
                }
                break;
        }
        return Math.min(scaleX, scaleY);
    
private voiddeactivateTargets()

        final int count = mTargetDrawables.size();
        for (int i = 0; i < count; i++) {
            TargetDrawable target = mTargetDrawables.get(i);
            target.setState(TargetDrawable.STATE_INACTIVE);
        }
        mActiveTarget = -1;
    
private voiddispatchOnFinishFinalAnimation()

        if (mOnTriggerListener != null) {
            mOnTriggerListener.onFinishFinalAnimation();
        }
    
private voiddispatchTriggerEvent(int whichTarget)
Dispatches a trigger event to listener. Ignored if a listener is not set.

param
whichTarget the target that was triggered.

        vibrate();
        if (mOnTriggerListener != null) {
            mOnTriggerListener.onTrigger(this, whichTarget);
        }
    
private floatdist2(float dx, float dy)

        return dx*dx + dy*dy;
    
private voiddoFinish()

        final int activeTarget = mActiveTarget;
        final boolean targetHit =  activeTarget != -1;

        if (targetHit) {
            if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit);

            highlightSelected(activeTarget);

            // Inform listener of any active targets.  Typically only one will be active.
            hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener);
            dispatchTriggerEvent(activeTarget);
            if (!mAlwaysTrackFinger) {
                // Force ring and targets to finish animation to final expanded state
                mTargetAnimations.stop();
            }
        } else {
            // Animate handle back to the center based on current state.
            hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing);
            hideTargets(true, false);
        }

        setGrabbedState(OnTriggerListener.NO_HANDLE);
    
private voiddump()

        Log.v(TAG, "Outer Radius = " + mOuterRadius);
        Log.v(TAG, "SnapMargin = " + mSnapMargin);
        Log.v(TAG, "FeedbackCount = " + mFeedbackCount);
        Log.v(TAG, "VibrationDuration = " + mVibrationDuration);
        Log.v(TAG, "GlowRadius = " + mGlowRadius);
        Log.v(TAG, "WaveCenterX = " + mWaveCenterX);
        Log.v(TAG, "WaveCenterY = " + mWaveCenterY);
    
private floatgetAngle(float alpha, int i)

        return mFirstItemOffset + alpha * i;
    
private java.lang.StringgetDirectionDescription(int index)

        if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) {
            mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId);
            if (mTargetDrawables.size() != mDirectionDescriptions.size()) {
                Log.w(TAG, "The number of target drawables must be"
                        + " equal to the number of direction descriptions.");
                return null;
            }
        }
        return mDirectionDescriptions.get(index);
    
public intgetDirectionDescriptionsResourceId()
Gets the resource id specifying the target direction descriptions.

return
The resource id.

        return mDirectionDescriptionsResourceId;
    
private intgetResourceId(android.content.res.TypedArray a, int id)

        TypedValue tv = a.peekValue(id);
        return tv == null ? 0 : tv.resourceId;
    
public intgetResourceIdForTarget(int index)

        final TargetDrawable drawable = mTargetDrawables.get(index);
        return drawable == null ? 0 : drawable.getResourceId();
    
private floatgetRingHeight()

        return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
    
private floatgetRingWidth()

        return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
    
private floatgetScaledGlowRadiusSquared()

        final float scaledTapRadius;
        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
            scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
        } else {
            scaledTapRadius = mGlowRadius;
        }
        return square(scaledTapRadius);
    
protected intgetScaledSuggestedMinimumHeight()
This gets the suggested height accounting for the ring's scale factor.

        return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius)
                + mMaxTargetHeight);
    
protected intgetScaledSuggestedMinimumWidth()
This gets the suggested width accounting for the ring's scale factor.

        return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius)
                + mMaxTargetWidth);
    
private floatgetSliceAngle()

        return (float) (-2.0f * Math.PI / mTargetDrawables.size());
    
protected intgetSuggestedMinimumHeight()

        // View should be large enough to contain the unlock ring + target and
        // target drawable on either edge
        return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight);
    
protected intgetSuggestedMinimumWidth()

        // View should be large enough to contain the background + handle and
        // target drawable on either edge.
        return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth);
    
private java.lang.StringgetTargetDescription(int index)

        if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) {
            mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId);
            if (mTargetDrawables.size() != mTargetDescriptions.size()) {
                Log.w(TAG, "The number of target drawables must be"
                        + " equal to the number of target descriptions.");
                return null;
            }
        }
        return mTargetDescriptions.get(index);
    
public intgetTargetDescriptionsResourceId()
Gets the resource id specifying the target descriptions for accessibility.

return
The resource id.

        return mTargetDescriptionsResourceId;
    
public intgetTargetPosition(int resourceId)
Gets the position of a target in the array that matches the given resource.

param
resourceId
return
the index or -1 if not found

        for (int i = 0; i < mTargetDrawables.size(); i++) {
            final TargetDrawable target = mTargetDrawables.get(i);
            if (target.getResourceId() == resourceId) {
                return i; // should never be more than one match
            }
        }
        return -1;
    
public intgetTargetResourceId()

        return mTargetResourceId;
    
private voidhandleCancel(android.view.MotionEvent event)

        if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL");

        // Drop the active target if canceled.
        mActiveTarget = -1; 

        int actionIndex = event.findPointerIndex(mPointerId);
        actionIndex = actionIndex == -1 ? 0 : actionIndex;
        switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
    
private voidhandleDown(android.view.MotionEvent event)

        int actionIndex = event.getActionIndex();
        float eventX = event.getX(actionIndex);
        float eventY = event.getY(actionIndex);
        switchToState(STATE_START, eventX, eventY);
        if (!trySwitchToFirstTouchState(eventX, eventY)) {
            mDragging = false;
        } else {
            mPointerId = event.getPointerId(actionIndex);
            updateGlowPosition(eventX, eventY);
        }
    
private voidhandleMove(android.view.MotionEvent event)

        int activeTarget = -1;
        final int historySize = event.getHistorySize();
        ArrayList<TargetDrawable> targets = mTargetDrawables;
        int ntargets = targets.size();
        float x = 0.0f;
        float y = 0.0f;
        float activeAngle = 0.0f;
        int actionIndex = event.findPointerIndex(mPointerId);

        if (actionIndex == -1) {
            return;  // no data for this pointer
        }

        for (int k = 0; k < historySize + 1; k++) {
            float eventX = k < historySize ? event.getHistoricalX(actionIndex, k)
                    : event.getX(actionIndex);
            float eventY = k < historySize ? event.getHistoricalY(actionIndex, k)
                    : event.getY(actionIndex);
            // tx and ty are relative to wave center
            float tx = eventX - mWaveCenterX;
            float ty = eventY - mWaveCenterY;
            float touchRadius = (float) Math.sqrt(dist2(tx, ty));
            final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
            float limitX = tx * scale;
            float limitY = ty * scale;
            double angleRad = Math.atan2(-ty, tx);

            if (!mDragging) {
                trySwitchToFirstTouchState(eventX, eventY);
            }

            if (mDragging) {
                // For multiple targets, snap to the one that matches
                final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin;
                final float snapDistance2 = snapRadius * snapRadius;
                // Find first target in range
                for (int i = 0; i < ntargets; i++) {
                    TargetDrawable target = targets.get(i);

                    double targetMinRad = mFirstItemOffset + (i - 0.5) * 2 * Math.PI / ntargets;
                    double targetMaxRad = mFirstItemOffset + (i + 0.5) * 2 * Math.PI / ntargets;
                    if (target.isEnabled()) {
                        boolean angleMatches =
                            (angleRad > targetMinRad && angleRad <= targetMaxRad) ||
                            (angleRad + 2 * Math.PI > targetMinRad &&
                             angleRad + 2 * Math.PI <= targetMaxRad) ||
                            (angleRad - 2 * Math.PI > targetMinRad &&
                             angleRad - 2 * Math.PI <= targetMaxRad);
                        if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
                            activeTarget = i;
                            activeAngle = (float) -angleRad;
                        }
                    }
                }
            }
            x = limitX;
            y = limitY;
        }

        if (!mDragging) {
            return;
        }

        if (activeTarget != -1) {
            switchToState(STATE_SNAP, x,y);
            updateGlowPosition(x, y);
        } else {
            switchToState(STATE_TRACKING, x, y);
            updateGlowPosition(x, y);
        }

        if (mActiveTarget != activeTarget) {
            // Defocus the old target
            if (mActiveTarget != -1) {
                TargetDrawable target = targets.get(mActiveTarget);
                if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
                    target.setState(TargetDrawable.STATE_INACTIVE);
                }
                if (mMagneticTargets) {
                    updateTargetPosition(mActiveTarget, mWaveCenterX, mWaveCenterY);
                }
            }
            // Focus the new target
            if (activeTarget != -1) {
                TargetDrawable target = targets.get(activeTarget);
                if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
                    target.setState(TargetDrawable.STATE_FOCUSED);
                }
                if (mMagneticTargets) {
                    updateTargetPosition(activeTarget, mWaveCenterX, mWaveCenterY, activeAngle);
                }
                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
                    String targetContentDescription = getTargetDescription(activeTarget);
                    announceForAccessibility(targetContentDescription);
                }
            }
        }
        mActiveTarget = activeTarget;
    
private voidhandleUp(android.view.MotionEvent event)

        if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE");
        int actionIndex = event.getActionIndex();
        if (event.getPointerId(actionIndex) == mPointerId) {
            switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
        }
    
private voidhideGlow(int duration, int delay, float finalAlpha, android.animation.Animator.AnimatorListener finishListener)

        mGlowAnimations.cancel();
        mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
                "ease", Ease.Quart.easeOut,
                "delay", delay,
                "alpha", finalAlpha,
                "x", 0.0f,
                "y", 0.0f,
                "onUpdate", mUpdateListener,
                "onComplete", finishListener));
        mGlowAnimations.start();
    
private voidhideTargets(boolean animate, boolean expanded)

        mTargetAnimations.cancel();
        // Note: these animations should complete at the same time so that we can swap out
        // the target assets asynchronously from the setTargetResources() call.
        mAnimatingTargets = animate;
        final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
        final int delay = animate ? HIDE_ANIMATION_DELAY : 0;

        final float targetScale = expanded ?
                TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
        final int length = mTargetDrawables.size();
        final TimeInterpolator interpolator = Ease.Cubic.easeOut;
        for (int i = 0; i < length; i++) {
            TargetDrawable target = mTargetDrawables.get(i);
            target.setState(TargetDrawable.STATE_INACTIVE);
            mTargetAnimations.add(Tweener.to(target, duration,
                    "ease", interpolator,
                    "alpha", 0.0f,
                    "scaleX", targetScale,
                    "scaleY", targetScale,
                    "delay", delay,
                    "onUpdate", mUpdateListener));
        }

        float ringScaleTarget = expanded ?
                RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
        ringScaleTarget *= mRingScaleFactor;
        mTargetAnimations.add(Tweener.to(mOuterRing, duration,
                "ease", interpolator,
                "alpha", 0.0f,
                "scaleX", ringScaleTarget,
                "scaleY", ringScaleTarget,
                "delay", delay,
                "onUpdate", mUpdateListener,
                "onComplete", mTargetUpdateListener));

        mTargetAnimations.start();
    
private voidhideUnselected(int active)

        for (int i = 0; i < mTargetDrawables.size(); i++) {
            if (i != active) {
                mTargetDrawables.get(i).setAlpha(0.0f);
            }
        }
    
private voidhighlightSelected(int activeTarget)

        // Highlight the given target and fade others
        mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
        hideUnselected(activeTarget);
    
private voidinternalSetTargetResources(int resourceId)

        final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId);
        mTargetDrawables = targets;
        mTargetResourceId = resourceId;

        int maxWidth = mHandleDrawable.getWidth();
        int maxHeight = mHandleDrawable.getHeight();
        final int count = targets.size();
        for (int i = 0; i < count; i++) {
            TargetDrawable target = targets.get(i);
            maxWidth = Math.max(maxWidth, target.getWidth());
            maxHeight = Math.max(maxHeight, target.getHeight());
        }
        if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
            mMaxTargetWidth = maxWidth;
            mMaxTargetHeight = maxHeight;
            requestLayout(); // required to resize layout and call updateTargetPositions()
        } else {
            updateTargetPositions(mWaveCenterX, mWaveCenterY);
            updatePointCloudPosition(mWaveCenterX, mWaveCenterY);
        }
    
private java.util.ArrayListloadDescriptions(int resourceId)

        TypedArray array = getContext().getResources().obtainTypedArray(resourceId);
        final int count = array.length();
        ArrayList<String> targetContentDescriptions = new ArrayList<String>(count);
        for (int i = 0; i < count; i++) {
            String contentDescription = array.getString(i);
            targetContentDescriptions.add(contentDescription);
        }
        array.recycle();
        return targetContentDescriptions;
    
private java.util.ArrayListloadDrawableArray(int resourceId)

        Resources res = getContext().getResources();
        TypedArray array = res.obtainTypedArray(resourceId);
        final int count = array.length();
        ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count);
        for (int i = 0; i < count; i++) {
            TypedValue value = array.peekValue(i);
            TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0);
            drawables.add(target);
        }
        array.recycle();
        return drawables;
    
protected voidonDraw(android.graphics.Canvas canvas)

        mPointCloud.draw(canvas);
        mOuterRing.draw(canvas);
        final int ntargets = mTargetDrawables.size();
        for (int i = 0; i < ntargets; i++) {
            TargetDrawable target = mTargetDrawables.get(i);
            if (target != null) {
                target.draw(canvas);
            }
        }
        mHandleDrawable.draw(canvas);
    
public booleanonHoverEvent(android.view.MotionEvent event)

        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
            final int action = event.getAction();
            switch (action) {
                case MotionEvent.ACTION_HOVER_ENTER:
                    event.setAction(MotionEvent.ACTION_DOWN);
                    break;
                case MotionEvent.ACTION_HOVER_MOVE:
                    event.setAction(MotionEvent.ACTION_MOVE);
                    break;
                case MotionEvent.ACTION_HOVER_EXIT:
                    event.setAction(MotionEvent.ACTION_UP);
                    break;
            }
            onTouchEvent(event);
            event.setAction(action);
        }
        super.onHoverEvent(event);
        return true;
    
protected voidonLayout(boolean changed, int left, int top, int right, int bottom)

        super.onLayout(changed, left, top, right, bottom);
        final int width = right - left;
        final int height = bottom - top;

        // Target placement width/height. This puts the targets on the greater of the ring
        // width or the specified outer radius.
        final float placementWidth = getRingWidth();
        final float placementHeight = getRingHeight();
        float newWaveCenterX = mHorizontalInset
                + Math.max(width, mMaxTargetWidth + placementWidth) / 2;
        float newWaveCenterY = mVerticalInset
                + Math.max(height, + mMaxTargetHeight + placementHeight) / 2;

        if (mInitialLayout) {
            stopAndHideWaveAnimation();
            hideTargets(false, false);
            mInitialLayout = false;
        }

        mOuterRing.setPositionX(newWaveCenterX);
        mOuterRing.setPositionY(newWaveCenterY);

        mPointCloud.setScale(mRingScaleFactor);

        mHandleDrawable.setPositionX(newWaveCenterX);
        mHandleDrawable.setPositionY(newWaveCenterY);

        updateTargetPositions(newWaveCenterX, newWaveCenterY);
        updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
        updateGlowPosition(newWaveCenterX, newWaveCenterY);

        mWaveCenterX = newWaveCenterX;
        mWaveCenterY = newWaveCenterY;

        if (DEBUG) dump();
    
protected voidonMeasure(int widthMeasureSpec, int heightMeasureSpec)

        final int minimumWidth = getSuggestedMinimumWidth();
        final int minimumHeight = getSuggestedMinimumHeight();
        int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
        int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);

        mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight,
                computedWidth, computedHeight);

        int scaledWidth = getScaledSuggestedMinimumWidth();
        int scaledHeight = getScaledSuggestedMinimumHeight();

        computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight);
        setMeasuredDimension(computedWidth, computedHeight);
    
public booleanonTouchEvent(android.view.MotionEvent event)

        final int action = event.getActionMasked();
        boolean handled = false;
        switch (action) {
            case MotionEvent.ACTION_POINTER_DOWN:
            case MotionEvent.ACTION_DOWN:
                if (DEBUG) Log.v(TAG, "*** DOWN ***");
                handleDown(event);
                handleMove(event);
                handled = true;
                break;

            case MotionEvent.ACTION_MOVE:
                if (DEBUG) Log.v(TAG, "*** MOVE ***");
                handleMove(event);
                handled = true;
                break;

            case MotionEvent.ACTION_POINTER_UP:
            case MotionEvent.ACTION_UP:
                if (DEBUG) Log.v(TAG, "*** UP ***");
                handleMove(event);
                handleUp(event);
                handled = true;
                break;

            case MotionEvent.ACTION_CANCEL:
                if (DEBUG) Log.v(TAG, "*** CANCEL ***");
                handleMove(event);
                handleCancel(event);
                handled = true;
                break;

        }
        invalidate();
        return handled ? true : super.onTouchEvent(event);
    
public voidping()
Starts wave animation.

        if (mFeedbackCount > 0) {
            boolean doWaveAnimation = true;
            final AnimationBundle waveAnimations = mWaveAnimations;

            // Don't do a wave if there's already one in progress
            if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
                long t = waveAnimations.get(0).animator.getCurrentPlayTime();
                if (t < WAVE_ANIMATION_DURATION/2) {
                    doWaveAnimation = false;
                }
            }

            if (doWaveAnimation) {
                startWaveAnimation();
            }
        }
    
private booleanreplaceTargetDrawables(android.content.res.Resources res, int existingResourceId, int newResourceId)

        if (existingResourceId == 0 || newResourceId == 0) {
            return false;
        }

        boolean result = false;
        final ArrayList<TargetDrawable> drawables = mTargetDrawables;
        final int size = drawables.size();
        for (int i = 0; i < size; i++) {
            final TargetDrawable target = drawables.get(i);
            if (target != null && target.getResourceId() == existingResourceId) {
                target.setDrawable(res, newResourceId);
                result = true;
            }
        }

        if (result) {
            requestLayout(); // in case any given drawable's size changes
        }

        return result;
    
public booleanreplaceTargetDrawablesIfPresent(android.content.ComponentName component, java.lang.String name, int existingResId)
Searches the given package for a resource to use to replace the Drawable on the target with the given resource id

param
component of the .apk that contains the resource
param
name of the metadata in the .apk
param
existingResId the resource id of the target to search for
return
true if found in the given package and replaced at least one target Drawables

        if (existingResId == 0) return false;

        boolean replaced = false;
        if (component != null) {
            try {
                PackageManager packageManager = mContext.getPackageManager();
                // Look for the search icon specified in the activity meta-data
                Bundle metaData = packageManager.getActivityInfo(
                        component, PackageManager.GET_META_DATA).metaData;
                if (metaData != null) {
                    int iconResId = metaData.getInt(name);
                    if (iconResId != 0) {
                        Resources res = packageManager.getResourcesForActivity(component);
                        replaced = replaceTargetDrawables(res, existingResId, iconResId);
                    }
                }
            } catch (NameNotFoundException e) {
                Log.w(TAG, "Failed to swap drawable; "
                        + component.flattenToShortString() + " not found", e);
            } catch (Resources.NotFoundException nfe) {
                Log.w(TAG, "Failed to swap drawable from "
                        + component.flattenToShortString(), nfe);
            }
        }
        if (!replaced) {
            // Restore the original drawable
            replaceTargetDrawables(mContext.getResources(), existingResId, existingResId);
        }
        return replaced;
    
public voidreset(boolean animate)
Resets the widget to default state and cancels all animation. If animate is 'true', will animate objects into place. Otherwise, objects will snap back to place.

param
animate

        mGlowAnimations.stop();
        mTargetAnimations.stop();
        startBackgroundAnimation(0, 0.0f);
        stopAndHideWaveAnimation();
        hideTargets(animate, false);
        hideGlow(0, 0, 0.0f, null);
        Tweener.reset();
    
private intresolveMeasured(int measureSpec, int desired)

        int result = 0;
        int specSize = MeasureSpec.getSize(measureSpec);
        switch (MeasureSpec.getMode(measureSpec)) {
            case MeasureSpec.UNSPECIFIED:
                result = desired;
                break;
            case MeasureSpec.AT_MOST:
                result = Math.min(specSize, desired);
                break;
            case MeasureSpec.EXACTLY:
            default:
                result = specSize;
        }
        return result;
    
public voidresumeAnimations()

        mWaveAnimations.setSuspended(false);
        mTargetAnimations.setSuspended(false);
        mGlowAnimations.setSuspended(false);
        mWaveAnimations.start();
        mTargetAnimations.start();
        mGlowAnimations.start();
    
public voidsetDirectionDescriptionsResourceId(int resourceId)
Sets the resource id specifying the target direction descriptions for accessibility.

param
resourceId The resource id.

        mDirectionDescriptionsResourceId = resourceId;
        if (mDirectionDescriptions != null) {
            mDirectionDescriptions.clear();
        }
    
public voidsetEnableTarget(int resourceId, boolean enabled)

        for (int i = 0; i < mTargetDrawables.size(); i++) {
            final TargetDrawable target = mTargetDrawables.get(i);
            if (target.getResourceId() == resourceId) {
                target.setEnabled(enabled);
                break; // should never be more than one match
            }
        }
    
private voidsetGrabbedState(int newState)
Sets the current grabbed state, and dispatches a grabbed state change event to our listener.

        if (newState != mGrabbedState) {
            if (newState != OnTriggerListener.NO_HANDLE) {
                vibrate();
            }
            mGrabbedState = newState;
            if (mOnTriggerListener != null) {
                if (newState == OnTriggerListener.NO_HANDLE) {
                    mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE);
                } else {
                    mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE);
                }
                mOnTriggerListener.onGrabbedStateChange(this, newState);
            }
        }
    
public voidsetOnTriggerListener(com.android.internal.widget.multiwaveview.GlowPadView$OnTriggerListener listener)

        mOnTriggerListener = listener;
    
public voidsetTargetDescriptionsResourceId(int resourceId)
Sets the resource id specifying the target descriptions for accessibility.

param
resourceId The resource id.

        mTargetDescriptionsResourceId = resourceId;
        if (mTargetDescriptions != null) {
            mTargetDescriptions.clear();
        }
    
public voidsetTargetResources(int resourceId)
Loads an array of drawables from the given resourceId.

param
resourceId

        if (mAnimatingTargets) {
            // postpone this change until we return to the initial state
            mNewTargetResources = resourceId;
        } else {
            internalSetTargetResources(resourceId);
        }
    
public voidsetVibrateEnabled(boolean enabled)
Enable or disable vibrate on touch.

param
enabled

        if (enabled && mVibrator == null) {
            mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
        } else {
            mVibrator = null;
        }
    
private voidshowGlow(int duration, int delay, float finalAlpha, android.animation.Animator.AnimatorListener finishListener)

        mGlowAnimations.cancel();
        mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
                "ease", Ease.Cubic.easeIn,
                "delay", delay,
                "alpha", finalAlpha,
                "onUpdate", mUpdateListener,
                "onComplete", finishListener));
        mGlowAnimations.start();
    
private voidshowTargets(boolean animate)

        mTargetAnimations.stop();
        mAnimatingTargets = animate;
        final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
        final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
        final int length = mTargetDrawables.size();
        for (int i = 0; i < length; i++) {
            TargetDrawable target = mTargetDrawables.get(i);
            target.setState(TargetDrawable.STATE_INACTIVE);
            mTargetAnimations.add(Tweener.to(target, duration,
                    "ease", Ease.Cubic.easeOut,
                    "alpha", 1.0f,
                    "scaleX", 1.0f,
                    "scaleY", 1.0f,
                    "delay", delay,
                    "onUpdate", mUpdateListener));
        }

        float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED;
        mTargetAnimations.add(Tweener.to(mOuterRing, duration,
                "ease", Ease.Cubic.easeOut,
                "alpha", 1.0f,
                "scaleX", ringScale,
                "scaleY", ringScale,
                "delay", delay,
                "onUpdate", mUpdateListener,
                "onComplete", mTargetUpdateListener));

        mTargetAnimations.start();
    
private floatsquare(float d)

        return d * d;
    
private voidstartBackgroundAnimation(int duration, float alpha)

        final Drawable background = getBackground();
        if (mAlwaysTrackFinger && background != null) {
            if (mBackgroundAnimator != null) {
                mBackgroundAnimator.animator.cancel();
            }
            mBackgroundAnimator = Tweener.to(background, duration,
                    "ease", Ease.Cubic.easeIn,
                    "alpha", (int)(255.0f * alpha),
                    "delay", SHOW_ANIMATION_DELAY);
            mBackgroundAnimator.animator.start();
        }
    
private voidstartWaveAnimation()

        mWaveAnimations.cancel();
        mPointCloud.waveManager.setAlpha(1.0f);
        mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f);
        mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION,
                "ease", Ease.Quad.easeOut,
                "delay", 0,
                "radius", 2.0f * mOuterRadius,
                "onUpdate", mUpdateListener,
                "onComplete",
                new AnimatorListenerAdapter() {
                    public void onAnimationEnd(Animator animator) {
                        mPointCloud.waveManager.setRadius(0.0f);
                        mPointCloud.waveManager.setAlpha(0.0f);
                    }
                }));
        mWaveAnimations.start();
    
private voidstopAndHideWaveAnimation()

        mWaveAnimations.cancel();
        mPointCloud.waveManager.setAlpha(0.0f);
    
public voidsuspendAnimations()

        mWaveAnimations.setSuspended(true);
        mTargetAnimations.setSuspended(true);
        mGlowAnimations.setSuspended(true);
    
private voidswitchToState(int state, float x, float y)

        switch (state) {
            case STATE_IDLE:
                deactivateTargets();
                hideGlow(0, 0, 0.0f, null);
                startBackgroundAnimation(0, 0.0f);
                mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
                mHandleDrawable.setAlpha(1.0f);
                break;

            case STATE_START:
                startBackgroundAnimation(0, 0.0f);
                break;

            case STATE_FIRST_TOUCH:
                mHandleDrawable.setAlpha(0.0f);
                deactivateTargets();
                showTargets(true);
                startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
                setGrabbedState(OnTriggerListener.CENTER_HANDLE);
                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
                    announceTargets();
                }
                break;

            case STATE_TRACKING:
                mHandleDrawable.setAlpha(0.0f);
                showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null);
                break;

            case STATE_SNAP:
                // TODO: Add transition states (see list_selector_background_transition.xml)
                mHandleDrawable.setAlpha(0.0f);
                showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null);
                break;

            case STATE_FINISH:
                doFinish();
                break;
        }
    
private booleantrySwitchToFirstTouchState(float x, float y)

        final float tx = x - mWaveCenterX;
        final float ty = y - mWaveCenterY;
        if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) {
            if (DEBUG) Log.v(TAG, "** Handle HIT");
            switchToState(STATE_FIRST_TOUCH, x, y);
            updateGlowPosition(tx, ty);
            mDragging = true;
            return true;
        }
        return false;
    
private voidupdateGlowPosition(float x, float y)

        float dx = x - mOuterRing.getX();
        float dy = y - mOuterRing.getY();
        dx *= 1f / mRingScaleFactor;
        dy *= 1f / mRingScaleFactor;
        mPointCloud.glowManager.setX(mOuterRing.getX() + dx);
        mPointCloud.glowManager.setY(mOuterRing.getY() + dy);
    
private voidupdatePointCloudPosition(float centerX, float centerY)

        mPointCloud.setCenter(centerX, centerY);
    
private voidupdateTargetPosition(int i, float centerX, float centerY)

        final float angle = getAngle(getSliceAngle(), i);
        updateTargetPosition(i, centerX, centerY, angle);
    
private voidupdateTargetPosition(int i, float centerX, float centerY, float angle)

        final float placementRadiusX = getRingWidth() / 2;
        final float placementRadiusY = getRingHeight() / 2;
        if (i >= 0) {
            ArrayList<TargetDrawable> targets = mTargetDrawables;
            final TargetDrawable targetIcon = targets.get(i);
            targetIcon.setPositionX(centerX);
            targetIcon.setPositionY(centerY);
            targetIcon.setX(placementRadiusX * (float) Math.cos(angle));
            targetIcon.setY(placementRadiusY * (float) Math.sin(angle));
        }
    
private voidupdateTargetPositions(float centerX, float centerY)

        updateTargetPositions(centerX, centerY, false);
    
private voidupdateTargetPositions(float centerX, float centerY, boolean skipActive)

        final int size = mTargetDrawables.size();
        final float alpha = getSliceAngle();
        // Reposition the target drawables if the view changed.
        for (int i = 0; i < size; i++) {
            if (!skipActive || i != mActiveTarget) {
                updateTargetPosition(i, centerX, centerY, getAngle(alpha, i));
            }
        }
    
private voidvibrate()

        final boolean hapticEnabled = Settings.System.getIntForUser(
                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
                UserHandle.USER_CURRENT) != 0;
        if (mVibrator != null && hapticEnabled) {
            mVibrator.vibrate(mVibrationDuration, VIBRATION_ATTRIBUTES);
        }