* Copyright (C) 2012 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fima.glowpadview;
import java.util.ArrayList;
import android.animation.Animator;
import android.animation.Animator.AnimatorListener;
import android.animation.AnimatorListenerAdapter;
import android.animation.TimeInterpolator;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Vibrator;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
/**
* This is a copy of com.android.internal.widget.multiwaveview.GlowPadView with
* minor changes to remove dependencies on private api's.
*
* Contains changes up to If296b60af2421bfa1a9a082e608ba77b2392a218
*
* A re-usable widget containing a center, outer ring and wave animation.
*/
public class GlowPadView extends View {
private static final String TAG = "GlowPadView";
private static final boolean DEBUG = false;
// Wave state machine
private static final int STATE_IDLE = 0;
private static final int STATE_START = 1;
private static final int STATE_FIRST_TOUCH = 2;
private static final int STATE_TRACKING = 3;
private static final int STATE_SNAP = 4;
private static final int STATE_FINISH = 5;
// Animation properties.
private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring
// before we snap to
// it
public interface OnTriggerListener {
int NO_HANDLE = 0;
int CENTER_HANDLE = 1;
public void onGrabbed(View v, int handle);
public void onReleased(View v, int handle);
public void onTrigger(View v, int target);
public void onGrabbedStateChange(View v, int handle);
public void onFinishFinalAnimation();
}
// Tuneable parameters for animation
private static final int WAVE_ANIMATION_DURATION = 1350;
private static final int RETURN_TO_HOME_DELAY = 1200;
private static final int RETURN_TO_HOME_DURATION = 200;
private static final int HIDE_ANIMATION_DELAY = 200;
private static final int HIDE_ANIMATION_DURATION = 200;
private static final int SHOW_ANIMATION_DURATION = 200;
private static final int SHOW_ANIMATION_DELAY = 50;
private static final int INITIAL_SHOW_HANDLE_DURATION = 200;
private static final int REVEAL_GLOW_DELAY = 0;
private static final int REVEAL_GLOW_DURATION = 0;
private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
private static final float TARGET_SCALE_EXPANDED = 1.0f;
private static final float TARGET_SCALE_COLLAPSED = 0.8f;
private static final float RING_SCALE_EXPANDED = 1.0f;
private static final float RING_SCALE_COLLAPSED = 0.5f;
private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
private AnimationBundle mWaveAnimations = new AnimationBundle();
private AnimationBundle mTargetAnimations = new AnimationBundle();
private AnimationBundle mGlowAnimations = new AnimationBundle();
private ArrayList<String> mTargetDescriptions;
private ArrayList<String> mDirectionDescriptions;
private OnTriggerListener mOnTriggerListener;
private TargetDrawable mHandleDrawable;
private TargetDrawable mOuterRing;
private Vibrator mVibrator;
private int mFeedbackCount = 3;
private int mVibrationDuration = 0;
private int mGrabbedState;
private int mActiveTarget = -1;
private float mGlowRadius;
private float mWaveCenterX;
private float mWaveCenterY;
private int mMaxTargetHeight;
private int mMaxTargetWidth;
private float mOuterRadius = 0.0f;
private float mSnapMargin = 0.0f;
private boolean mDragging;
private int mNewTargetResources;
private class AnimationBundle extends ArrayList<Tweener> {
private static final long serialVersionUID = 0xA84D78726F127468L;
private boolean mSuspended;
public void start() {
if (mSuspended)
return; // ignore attempts to start animations
final int count = size();
for (int i = 0; i < count; i++) {
Tweener anim = get(i);
anim.animator.start();
}
}
public void cancel() {
final int count = size();
for (int i = 0; i < count; i++) {
Tweener anim = get(i);
anim.animator.cancel();
}
clear();
}
public void stop() {
final int count = size();
for (int i = 0; i < count; i++) {
Tweener anim = get(i);
anim.animator.end();
}
clear();
}
public void setSuspended(boolean suspend) {
mSuspended = suspend;
}
};
private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animator) {
switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
dispatchOnFinishFinalAnimation();
}
};
private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animator) {
ping();
switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
dispatchOnFinishFinalAnimation();
}
};
private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
public void onAnimationUpdate(ValueAnimator animation) {
invalidate();
}
};
private boolean mAnimatingTargets;
private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() {
public void onAnimationEnd(Animator animator) {
if (mNewTargetResources != 0) {
internalSetTargetResources(mNewTargetResources);
mNewTargetResources = 0;
hideTargets(false, false);
}
mAnimatingTargets = false;
}
};
private int mTargetResourceId;
private int mTargetDescriptionsResourceId;
private int mDirectionDescriptionsResourceId;
private boolean mAlwaysTrackFinger;
private int mHorizontalInset;
private int mVerticalInset;
private int mGravity = Gravity.TOP;
private boolean mInitialLayout = true;
private Tweener mBackgroundAnimator;
private PointCloud mPointCloud;
private float mInnerRadius;
private int mPointerId;
private boolean mShowTargetsOnIdle;
public GlowPadView(Context context) {
this(context, null);
}
public GlowPadView(Context context, 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);
mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration, mVibrationDuration);
mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount, mFeedbackCount);
TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0, 2);
mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
mOuterRing = new TargetDrawable(res, getResourceId(a, R.styleable.GlowPadView_outerRingDrawable), 1);
mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);
int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
Drawable pointDrawable = pointId != 0 ? res.getDrawable(pointId) : null;
mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);
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);
}
a.recycle();
// Use gravity attribute from LinearLayout
// a = context.obtainStyledAttributes(attrs, R.styleable.LinearLayout);
mGravity = a.getInt(R.styleable.GlowPadView_android_gravity, Gravity.TOP);
a.recycle();
setVibrateEnabled(mVibrationDuration > 0);
assignDefaultsIfNeeded();
mPointCloud = new PointCloud(pointDrawable);
mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
mPointCloud.glowManager.setRadius(mGlowRadius);
}
private int getResourceId(TypedArray a, int id) {
TypedValue tv = a.peekValue(id);
return tv == null ? 0 : tv.resourceId;
}
private void dump() {
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);
}
public void suspendAnimations() {
mWaveAnimations.setSuspended(true);
mTargetAnimations.setSuspended(true);
mGlowAnimations.setSuspended(true);
}
public void resumeAnimations() {
mWaveAnimations.setSuspended(false);
mTargetAnimations.setSuspended(false);
mGlowAnimations.setSuspended(false);
mWaveAnimations.start();
mTargetAnimations.start();
mGlowAnimations.start();
}
@Override
protected int getSuggestedMinimumWidth() {
// 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);
}
@Override
protected int getSuggestedMinimumHeight() {
// 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);
}
private int resolveMeasured(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;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int minimumWidth = getSuggestedMinimumWidth();
final int minimumHeight = getSuggestedMinimumHeight();
int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
computeInsets((computedWidth - minimumWidth), (computedHeight - minimumHeight));
setMeasuredDimension(computedWidth, computedHeight);
}
private void switchToState(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);
if (mShowTargetsOnIdle) {
showTargets(true);
}
break;
case STATE_START:
startBackgroundAnimation(0, 0.0f);
if (mShowTargetsOnIdle) {
showTargets(false);
}
break;
case STATE_FIRST_TOUCH:
mHandleDrawable.setAlpha(0.0f);
deactivateTargets();
if (mShowTargetsOnIdle) {
showTargets(false);
} else {
showTargets(true);
}
startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
setGrabbedState(OnTriggerListener.CENTER_HANDLE);
final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager.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 void showGlow(int duration, int delay, float finalAlpha, 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 void hideGlow(int duration, int delay, float finalAlpha, 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 void deactivateTargets() {
final int count = mTargetDrawables.size();
for (int i = 0; i < count; i++) {
TargetDrawable target = mTargetDrawables.get(i);
target.setState(TargetDrawable.STATE_INACTIVE);
}
mActiveTarget = -1;
}
/**
* Dispatches a trigger event to listener. Ignored if a listener is not set.
*
* @param whichTarget
* the target that was triggered.
*/
private void dispatchTriggerEvent(int whichTarget) {
vibrate();
if (mOnTriggerListener != null) {
mOnTriggerListener.onTrigger(this, whichTarget);
}
}
private void dispatchOnFinishFinalAnimation() {
if (mOnTriggerListener != null) {
mOnTriggerListener.onFinishFinalAnimation();
}
}
private void doFinish() {
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);
if (!mShowTargetsOnIdle)
hideTargets(true, false);
}
setGrabbedState(OnTriggerListener.NO_HANDLE);
}
private void highlightSelected(int activeTarget) {
// Highlight the given target and fade others
mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
hideUnselected(activeTarget);
}
private void hideUnselected(int active) {
for (int i = 0; i < mTargetDrawables.size(); i++) {
if (i != active) {
mTargetDrawables.get(i).setAlpha(0.0f);
}
}
}
private void hideTargets(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));
}
final float ringScaleTarget = expanded ? RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
mTargetAnimations.add(Tweener.to(mOuterRing, duration, "ease", interpolator, "alpha", 0.0f, "scaleX", ringScaleTarget, "scaleY", ringScaleTarget,
"delay", delay, "onUpdate", mUpdateListener, "onComplete", mTargetUpdateListener));
mTargetAnimations.start();
}
public void showTargets(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));
}
mTargetAnimations.add(Tweener.to(mOuterRing, duration, "ease", Ease.Cubic.easeOut, "alpha", 1.0f, "scaleX", 1.0f, "scaleY", 1.0f, "delay", delay,
"onUpdate", mUpdateListener, "onComplete", mTargetUpdateListener));
mTargetAnimations.start();
}
private void vibrate() {
if (mVibrator != null) {
mVibrator.vibrate(mVibrationDuration);
}
}
private ArrayList<TargetDrawable> loadDrawableArray(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, 3);
drawables.add(target);
}
array.recycle();
return drawables;
}
private void internalSetTargetResources(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);
}
}
/**
* Loads an array of drawables from the given resourceId.
*
* @param resourceId
*/
public void setTargetResources(int resourceId) {
if (mAnimatingTargets) {
// postpone this change until we return to the initial state
mNewTargetResources = resourceId;
} else {
internalSetTargetResources(resourceId);
}
}
public int getTargetResourceId() {
return mTargetResourceId;
}
/**
* Sets the resource id specifying the target descriptions for
* accessibility.
*
* @param resourceId
* The resource id.
*/
public void setTargetDescriptionsResourceId(int resourceId) {
mTargetDescriptionsResourceId = resourceId;
if (mTargetDescriptions != null) {
mTargetDescriptions.clear();
}
}
/**
* Gets the resource id specifying the target descriptions for
* accessibility.
*
* @return The resource id.
*/
public int getTargetDescriptionsResourceId() {
return mTargetDescriptionsResourceId;
}
/**
* Sets the resource id specifying the target direction descriptions for
* accessibility.
*
* @param resourceId
* The resource id.
*/
public void setDirectionDescriptionsResourceId(int resourceId) {
mDirectionDescriptionsResourceId = resourceId;
if (mDirectionDescriptions != null) {
mDirectionDescriptions.clear();
}
}
/**
* Gets the resource id specifying the target direction descriptions.
*
* @return The resource id.
*/
public int getDirectionDescriptionsResourceId() {
return mDirectionDescriptionsResourceId;
}
/**
* Enable or disable vibrate on touch.
*
* @param enabled
*/
public void setVibrateEnabled(boolean enabled) {
if (enabled && mVibrator == null) {
mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
} else {
mVibrator = null;
}
}
/**
* Starts wave animation.
*
*/
public void ping() {
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 void stopAndHideWaveAnimation() {
mWaveAnimations.cancel();
mPointCloud.waveManager.setAlpha(0.0f);
}
private void startWaveAnimation() {
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();
}
/**
* 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
*/
public void reset(boolean animate) {
mGlowAnimations.stop();
mTargetAnimations.stop();
startBackgroundAnimation(0, 0.0f);
stopAndHideWaveAnimation();
hideTargets(animate, false);
hideGlow(0, 0, 0.0f, null);
Tweener.reset();
}
private void startBackgroundAnimation(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();
}
}
@Override
public boolean onTouchEvent(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);
}
private void updateGlowPosition(float x, float y) {
mPointCloud.glowManager.setX(x);
mPointCloud.glowManager.setY(y);
}
private void handleDown(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 void handleUp(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 void handleCancel(MotionEvent event) {
if (DEBUG && mDragging)
Log.v(TAG, "** Handle CANCEL");
// We should drop the active target here but it interferes with
// moving off the screen in the direction of the navigation bar. At some
// point we may
// want to revisit how we handle this. For now we'll allow a canceled
// event to
// activate the current target.
// mActiveTarget = -1; // Drop the active target if canceled.
int actionIndex = event.findPointerIndex(mPointerId);
actionIndex = actionIndex == -1 ? 0 : actionIndex;
switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
}
private void handleMove(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;
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 = 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 = (i - 0.5) * 2 * Math.PI / ntargets;
double targetMaxRad = (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);
if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
activeTarget = i;
}
}
}
}
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);
target.setState(TargetDrawable.STATE_INACTIVE);
}
// Focus the new target
if (activeTarget != -1) {
TargetDrawable target = targets.get(activeTarget);
target.setState(TargetDrawable.STATE_FOCUSED);
final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
// if (accessibilityManager.isEnabled()) {
// String targetContentDescription =
// getTargetDescription(activeTarget);
// announceForAccessibility(targetContentDescription);
// }
}
}
mActiveTarget = activeTarget;
}
// @Override
// public boolean onHoverEvent(MotionEvent event) {
// final AccessibilityManager accessibilityManager =
// (AccessibilityManager) getContext().getSystemService(
// Context.ACCESSIBILITY_SERVICE);
// if (accessibilityManager.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;
// }
/**
* Sets the current grabbed state, and dispatches a grabbed state change
* event to our listener.
*/
private void setGrabbedState(int newState) {
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);
}
}
}
private boolean trySwitchToFirstTouchState(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 void assignDefaultsIfNeeded() {
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;
}
}
@TargetApi(17)
private void computeInsets(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;
}
}
@Override
protected void onLayout(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 = Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
final float placementHeight = Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
float newWaveCenterX = mHorizontalInset + Math.max(width, mMaxTargetWidth + placementWidth) / 2;
float newWaveCenterY = mVerticalInset + Math.max(height, +mMaxTargetHeight + placementHeight) / 2;
if (mInitialLayout) {
stopAndHideWaveAnimation();
if (mShowTargetsOnIdle)
showTargets(false);
else
hideTargets(false, false);
mInitialLayout = false;
}
mOuterRing.setPositionX(newWaveCenterX);
mOuterRing.setPositionY(newWaveCenterY);
mHandleDrawable.setPositionX(newWaveCenterX);
mHandleDrawable.setPositionY(newWaveCenterY);
updateTargetPositions(newWaveCenterX, newWaveCenterY);
updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
updateGlowPosition(newWaveCenterX, newWaveCenterY);
mWaveCenterX = newWaveCenterX;
mWaveCenterY = newWaveCenterY;
if (DEBUG)
dump();
}
private void updateTargetPositions(float centerX, float centerY) {
// Reposition the target drawables if the view changed.
ArrayList<TargetDrawable> targets = mTargetDrawables;
final int size = targets.size();
final float alpha = (float) (-2.0f * Math.PI / size);
for (int i = 0; i < size; i++) {
final TargetDrawable targetIcon = targets.get(i);
final float angle = alpha * i;
targetIcon.setPositionX(centerX);
targetIcon.setPositionY(centerY);
targetIcon.setX(mOuterRadius * (float) Math.cos(angle));
targetIcon.setY(mOuterRadius * (float) Math.sin(angle));
}
}
private void updatePointCloudPosition(float centerX, float centerY) {
mPointCloud.setCenter(centerX, centerY);
}
@Override
protected void onDraw(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 void setOnTriggerListener(OnTriggerListener listener) {
mOnTriggerListener = listener;
}
private float square(float d) {
return d * d;
}
private float dist2(float dx, float dy) {
return dx * dx + dy * dy;
}
private float getScaledGlowRadiusSquared() {
final float scaledTapRadius;
final AccessibilityManager accessibilityManager = (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE);
if (accessibilityManager.isEnabled()) {
scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
} else {
scaledTapRadius = mGlowRadius;
}
return square(scaledTapRadius);
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void announceTargets() {
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) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
announceForAccessibility(utterance.toString());
}
}
private String getTargetDescription(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);
}
private String getDirectionDescription(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);
}
private ArrayList<String> loadDescriptions(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;
}
public int getResourceIdForTarget(int index) {
final TargetDrawable drawable = mTargetDrawables.get(index);
return drawable == null ? 0 : drawable.getResourceId();
}
public void setEnableTarget(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
}
}
}
/**
* Gets the position of a target in the array that matches the given
* resource.
*
* @param resourceId
* @return the index or -1 if not found
*/
public int getTargetPosition(int resourceId) {
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 boolean replaceTargetDrawables(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;
}
/**
* 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
*/
public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name, int existingResId) {
if (existingResId == 0)
return false;
boolean replaced = false;
if (component != null) {
try {
PackageManager packageManager = getContext().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(getContext().getResources(), existingResId, existingResId);
}
return replaced;
}
public boolean isShowTargetsOnIdle() {
return mShowTargetsOnIdle;
}
public void setShowTargetsOnIdle(boolean mShowTargetsOnIdle) {
this.mShowTargetsOnIdle = mShowTargetsOnIdle;
}
}
======================================================================================================================================================
package com.example.glowpadexample;
import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.Toast;
import com.fima.glowpadview.GlowPadView;
import com.fima.glowpadview.GlowPadView.OnTriggerListener;
public class MainActivity extends Activity implements OnTriggerListener, OnClickListener {
private GlowPadView mGlowPadView;
private Button mBtn;
private Button mOldBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mGlowPadView = (GlowPadView) findViewById(R.id.glow_pad_view);
mGlowPadView.setOnTriggerListener(this);
// uncomment this to make sure the glowpad doesn't vibrate on touch
// mGlowPadView.setVibrateEnabled(false);
// uncomment this to hide targets
mGlowPadView.setShowTargetsOnIdle(true);
mBtn = (Button) findViewById(R.id.btn);
mBtn.setOnClickListener(this);
mOldBtn = (Button) findViewById(R.id.btn_old);
mOldBtn.setOnClickListener(this);
}
@Override
public void onGrabbed(View v, int handle) {
// TODO Auto-generated method stub
}
@Override
public void onReleased(View v, int handle) {
mGlowPadView.ping();
}
@Override
public void onTrigger(View v, int target) {
final int resId = mGlowPadView.getResourceIdForTarget(target);
switch (resId) {
case R.drawable.ic_item_camera:
Toast.makeText(this, "Camera selected", Toast.LENGTH_SHORT).show();
break;
case R.drawable.ic_item_google:
Toast.makeText(this, "Google selected", Toast.LENGTH_SHORT).show();
break;
default:
// Code should never reach here.
}
}
@Override
public void onGrabbedStateChange(View v, int handle) {
// TODO Auto-generated method stub
}
@Override
public void onFinishFinalAnimation() {
// TODO Auto-generated method stub
}
@Override
public void onClick(View v) {
switch(v.getId()) {
case R.id.btn:
mGlowPadView.setTargetResources(R.array.my_snooze_dismiss_drawables);
mGlowPadView.setTargetDescriptionsResourceId(R.array.snooze_dismiss_direction_descriptions);
mGlowPadView.showTargets(true);
//mGlowPadView.reset(false);
//mGlowPadView.requestLayout();
//mGlowPadView.replaceTargetDrawables(getResources(), R.array.snooze_dismiss_drawables, R.array.my_snooze_dismiss_drawables);
break;
case R.id.btn_old:
mGlowPadView.setTargetResources(R.array.snooze_dismiss_drawables);
mGlowPadView.setTargetDescriptionsResourceId(R.array.snooze_dismiss_direction_descriptions);
mGlowPadView.showTargets(true);
break;
default:
break;
}
}
}