diff --git a/GlowPadBackport/.gitignore b/GlowPadBackport/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/GlowPadBackport/.gitignore @@ -0,0 +1 @@ +/build diff --git a/GlowPadBackport/GlowPadBackport.iml b/GlowPadBackport/GlowPadBackport.iml index 7ec238f..0a237cc 100644 --- a/GlowPadBackport/GlowPadBackport.iml +++ b/GlowPadBackport/GlowPadBackport.iml @@ -71,6 +71,7 @@ + diff --git a/GlowPadBackport/build.gradle b/GlowPadBackport/build.gradle index 28927b0..5546dec 100644 --- a/GlowPadBackport/build.gradle +++ b/GlowPadBackport/build.gradle @@ -5,8 +5,8 @@ android { buildToolsVersion "19.0.1" defaultConfig { - minSdkVersion 10 - targetSdkVersion 19 + minSdkVersion 8 + targetSdkVersion 16 versionCode 1 versionName "1.0" } @@ -17,3 +17,7 @@ android { } } } + +dependencies { + compile 'com.nineoldandroids:library:2.4.0' +} \ No newline at end of file diff --git a/GlowPadBackport/src/main/AndroidManifest.xml b/GlowPadBackport/src/main/AndroidManifest.xml index 05d1d2d..e42d285 100644 --- a/GlowPadBackport/src/main/AndroidManifest.xml +++ b/GlowPadBackport/src/main/AndroidManifest.xml @@ -1,4 +1,5 @@ - + package="net.sebastianopoggi.ui.GlowPadBackport" > + + \ No newline at end of file diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/Ease.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/Ease.java new file mode 100644 index 0000000..d45b4d7 --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/Ease.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2011 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 net.sebastianopoggi.ui.GlowPadBackport; + +import net.sebastianopoggi.ui.GlowPadBackport.util.TimeInterpolator; + +class Ease { + private static final float DOMAIN = 1.0f; + private static final float DURATION = 1.0f; + private static final float START = 0.0f; + + static class Linear { + public static final TimeInterpolator easeNone = new TimeInterpolator() { + public float getInterpolation(float input) { + return input; + } + }; + } + + static class Cubic { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * (input /= DURATION) * input * input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * ((input = input / DURATION - 1) * input * input + 1) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input /= DURATION / 2) < 1.0f) ? + (DOMAIN / 2 * input * input * input + START) + : (DOMAIN / 2 * ((input -= 2) * input * input + 2) + START); + } + }; + } + + static class Quad { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * (input /= DURATION) * input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN * (input /= DURATION) * (input - 2) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input /= DURATION / 2) < 1) ? + (DOMAIN / 2 * input * input + START) + : (-DOMAIN / 2 * ((--input) * (input - 2) - 1) + START); + } + }; + } + + static class Quart { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * (input /= DURATION) * input * input * input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN * ((input = input / DURATION - 1) * input * input * input - 1) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input /= DURATION / 2) < 1) ? + (DOMAIN / 2 * input * input * input * input + START) + : + (-DOMAIN / 2 * ((input -= 2) * input * input * input - 2) + START); + } + }; + } + + static class Quint { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * (input /= DURATION) * input * input * input * input + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * ((input = input / DURATION - 1) * input * input * input * input + 1) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return ((input /= DURATION / 2) < 1) ? + (DOMAIN / 2 * input * input * input * input * input + START) + : + (DOMAIN / 2 * ((input -= 2) * input * input * input * input + 2) + START); + } + }; + } + + static class Sine { + public static final TimeInterpolator easeIn = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN * (float) Math.cos(input / DURATION * (Math.PI / 2)) + DOMAIN + START; + } + }; + public static final TimeInterpolator easeOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return DOMAIN * (float) Math.sin(input / DURATION * (Math.PI / 2)) + START; + } + }; + public static final TimeInterpolator easeInOut = new TimeInterpolator() { + public float getInterpolation(float input) { + return -DOMAIN / 2 * ((float) Math.cos(Math.PI * input / DURATION) - 1.0f) + START; + } + }; + } + +} diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/GlowPadView.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/GlowPadView.java new file mode 100644 index 0000000..321c0b3 --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/GlowPadView.java @@ -0,0 +1,1484 @@ +/* + * 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 net.sebastianopoggi.ui.GlowPadBackport; + +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.provider.Settings; +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; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ValueAnimator; +import net.sebastianopoggi.ui.GlowPadBackport.util.Const; +import net.sebastianopoggi.ui.GlowPadBackport.util.TimeInterpolator; + +import java.util.ArrayList; + +/** 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 mTargetDrawables = new ArrayList(); + private AnimationBundle mWaveAnimations = new AnimationBundle(); + private AnimationBundle mTargetAnimations = new AnimationBundle(); + private AnimationBundle mGlowAnimations = new AnimationBundle(); + private ArrayList mTargetDescriptions; + private ArrayList 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 mRingScaleFactor = 1f; + private boolean mAllowScaling; + + private float mOuterRadius = 0.0f; + private float mSnapMargin = 0.0f; + private float mFirstItemOffset = 0.0f; + private boolean mMagneticTargets = false; + private boolean mDragging; + private int mNewTargetResources; + + private class AnimationBundle extends ArrayList { + 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 Animator.AnimatorListener mResetListener = new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); + dispatchOnFinishFinalAnimation(); + } + }; + + private Animator.AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() { + public void onAnimationEnd(Animator animator) { + ping(); + switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY); + dispatchOnFinishFinalAnimation(); + } + }; + + private ValueAnimator.AnimatorUpdateListener mUpdateListener = new ValueAnimator.AnimatorUpdateListener() { + public void onAnimationUpdate(ValueAnimator animation) { + invalidate(); + } + }; + + private boolean mAnimatingTargets; + private Animator.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; + + 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); + 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 ? 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); + } + + mGravity = a.getInt(R.styleable.GlowPadView_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); + } + + /** This gets the suggested width accounting for the ring's scale factor. */ + protected int getScaledSuggestedMinimumWidth() { + return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + + mMaxTargetWidth); + } + + /** This gets the suggested height accounting for the ring's scale factor. */ + protected int getScaledSuggestedMinimumHeight() { + return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + + mMaxTargetHeight); + } + + private int resolveMeasured(int measureSpec, int desired) { + int result; + 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; + } + + 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); + 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); + + 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, + 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(); + } + + public void setPointsMultiplier(int mult) { + mPointCloud.setPointsMultiplier(mult); + } + + public int getPointsMultiplier() { + return mPointCloud.getPointsMultiplier(); + } + + private void hideGlow(int duration, int delay, float finalAlpha, + 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 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); + 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)); + } + + 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 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)); + } + + 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 void vibrate() { + final boolean hapticEnabled; + hapticEnabled = + Settings.System.getInt(getContext().getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1) != 0; + + if (mVibrator != null && hapticEnabled) { + mVibrator.vibrate(mVibrationDuration); + } + } + + private ArrayList loadDrawableArray(int resourceId) { + Resources res = getContext().getResources(); + TypedArray array = res.obtainTypedArray(resourceId); + final int count = array.length(); + ArrayList drawables = new ArrayList(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; + } + + private void internalSetTargetResources(int resourceId) { + final ArrayList 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 The ID of the Array resource with the new Drawables. + */ + 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 True to enable vibration, false otherwise. + */ + 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 When true, an animation is used to reset to the default state. + */ + 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 + @TargetApi(Build.VERSION_CODES.FROYO) + public boolean onTouchEvent(MotionEvent event) { + final int action; + if (Const.IS_FROYO) { + action = event.getActionMasked(); + } + else { + action = event.getAction(); + } + + 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 || super.onTouchEvent(event); + } + + private void updateGlowPosition(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); + } + + @TargetApi(Build.VERSION_CODES.FROYO) + private void handleDown(MotionEvent event) { + int actionIndex = 0; + if (Const.IS_FROYO) { + actionIndex = event.getActionIndex(); + } + float eventX; + float eventY; + + if (Const.IS_FROYO) { + eventX = event.getX(actionIndex); + eventY = event.getY(actionIndex); + } + else { + eventX = event.getX(); + eventY = event.getY(); + } + + switchToState(STATE_START, eventX, eventY); + if (!trySwitchToFirstTouchState(eventX, eventY)) { + mDragging = false; + } + else { + if (Const.IS_ECLAIR) { + mPointerId = event.getPointerId(actionIndex); + } + + updateGlowPosition(eventX, eventY); + } + } + + @TargetApi(Build.VERSION_CODES.FROYO) + private void handleUp(MotionEvent event) { + if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE"); + + if (Const.IS_FROYO) { + int actionIndex = event.getActionIndex(); + + if (event.getPointerId(actionIndex) == mPointerId) { + switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); + } + } + else { + switchToState(STATE_FINISH, event.getX(), event.getY()); + } + } + + @TargetApi(Build.VERSION_CODES.FROYO) + private void handleCancel(MotionEvent event) { + if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL"); + + // Drop the active target if canceled. + mActiveTarget = -1; + + if (Const.IS_FROYO) { + int actionIndex = event.getActionIndex(); + actionIndex = actionIndex == -1 ? 0 : actionIndex; + + switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex)); + } + else { + switchToState(STATE_FINISH, event.getX(), event.getY()); + } + } + + @TargetApi(Build.VERSION_CODES.JELLY_BEAN) + private void handleMove(MotionEvent event) { + int activeTarget = -1; + final int historySize = event.getHistorySize(); + ArrayList targets = mTargetDrawables; + int ntargets = targets.size(); + float x = 0.0f; + float y = 0.0f; + float activeAngle = 0.0f; + + int actionIndex = 0; + if (Const.IS_ECLAIR) { + actionIndex = event.findPointerIndex(mPointerId); + + if (actionIndex == -1) { + return; // no data for this pointer + } + } + + for (int k = 0; k < historySize + 1; k++) { + float eventX; + float eventY; + + if (Const.IS_FROYO) { + eventX = k < historySize ? event.getHistoricalX(actionIndex, k) + : event.getX(actionIndex); + eventY = k < historySize ? event.getHistoricalY(actionIndex, k) + : event.getY(actionIndex); + } + else { + eventX = k < historySize ? event.getHistoricalX(k) + : event.getX(); + eventY = k < historySize ? event.getHistoricalY(k) + : event.getY(); + } + + // 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); + } + + AccessibilityManager accessibilityManager = + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (accessibilityManager.isEnabled()) { + String targetContentDescription = getTargetDescription(activeTarget); + if (Const.IS_JB) { + announceForAccessibility(targetContentDescription); + } + } + } + } + mActiveTarget = activeTarget; + } + + @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) + @Override + public boolean onHoverEvent(MotionEvent event) { + AccessibilityManager accessibilityManager = + (AccessibilityManager) getContext().getSystemService(Context.ACCESSIBILITY_SERVICE); + + if (Const.IS_ICS && 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(Build.VERSION_CODES.JELLY_BEAN_MR1) + private void computeInsets(int dx, int dy) { + final int layoutDirection; + int absoluteGravity = mGravity; + if (Const.IS_JB_MR1) { + layoutDirection = getLayoutDirection(); + 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; + } + } + + /** + * Given the desired width and height of the ring and the allocated width and height, compute + * how much we need to scale the ring. + */ + @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1) + private float computeScaleFactor(int desiredWidth, int desiredHeight, + int actualWidth, int actualHeight) { + + // Return unity if scaling is not allowed. + if (!mAllowScaling) return 1f; + + final int layoutDirection; + int absoluteGravity = mGravity; + if (Const.IS_JB_MR1) { + layoutDirection = getLayoutDirection(); + 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); + } + + @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); + + mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight, + computedWidth, computedHeight); + + int scaledWidth = getScaledSuggestedMinimumWidth(); + int scaledHeight = getScaledSuggestedMinimumHeight(); + + computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight); + setMeasuredDimension(computedWidth, computedHeight); + } + + private float getRingWidth() { + return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius); + } + + private float getRingHeight() { + return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius); + } + + @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 = 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(); + } + + private void updateTargetPosition(int i, float centerX, float centerY) { + final float angle = getAngle(getSliceAngle(), i); + updateTargetPosition(i, centerX, centerY, angle); + } + + private void updateTargetPosition(int i, float centerX, float centerY, float angle) { + final float placementRadiusX = getRingWidth() / 2; + final float placementRadiusY = getRingHeight() / 2; + if (i >= 0) { + ArrayList 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 void updateTargetPositions(float centerX, float centerY) { + updateTargetPositions(centerX, centerY, false); + } + + private void updateTargetPositions(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 float getAngle(float alpha, int i) { + return mFirstItemOffset + alpha * i; + } + + private float getSliceAngle() { + return (float) (-2.0f * Math.PI / mTargetDrawables.size()); + } + + 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; + + 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 (Const.IS_JB && utterance.length() > 0) { + 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 loadDescriptions(int resourceId) { + TypedArray array = getContext().getResources().obtainTypedArray(resourceId); + final int count = array.length(); + ArrayList targetContentDescriptions = new ArrayList(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 The ID of the resource to find in the current array. + * + * @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; + } + + private boolean replaceTargetDrawables(Resources res, int existingResourceId, + int newResourceId) { + if (existingResourceId == 0 || newResourceId == 0) { + return false; + } + + boolean result = false; + final ArrayList 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; + Context context = getContext(); + + boolean replaced = false; + if (component != null) { + try { + PackageManager packageManager = context.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(context.getResources(), existingResId, existingResId); + } + return replaced; + } +} diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/PointCloud.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/PointCloud.java new file mode 100644 index 0000000..f617aba --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/PointCloud.java @@ -0,0 +1,247 @@ +/* + * 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 net.sebastianopoggi.ui.GlowPadBackport; + +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.util.FloatMath; +import android.util.Log; + +import java.util.ArrayList; + +public class PointCloud { + private static final float MIN_POINT_SIZE = 2.0f; + private static final float MAX_POINT_SIZE = 4.0f; + private static int INNER_POINTS = 8; + private static final String TAG = "PointCloud"; + private ArrayList mPointCloud = new ArrayList(); + private Drawable mDrawable; + private float mCenterX; + private float mCenterY; + private Paint mPaint; + private float mScale = 1.0f; + private static final float PI = (float) Math.PI; + + // These allow us to have multiple concurrent animations. + WaveManager waveManager = new WaveManager(); + GlowManager glowManager = new GlowManager(); + private float mOuterRadius, mInnerRadius; + + public class WaveManager { + private float radius = 50; + private float width = 200.0f; // TODO: Make configurable + private float alpha = 0.0f; + + public void setRadius(float r) { + radius = r; + } + + public float getRadius() { + return radius; + } + + public void setAlpha(float a) { + alpha = a; + } + + public float getAlpha() { + return alpha; + } + } + + public class GlowManager { + private float x; + private float y; + private float radius = 0.0f; + private float alpha = 0.0f; + + public void setX(float x1) { + x = x1; + } + + public float getX() { + return x; + } + + public void setY(float y1) { + y = y1; + } + + public float getY() { + return y; + } + + public void setAlpha(float a) { + alpha = a; + } + + public float getAlpha() { + return alpha; + } + + public void setRadius(float r) { + radius = r; + } + + public float getRadius() { + return radius; + } + } + + class Point { + float x; + float y; + float radius; + + public Point(float x2, float y2, float r) { + x = x2; + y = y2; + radius = r; + } + } + + public PointCloud(Drawable drawable) { + mPaint = new Paint(); + mPaint.setFilterBitmap(true); + mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable + mPaint.setAntiAlias(true); + mPaint.setDither(true); + + mDrawable = drawable; + if (mDrawable != null) { + drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + } + } + + /*package*/ void setPointsMultiplier(int mult) { + INNER_POINTS = mult; + makePointCloud(mInnerRadius, mOuterRadius); + } + + /*package*/ int getPointsMultiplier() { + return INNER_POINTS; + } + + public void setCenter(float x, float y) { + mCenterX = x; + mCenterY = y; + } + + public void makePointCloud(float innerRadius, float outerRadius) { + if (innerRadius == 0) { + Log.w(TAG, "Must specify an inner radius"); + return; + } + mOuterRadius = outerRadius; + mInnerRadius = innerRadius; + mPointCloud.clear(); + final float pointAreaRadius = (outerRadius - innerRadius); + final float ds = (2.0f * PI * innerRadius / INNER_POINTS); + final int bands = Math.round(pointAreaRadius / ds); + final float dr = pointAreaRadius / bands; + float r = innerRadius; + for (int b = 0; b <= bands; b++, r += dr) { + float circumference = 2.0f * PI * r; + final int pointsInBand = (int) (circumference / ds); + float eta = PI / 2.0f; + float dEta = 2.0f * PI / pointsInBand; + for (int i = 0; i < pointsInBand; i++) { + float x = r * FloatMath.cos(eta); + float y = r * FloatMath.sin(eta); + eta += dEta; + mPointCloud.add(new Point(x, y, r)); + } + } + } + + public void setScale(float scale) { + mScale = scale; + } + + public float getScale() { + return mScale; + } + + private static float hypot(float x, float y) { + return FloatMath.sqrt(x * x + y * y); + } + + private static float max(float a, float b) { + return a > b ? a : b; + } + + public int getAlphaForPoint(Point point) { + // Contribution from positional glow + float glowDistance = hypot(glowManager.x - point.x, glowManager.y - point.y); + float glowAlpha = 0.0f; + if (glowDistance < glowManager.radius) { + float cosf = FloatMath.cos(PI * 0.25f * glowDistance / glowManager.radius); + glowAlpha = glowManager.alpha * max(0.0f, (float) Math.pow(cosf, 10.0f)); + } + + // Compute contribution from Wave + float radius = hypot(point.x, point.y); + float distanceToWaveRing = (radius - waveManager.radius); + float waveAlpha = 0.0f; + if (distanceToWaveRing < waveManager.width * 0.5f && distanceToWaveRing < 0.0f) { + float cosf = FloatMath.cos(PI * 0.25f * distanceToWaveRing / waveManager.width); + waveAlpha = waveManager.alpha * max(0.0f, (float) Math.pow(cosf, 20.0f)); + } + + return (int) (max(glowAlpha, waveAlpha) * 255); + } + + private float interp(float min, float max, float f) { + return min + (max - min) * f; + } + + public void draw(Canvas canvas) { + ArrayList points = mPointCloud; + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(mScale, mScale, mCenterX, mCenterY); + for (int i = 0; i < points.size(); i++) { + Point point = points.get(i); + final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE, + point.radius / mOuterRadius); + final float px = point.x + mCenterX; + final float py = point.y + mCenterY; + int alpha = getAlphaForPoint(point); + + if (alpha == 0) continue; + + if (mDrawable != null) { + canvas.save(Canvas.MATRIX_SAVE_FLAG); + final float cx = mDrawable.getIntrinsicWidth() * 0.5f; + final float cy = mDrawable.getIntrinsicHeight() * 0.5f; + final float s = pointSize / MAX_POINT_SIZE; + canvas.scale(s, s, px, py); + canvas.translate(px - cx, py - cy); + mDrawable.setAlpha(alpha); + mDrawable.draw(canvas); + canvas.restore(); + } + else { + mPaint.setAlpha(alpha); + canvas.drawCircle(px, py, pointSize, mPaint); + } + } + canvas.restore(); + } + +} diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/TargetDrawable.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/TargetDrawable.java new file mode 100644 index 0000000..9db955d --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/TargetDrawable.java @@ -0,0 +1,341 @@ +/* + * Copyright (C) 2011 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 net.sebastianopoggi.ui.GlowPadBackport; + +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.StateListDrawable; +import android.util.Log; + +import java.lang.reflect.Method; + +public class TargetDrawable { + private static final String TAG = "TargetDrawable"; + private static final boolean DEBUG = false; + + public static final int[] STATE_ACTIVE = + {android.R.attr.state_enabled, android.R.attr.state_active}; + public static final int[] STATE_INACTIVE = + {android.R.attr.state_enabled, -android.R.attr.state_active}; + public static final int[] STATE_FOCUSED = + {android.R.attr.state_enabled, -android.R.attr.state_active, + android.R.attr.state_focused}; + + // We're using Reflection to access these private APIs on older Android versions + static Method mGetStateDrawableIndex, mGetStateCount, mGetStateDrawable; + + static { + // In this static block we initialize all reflected methods (so that we can work around + // the @hide annotations for those Android Framework methods). This is not ideal, but + // there's not much we can do about that either. + try { + mGetStateCount = StateListDrawable.class.getDeclaredMethod("getStateCount"); + mGetStateCount.setAccessible(true); + } + catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't access the StateListDrawable#getStateCount() method. " + + "Some stuff might break!", e); + } + try { + mGetStateDrawable = StateListDrawable.class.getDeclaredMethod("getStateDrawable", int.class); + mGetStateDrawable.setAccessible(true); + } + catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't access the StateListDrawable#getStateDrawable(int) method. " + + "Some stuff might break!", e); + } + try { + mGetStateDrawableIndex = StateListDrawable.class.getDeclaredMethod("getStateDrawableIndex", int[].class); + mGetStateDrawableIndex.setAccessible(true); + } + catch (NoSuchMethodException e) { + Log.e(TAG, "Couldn't access the StateListDrawable#mGetStateDrawableIndex(int[]) method. " + + "Some stuff might break!", e); + } + } + + private float mTranslationX = 0.0f; + private float mTranslationY = 0.0f; + private float mPositionX = 0.0f; + private float mPositionY = 0.0f; + private float mScaleX = 1.0f; + private float mScaleY = 1.0f; + private float mAlpha = 1.0f; + private Drawable mDrawable; + private boolean mEnabled = true; + private final int mResourceId; + + /* package */ static class DrawableWithAlpha extends Drawable { + private float mAlpha = 1.0f; + private Drawable mRealDrawable; + + public DrawableWithAlpha(Drawable realDrawable) { + mRealDrawable = realDrawable; + } + + public void setAlphaFloat(float alpha) { + mAlpha = alpha; + } + + public int getAlphaFloat() { + return (int) (mAlpha * 255); + } + + public void draw(Canvas canvas) { + mRealDrawable.setAlpha(Math.round(mAlpha * 255f)); + mRealDrawable.draw(canvas); + } + + @Override + public void setAlpha(int alpha) { + mRealDrawable.setAlpha(alpha); + } + + @Override + public int getAlpha() { + return mRealDrawable.getAlpha(); + } + + @Override + public void setColorFilter(ColorFilter cf) { + mRealDrawable.setColorFilter(cf); + } + + @Override + public int getOpacity() { + return mRealDrawable.getOpacity(); + } + } + + public TargetDrawable(Resources res, int resId) { + mResourceId = resId; + setDrawable(res, resId); + } + + public void setDrawable(Resources res, int resId) { + // Note we explicitly don't set mResourceId to resId since we allow the drawable to be + // swapped at runtime and want to re-use the existing resource id for identification. + Drawable drawable = resId == 0 ? null : res.getDrawable(resId); + // Mutate the drawable so we can animate shared drawable properties. + mDrawable = drawable != null ? drawable.mutate() : null; + resizeDrawables(); + setState(STATE_INACTIVE); + } + + public TargetDrawable(TargetDrawable other) { + mResourceId = other.mResourceId; + // Mutate the drawable so we can animate shared drawable properties. + mDrawable = other.mDrawable != null ? other.mDrawable.mutate() : null; + resizeDrawables(); + setState(STATE_INACTIVE); + } + + public void setState(int[] state) { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + d.setState(state); + } + } + + public boolean hasState(int[] state) { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + // TODO: this doesn't seem to work + try { + return (Integer) mGetStateDrawableIndex.invoke(d, state) != -1; + } + catch (Exception e) { + Log.w(TAG, "StateListDrawable#getStateDrawableIndex(int[]) call failed!", e); + } + } + return false; + } + + /** + * Returns true if the drawable is a StateListDrawable and is in the focused state. + * + * @return Returns true if the drawable is a StateListDrawable and is in the focused state + */ + public boolean isActive() { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + int[] states = d.getState(); + for (int i = 0; i < states.length; i++) { + if (states[i] == android.R.attr.state_focused) { + return true; + } + } + } + return false; + } + + /** + * Returns true if this target is enabled. Typically an enabled target contains a valid + * drawable in a valid state. Currently all targets with valid drawables are valid. + * + * @return Returns true if the target is enabled, false otherwise + */ + public boolean isEnabled() { + return mDrawable != null && mEnabled; + } + + /** + * Makes drawables in a StateListDrawable all the same dimensions. + * If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the + * drawable. + */ + private void resizeDrawables() { + if (mDrawable instanceof StateListDrawable) { + StateListDrawable d = (StateListDrawable) mDrawable; + int maxWidth = 0; + int maxHeight = 0; + Integer stateCount = 0; + try { + stateCount = (Integer) mGetStateCount.invoke(d); + } + catch (Exception e) { + Log.w(TAG, "StateListDrawable#getStateCount() call failed!", e); + } + + for (int i = 0; i < stateCount; i++) { + Drawable childDrawable; + try { + childDrawable = (Drawable) mGetStateDrawable.invoke(d, i); + } + catch (Exception e) { + Log.w(TAG, "StateListDrawable#getStateDrawable(int) call failed!", e); + continue; + } + + maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth()); + maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight()); + } + if (DEBUG) { + Log.v(TAG, "union of childDrawable rects " + d + " to: " + + maxWidth + "x" + maxHeight); + } + d.setBounds(0, 0, maxWidth, maxHeight); + for (int i = 0; i < stateCount; i++) { + Drawable childDrawable; + try { + childDrawable = (Drawable) mGetStateDrawable.invoke(d, i); + } + catch (Exception e) { + Log.w(TAG, "StateListDrawable#getStateDrawable(int) call failed!", e); + continue; + } + + if (DEBUG) { + Log.v(TAG, "sizing drawable " + childDrawable + " to: " + + maxWidth + "x" + maxHeight); + } + childDrawable.setBounds(0, 0, maxWidth, maxHeight); + } + } + else if (mDrawable != null) { + mDrawable.setBounds(0, 0, + mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight()); + } + } + + public void setX(float x) { + mTranslationX = x; + } + + public void setY(float y) { + mTranslationY = y; + } + + public void setScaleX(float x) { + mScaleX = x; + } + + public void setScaleY(float y) { + mScaleY = y; + } + + public void setAlpha(float alpha) { + mAlpha = alpha; + } + + public float getX() { + return mTranslationX; + } + + public float getY() { + return mTranslationY; + } + + public float getScaleX() { + return mScaleX; + } + + public float getScaleY() { + return mScaleY; + } + + public float getAlpha() { + return mAlpha; + } + + public void setPositionX(float x) { + mPositionX = x; + } + + public void setPositionY(float y) { + mPositionY = y; + } + + public float getPositionX() { + return mPositionX; + } + + public float getPositionY() { + return mPositionY; + } + + public int getWidth() { + return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0; + } + + public int getHeight() { + return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0; + } + + public void draw(Canvas canvas) { + if (mDrawable == null || !mEnabled) { + return; + } + canvas.save(Canvas.MATRIX_SAVE_FLAG); + canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY); + canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY); + canvas.translate(-0.5f * getWidth(), -0.5f * getHeight()); + mDrawable.setAlpha(Math.round(mAlpha * 255f)); + mDrawable.draw(canvas); + canvas.restore(); + } + + public void setEnabled(boolean enabled) { + mEnabled = enabled; + } + + public int getResourceId() { + return mResourceId; + } +} diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/Tweener.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/Tweener.java new file mode 100644 index 0000000..73a4003 --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/Tweener.java @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2011 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 net.sebastianopoggi.ui.GlowPadBackport; + +import android.util.Log; +import com.nineoldandroids.animation.Animator; +import com.nineoldandroids.animation.Animator.AnimatorListener; +import com.nineoldandroids.animation.AnimatorListenerAdapter; +import com.nineoldandroids.animation.ObjectAnimator; +import com.nineoldandroids.animation.PropertyValuesHolder; +import com.nineoldandroids.animation.ValueAnimator.AnimatorUpdateListener; +import net.sebastianopoggi.ui.GlowPadBackport.util.TimeInterpolator; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map.Entry; + +class Tweener { + private static final String TAG = "Tweener"; + private static final boolean DEBUG = false; + + ObjectAnimator animator; + private static HashMap sTweens = new HashMap(); + + public Tweener(ObjectAnimator anim) { + animator = anim; + } + + private static void remove(Animator animator) { + Iterator> iter = sTweens.entrySet().iterator(); + while (iter.hasNext()) { + Entry entry = iter.next(); + if (entry.getValue().animator == animator) { + if (DEBUG) { + Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey()) + + " sTweens.size() = " + sTweens.size()); + } + iter.remove(); + break; // an animator can only be attached to one object + } + } + } + + public static Tweener to(Object object, long duration, Object... vars) { + long delay = 0; + AnimatorUpdateListener updateListener = null; + AnimatorListener listener = null; + TimeInterpolator interpolator = null; + + // Iterate through arguments and discover properties to animate + ArrayList props = new ArrayList(vars.length / 2); + for (int i = 0; i < vars.length; i += 2) { + if (!(vars[i] instanceof String)) { + throw new IllegalArgumentException("Key must be a string: " + vars[i]); + } + String key = (String) vars[i]; + Object value = vars[i + 1]; + if ("simultaneousTween".equals(key)) { + // TODO + } + else if ("ease".equals(key)) { + interpolator = (TimeInterpolator) value; // TODO: multiple interpolators? + } + else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) { + updateListener = (AnimatorUpdateListener) value; + } + else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) { + listener = (AnimatorListener) value; + } + else if ("delay".equals(key)) { + delay = ((Number) value).longValue(); + } + else if ("syncWith".equals(key)) { + // TODO + } + else if (value instanceof float[]) { + props.add(PropertyValuesHolder.ofFloat(key, + ((float[]) value)[0], ((float[]) value)[1])); + } + else if (value instanceof int[]) { + props.add(PropertyValuesHolder.ofInt(key, + ((int[]) value)[0], ((int[]) value)[1])); + } + else if (value instanceof Number) { + float floatValue = ((Number) value).floatValue(); + props.add(PropertyValuesHolder.ofFloat(key, floatValue)); + } + else { + throw new IllegalArgumentException( + "Bad argument for key \"" + key + "\" with value " + value.getClass()); + } + } + + // Re-use existing tween, if present + Tweener tween = sTweens.get(object); + ObjectAnimator anim; + if (tween == null) { + anim = ObjectAnimator.ofPropertyValuesHolder(object, + props.toArray(new PropertyValuesHolder[props.size()])); + tween = new Tweener(anim); + sTweens.put(object, tween); + if (DEBUG) Log.v(TAG, "Added new Tweener " + tween); + } + else { + anim = sTweens.get(object).animator; + replace(props, object); // Cancel all animators for given object + } + + if (interpolator != null) { + anim.setInterpolator(interpolator); + } + + // Update animation with properties discovered in loop above + anim.setStartDelay(delay); + anim.setDuration(duration); + if (updateListener != null) { + anim.removeAllUpdateListeners(); // There should be only one + anim.addUpdateListener(updateListener); + } + if (listener != null) { + anim.removeAllListeners(); // There should be only one. + anim.addListener(listener); + } + anim.addListener(mCleanupListener); + + return tween; + } + + Tweener from(Object object, long duration, Object... vars) { + // TODO: for v of vars + // toVars[v] = object[v] + // object[v] = vars[v] + return Tweener.to(object, duration, vars); + } + + // Listener to watch for completed animations and remove them. + private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() { + + @Override + public void onAnimationEnd(Animator animation) { + remove(animation); + } + + @Override + public void onAnimationCancel(Animator animation) { + remove(animation); + } + }; + + public static void reset() { + if (DEBUG) { + Log.v(TAG, "Reset()"); + if (sTweens.size() > 0) { + Log.v(TAG, "Cleaning up " + sTweens.size() + " animations"); + } + } + sTweens.clear(); + } + + private static void replace(ArrayList props, Object... args) { + for (final Object killobject : args) { + Tweener tween = sTweens.get(killobject); + if (tween != null) { + tween.animator.cancel(); + if (props != null) { + tween.animator.setValues( + props.toArray(new PropertyValuesHolder[props.size()])); + } + else { + sTweens.remove(tween); + } + } + } + } +} diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/util/Const.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/util/Const.java new file mode 100644 index 0000000..e337038 --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/util/Const.java @@ -0,0 +1,22 @@ +package net.sebastianopoggi.ui.GlowPadBackport.util; + +import android.os.Build; + +/** + * Class + *

+ * Author: Sebastiano Poggi + * Created on: 1/28/13 Time: 3:57 PM + * File version: 1.0 + *

+ * Changelog: + * Version 1.0 + * * Initial revision + */ +public class Const { + public static final boolean IS_ECLAIR = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ECLAIR; + public static final boolean IS_FROYO = Build.VERSION.SDK_INT >= Build.VERSION_CODES.FROYO; + public static final boolean IS_ICS = Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH; + public static final boolean IS_JB = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN; + public static final boolean IS_JB_MR1 = Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1; +} diff --git a/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/util/TimeInterpolator.java b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/util/TimeInterpolator.java new file mode 100644 index 0000000..4bb36e6 --- /dev/null +++ b/GlowPadBackport/src/main/java/net/sebastianopoggi/ui/GlowPadBackport/util/TimeInterpolator.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 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 net.sebastianopoggi.ui.GlowPadBackport.util; + +import android.view.animation.Interpolator; + +/** + * A time interpolator defines the rate of change of an animation. This allows animations + * to have non-linear motion, such as acceleration and deceleration. + */ +public interface TimeInterpolator extends Interpolator { + + /** + * Maps a value representing the elapsed fraction of an animation to a value that represents + * the interpolated fraction. This interpolated value is then multiplied by the change in + * value of an animation to derive the animated value at the current elapsed animation time. + * + * @param input A value between 0 and 1.0 indicating our current point + * in the animation where 0 represents the start and 1.0 represents + * the end + * @return The interpolation value. This value can be more than 1.0 for + * interpolators which overshoot their targets, or less than 0 for + * interpolators that undershoot their targets. + */ + float getInterpolation(float input); +} diff --git a/GlowPadBackport/src/main/res/drawable-hdpi/ic_launcher.png b/GlowPadBackport/src/main/res/drawable-hdpi/ic_launcher.png deleted file mode 100644 index 96a442e..0000000 Binary files a/GlowPadBackport/src/main/res/drawable-hdpi/ic_launcher.png and /dev/null differ diff --git a/GlowPadBackport/src/main/res/drawable-mdpi/ic_launcher.png b/GlowPadBackport/src/main/res/drawable-mdpi/ic_launcher.png deleted file mode 100644 index 359047d..0000000 Binary files a/GlowPadBackport/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ diff --git a/GlowPadBackport/src/main/res/drawable-xhdpi/ic_launcher.png b/GlowPadBackport/src/main/res/drawable-xhdpi/ic_launcher.png deleted file mode 100644 index 71c6d76..0000000 Binary files a/GlowPadBackport/src/main/res/drawable-xhdpi/ic_launcher.png and /dev/null differ diff --git a/GlowPadBackport/src/main/res/drawable-xxhdpi/ic_launcher.png b/GlowPadBackport/src/main/res/drawable-xxhdpi/ic_launcher.png deleted file mode 100644 index 4df1894..0000000 Binary files a/GlowPadBackport/src/main/res/drawable-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/GlowPadBackport/src/main/res/values/attrs.xml b/GlowPadBackport/src/main/res/values/attrs.xml new file mode 100644 index 0000000..8e453b5 --- /dev/null +++ b/GlowPadBackport/src/main/res/values/attrs.xml @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/GlowPadBackport/src/main/res/values/strings.xml b/GlowPadBackport/src/main/res/values/strings.xml deleted file mode 100644 index 47a7dd3..0000000 --- a/GlowPadBackport/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - GlowPadBackport - diff --git a/HypoAlarm/HypoAlarm-HypoAlarm.iml b/HypoAlarm/HypoAlarm-HypoAlarm.iml index 893cde5..4ac6fc1 100644 --- a/HypoAlarm/HypoAlarm-HypoAlarm.iml +++ b/HypoAlarm/HypoAlarm-HypoAlarm.iml @@ -73,6 +73,8 @@ + + diff --git a/HypoAlarm/build.gradle b/HypoAlarm/build.gradle index 58bf143..e1d0fe9 100644 --- a/HypoAlarm/build.gradle +++ b/HypoAlarm/build.gradle @@ -17,11 +17,16 @@ android { } } } - +repositories { + flatDir { + dirs 'libs' + } +} dependencies { compile 'com.android.support:support-v4:19.0.1' compile 'com.android.support:appcompat-v7:19.0.1' - compile 'com.nineoldandroids:library:2.4.0' compile fileTree(dir: 'libs', include: ['*.aar']) compile fileTree(dir: 'libs', include: ['*.jar']) + compile project(':GlowPadBackport') + compile project(':SeekArc') } diff --git a/HypoAlarm/src/main/AndroidManifest.xml b/HypoAlarm/src/main/AndroidManifest.xml index 4a9f3a9..b406700 100644 --- a/HypoAlarm/src/main/AndroidManifest.xml +++ b/HypoAlarm/src/main/AndroidManifest.xml @@ -30,6 +30,7 @@ + @@ -47,6 +48,6 @@ - + diff --git a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmAlertActivity.java b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmAlertActivity.java index d9f1390..925d026 100644 --- a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmAlertActivity.java +++ b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmAlertActivity.java @@ -8,13 +8,13 @@ import android.content.Intent; import android.os.Build; import android.os.Bundle; import android.os.Handler; -import android.os.Vibrator; import android.util.Log; -import android.view.View; import android.view.Window; import android.view.WindowManager; import android.widget.Button; +import com.triggertrap.seekarc.SeekArc; + // TODO See GlowPad. // TODO sound audible alarm -- see AlarmKlaxon.java @@ -22,16 +22,17 @@ import android.widget.Button; // TODO Snooze? set another alarm for the next half-hour (or grace_period / 2)? public class AlarmAlertActivity extends Activity { - // TODO correct alert lifetime - private static final int ALERT_LIFE = 1000*10; //1000*60*2; // 2 minutes - private static final long[] vPattern = {500, 500}; - private static Boolean userCancelled; - private static Vibrator vibrator; private static Intent notifyIntent; + public static Boolean alertFinished, userCancelled; + public static Activity alertActivity; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + // Self-reference so we can run finish() from outside. + alertActivity = this; + requestWindowFeature(Window.FEATURE_NO_TITLE); Window window = getWindow(); // Set to use the full screen @@ -47,32 +48,15 @@ public class AlarmAlertActivity extends Activity { setContentView(R.layout.alarm_alert); notifyIntent = new Intent(getApplicationContext(), AlarmNotify.class); - // Disable any current notifications + // Disable any current notifications (if we're snoozing) stopService(notifyIntent); - // Turn off the alert activity, and switch to a notification - new Handler().postDelayed(new Runnable() { - public void run() { - // Close the dialogue and switch to notification - // if the Activity has not been closed by the user - if (!userCancelled) { - startService(notifyIntent); - finish(); - } - } - }, ALERT_LIFE); } @Override public void onStart() { super.onStart(); - userCancelled = false; - - vibrator = (Vibrator) getSystemService(Context.VIBRATOR_SERVICE); - vibrator.vibrate(vPattern, 0); - - // TODO change button to slide... - // TODO change slide to circle slide? https://github.com/JesusM/HoloCircleSeekBar + /* Button cancelButton = (Button) findViewById(R.id.cancel_dialog_button); cancelButton.setOnClickListener (new View.OnClickListener() { @Override @@ -80,39 +64,38 @@ public class AlarmAlertActivity extends Activity { cancelGraceAlarm(); } }); + */ + SeekArc cancelArc = (SeekArc) findViewById(R.id.cancel_dialog_seekArc); + cancelArc.setOnSeekArcChangeListener(new SeekArc.OnSeekArcChangeListener() { + volatile Boolean seekFinished = false; + @Override + public void onProgressChanged(SeekArc seekArc, int progress, boolean fromUser) { + if (progress > 98 && !seekFinished && fromUser) { + AlarmReceiver.dismissAlarm(alertActivity); + seekFinished = true; + } + } + @Override + public void onStartTrackingTouch(SeekArc seekArc) { + } + @Override + public void onStopTrackingTouch(SeekArc seekArc) { + } + }); + } /** - * Handle the user pressing the back/return button - * TODO Do we want this to cancel the alarm and grace period? - * TODO Or do we trigger the notification and finish() right away? - * TODO probably most intuitive to cancel the alarms... + * Handle the user pressing the back/return and home buttons */ @Override public void onBackPressed() { - cancelGraceAlarm(); - /* OR: - // Ensure that the we don't trigger the notification a second time - userCancelled = true; - startService(notifyIntent); - finish(); - */ + AlarmReceiver.setAlarmStatus(AlarmReceiver.ALARM_IGNORED); + AlarmReceiver.snoozeAlarm(this); } - public void onStop() { - vibrator.cancel(); - super.onStop(); - } - - private void cancelGraceAlarm() { - AlarmManager graceManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE); - Intent graceIntent = new Intent(getApplicationContext(), GraceReceiver.class); - PendingIntent gracePendingIntent = PendingIntent.getBroadcast(getApplicationContext(), MainActivity.GRACE_REQUEST, graceIntent, 0); - graceManager.cancel(gracePendingIntent); - Log.d("AlarmAlertActivity", "Cancelled grace alarm."); - // Ensure we don't load a notification now - userCancelled = true; - // Close the dialogue (stop vibration &c) - finish(); + public void onUserLeaveHint() { + AlarmReceiver.setAlarmStatus(AlarmReceiver.ALARM_IGNORED); + AlarmReceiver.snoozeAlarm(this); } } \ No newline at end of file diff --git a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmKlaxon.java b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmKlaxon.java new file mode 100644 index 0000000..af21e54 --- /dev/null +++ b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmKlaxon.java @@ -0,0 +1,130 @@ +package za.org.treehouse.hypoalarm; + +import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.media.AudioManager; +import android.media.MediaPlayer; +import android.media.RingtoneManager; +import android.net.Uri; +import android.os.Vibrator; +import android.telephony.TelephonyManager; +import android.util.Log; + +import java.io.IOException; + +public class AlarmKlaxon { + private static final long[] vPattern = {500, 500}; + // Volume modification for alarms while a phone call is active, from com.android.deskclock.alarms + private static final float IN_CALL_VOLUME = 0.125f; + private static MediaPlayer mediaPlayer = null; + private static TelephonyManager telephonyManager; + private static Vibrator vibrator; + + public static void start(final Context context) { + + /** + * + * TODO add raw ring tone to use as fallback + * TODO add in-call ring tone + * TODO lower volume if in a call + * TODO cancel noise if a call comes in (add TelephonyManager listener which cancels the alert but calls the notification) + * + * TODO start telephony listener + * + * TODO snooze 5 minutes on press back button or home button (new runnable) + * TODO remove back/home button and most top icons + * TODO fix glowpad/seekarc + */ + vibrator = (Vibrator) context.getSystemService(Context.VIBRATOR_SERVICE); + vibrator.cancel(); + vibrator.vibrate(vPattern, 0); + + if (true) + return; // TODO remove after testing -- is mediaplayer responsible for delays? + + telephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + // TODO select alarm tone? + // Use the default alarm tone... + Uri alarmNoise = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM); + stopAudio(context); + mediaPlayer = new MediaPlayer(); + mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + Log.e("AlarmAlertActivity", "Error occurred while playing audio. Stopping alarm."); + stopAudio(context); + return true; + } + }); + + try { + /* + * TODO find out if we're in a call + if (inTelephoneCall) { + Log.v("Using the in-call alarm"); + sMediaPlayer.setVolume(IN_CALL_VOLUME, IN_CALL_VOLUME); + setDataSourceFromResource(context, sMediaPlayer, R.raw.in_call_alarm); + } else { + */ + mediaPlayer.setDataSource(context, alarmNoise); + startAudio(context); + //} + } catch (Exception ex) { + // The alarmNoise may be on the sd card which could be busy right + // now. Use the fallback ringtone. + try { + // Reset the media player to clear the error state. + mediaPlayer.reset(); + //setDataSourceFromResource(this, mediaPlayer, R.raw.fallbackring); + startAudio(context); + } catch (Exception ex2) { + // At this point we just don't play anything. + Log.e("AlarmAlertActivity", "Failed to play fallback ringtone", ex2); + } + } + + } + + public static void stop(final Context context) { + vibrator.cancel(); + if (true) + return; // TODO remove after testing + stopAudio(context); + } + + private static void startAudio(final Context context) throws IOException { + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + // do not play alarms if stream volume is 0 (typically because ringer mode is silent). + if (audioManager.getStreamVolume(AudioManager.STREAM_ALARM) != 0) { + mediaPlayer.setAudioStreamType(AudioManager.STREAM_ALARM); + mediaPlayer.setLooping(true); + try { + mediaPlayer.prepare(); + } catch (Exception e) { + Log.e("AlarmAlertActivity", "Prepare failed. Exiting", e); + return; + } + audioManager.requestAudioFocus(null, + AudioManager.STREAM_ALARM, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + mediaPlayer.start(); + } + } + + private static void stopAudio(final Context context) { + if (mediaPlayer != null) { + mediaPlayer.stop(); + AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); + audioManager.abandonAudioFocus(null); + mediaPlayer.release(); + mediaPlayer = null; + } + } + // Load ringtone from a resource + private static void setDataSourceFromResource(Context context, MediaPlayer player, int res) throws IOException { + AssetFileDescriptor afd = context.getResources().openRawResourceFd(res); + if (afd != null) { + player.setDataSource(afd.getFileDescriptor(), afd.getStartOffset(), afd.getLength()); + afd.close(); + } + } +} diff --git a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmNotify.java b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmNotify.java index 3460bf9..b1232dc 100644 --- a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmNotify.java +++ b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmNotify.java @@ -27,10 +27,10 @@ public class AlarmNotify extends Service { public void onDestroy() { // If the notification is cancelled, stop updating. notificationRunning = false; - Log.d("AlarmNotify", "1: Setting notificationRunning to false"); // Remove the notification in the notification bar NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); nm.cancel(notifyID); + Log.d("AlarmNotify", "Notification stopped."); } @Override @@ -38,10 +38,10 @@ public class AlarmNotify extends Service { final int UPDATE_INTERVAL = 10*1000; // Timer is updated six times a minute SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); + Log.d("AlarmNotify", "Notification started."); + //final String phoneNumber = sharedPref.getString(getString(R.string.PhoneNumberPref), null); final int gracePeriod = sharedPref.getInt(getString(R.string.GracePeriodPref), 60); - // convert gracePeriod to milliseconds and calculate when it'll fire - final long endTime = System.currentTimeMillis() + (gracePeriod * 60 * 1000); Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher_grey); final NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); @@ -55,6 +55,8 @@ public class AlarmNotify extends Service { .setAutoCancel(false) .setPriority(Notification.PRIORITY_HIGH); + +// TODO if alarm alert is snoozing and we cancel, cancel the snooze. // Set up dismiss action Intent cancellerIntent = new Intent(getBaseContext(), CancelGraceReceiver.class); PendingIntent cancellerPendingIntent = PendingIntent.getBroadcast(getBaseContext(), MainActivity.CANCEL_GRACE_REQUEST, cancellerIntent, PendingIntent.FLAG_CANCEL_CURRENT); @@ -69,12 +71,13 @@ public class AlarmNotify extends Service { /** * TODO load alert activity (without sound or vibration) on select? * TODO This would allow the user to test competence - Intent alertActivityIntent = new Intent(this, AlarmAlertActivity.class); - alertActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | - Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - notification.setContentIntent(alertActivityIntent); + * something like: + Intent alarmAlertIntent = new Intent(this, AlarmAlertActivity.class); + PendingIntent alarmPendingIntent = PendingIntent.getBroadcast(this, 0, alarmAlertIntent, 0); + notification.setContentIntent(alarmPen`dingIntent); */ + nm.cancel(notifyID); nm.notify(notifyID, notification.build()); @@ -82,21 +85,22 @@ public class AlarmNotify extends Service { @Override public void run() { notificationRunning = true; - Log.d("AlarmNotify", "2: Setting notificationRunning to true"); int max = 1000; - // Convert endTime from milliseconds to seconds, and translate to time remaining - int secondsLeft = (int) ((endTime - System.currentTimeMillis())) / (1000); - int gracePeriodSeconds = gracePeriod * 60; - // Multiply each int by 1000 for greater progress resolution - int progress = (((gracePeriodSeconds * 1000) - (secondsLeft * 1000)) * max) / (gracePeriodSeconds * 1000); + /* TODO check that graceEndTime is always set. + if (AlarmReceiver.graceEndTime == 0) { + AlarmReceiver.graceEndTime = System.currentTimeMillis() + (gracePeriod * 60 * 1000); + }*/ + // Count in milliseconds for greater progress resolution + int milliSecondsLeft = (int) ((AlarmReceiver.graceEndTime - System.currentTimeMillis())); + int gracePeriodMilliSeconds = gracePeriod * 60 * 1000; + int progress = ((gracePeriodMilliSeconds - milliSecondsLeft) * max) / gracePeriodMilliSeconds; while (progress < max) { - // Stop the thread if cancelled elsewhere - Log.d("AlarmNotify", "notificationRunning is " + notificationRunning); + // Stop the thread if the notification is cancelled elsewhere if (!notificationRunning) { return; } - int minutesLeft = secondsLeft / 60; + int minutesLeft = (milliSecondsLeft / 1000) / 60; if (Build.VERSION.SDK_INT >= 11) { notification.setContentText(String.format(getString(R.string.notificationText), MainActivity.MinutesToGracePeriodStr(minutesLeft))); notification.setProgress(max, progress, false); @@ -104,12 +108,12 @@ public class AlarmNotify extends Service { nm.notify(notifyID, notification.build()); } // Prepare secondsLeft and progress for the next loop - secondsLeft = secondsLeft - (UPDATE_INTERVAL / 1000); + milliSecondsLeft = milliSecondsLeft - UPDATE_INTERVAL; // Multiply each int by 1000 for greater progress resolution - progress = (((gracePeriodSeconds * 1000) - (secondsLeft * 1000)) * max) / (gracePeriodSeconds * 1000); - Log.d("AlarmNotify", "secondsLeft is " + secondsLeft + " and progress is " + progress + " (gracePeriodSeconds is " + gracePeriodSeconds + ")"); - // Sleeps the thread, simulating an operation - // that takes time + progress = ((gracePeriodMilliSeconds - milliSecondsLeft) * max) / gracePeriodMilliSeconds; + //Log.d("AlarmNotify", "milliSecondsLeft is " + milliSecondsLeft + " and progress is " + progress + " (gracePeriodMilliSeconds is " + gracePeriodMilliSeconds + ")"); + + // Sleep until we need to update again try { Thread.sleep(UPDATE_INTERVAL); } catch (InterruptedException e) { diff --git a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmReceiver.java b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmReceiver.java index 50af2eb..232cd56 100644 --- a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmReceiver.java +++ b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/AlarmReceiver.java @@ -1,34 +1,55 @@ package za.org.treehouse.hypoalarm; import android.app.AlarmManager; -import android.app.NotificationManager; import android.app.PendingIntent; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.os.Build; +import android.os.Handler; import android.preference.PreferenceManager; import android.util.Log; -import android.view.WindowManager; +import android.widget.Toast; import java.util.Calendar; +/** + * TODO change alarm state if a phone call comes in + * + * TODO display notification if alarm is about to go off (and allow user to cancel it before alarm goes off) + * + * TODO allow snooze state + */ + public class AlarmReceiver extends BroadcastReceiver { + private static final int SNOOZE_TIME = 1000*20; //1000*60*5; // Snooze for 5 minutes if need be + private static final int ALERT_LIFE = 1000*10; //1000*60*2; // 2 minutes private static SharedPreferences sharedPref; private static AlarmManager alarmManager, graceManager; private static PendingIntent alarmPendingIntent, gracePendingIntent; + private static Intent alertActivityIntent, notifyIntent; + public static volatile String alarmStatus; // Register ALARM_DISMISSED and its brethren here + public static final String ALARM_RUNNING = "ALARM_RUNNING"; + public static final String ALARM_DISMISSED = "ALARM_DISMISSED"; + public static final String ALARM_IGNORED = "ALARM_IGNORED"; + public static final String ALARM_SNOOZED = "ALARM_SNOOZED"; + public static final String ALARM_SNOOZE_RUNNING = "ALARM_SNOOZE_RUNNING"; + public static long graceEndTime; @Override - public void onReceive(Context context, Intent intent) { + public void onReceive(final Context context, Intent intent) { sharedPref = PreferenceManager.getDefaultSharedPreferences(context); Boolean alarmActive = sharedPref.getBoolean(context.getString(R.string.AlarmActivePref), true); + int gracePeriod = sharedPref.getInt(context.getString(R.string.GracePeriodPref), 60); + String alarmTimeStr = sharedPref.getString(context.getString(R.string.AlarmTimePref), null); if (alarmActive) { - // Set a grace period alarm to send SMS - int gracePeriod = sharedPref.getInt(context.getString(R.string.GracePeriodPref), 60); + // if nothing else happens, assume the alert was ignored. + alarmStatus = ALARM_RUNNING; + // Set a grace period alarm to send SMS Calendar graceCal = Calendar.getInstance(); graceCal.set(Calendar.SECOND, 0); graceCal.add(Calendar.MINUTE, gracePeriod); @@ -43,16 +64,20 @@ public class AlarmReceiver extends BroadcastReceiver { } Log.d("AlarmReceiver", "Setting grace alarm for " + MainActivity.debugDate(graceCal)); - // Allow user to acknowledge alarm and cancel grace alarm - Intent alertActivityIntent = new Intent(context, AlarmAlertActivity.class); + // Calculate when the grace period ends + graceEndTime = System.currentTimeMillis() + (gracePeriod * 60 * 1000); + + // Set up intents for later use + notifyIntent = new Intent(context, AlarmNotify.class); + alertActivityIntent = new Intent(context, AlarmAlertActivity.class); alertActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); - context.startActivity(alertActivityIntent); + // Allow user to acknowledge alarm and cancel grace alarm + startAlert(context); // Reset for tomorrow; as of API 19, setRepeating() is inexact, so we use setExact() - String alarmTimeStr = sharedPref.getString(context.getString(R.string.AlarmTimePref), null); - // Calendar automatically advances the day since alarmTimeStr is now in the past. + // (Calendar will automatically advance the day since today's alarmTimeStr is now in the past.) Calendar cal = MainActivity.TimeStringToCalendar(alarmTimeStr); alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); alarmPendingIntent = PendingIntent.getBroadcast(context, MainActivity.ALARM_REQUEST, intent, 0); @@ -65,4 +90,88 @@ public class AlarmReceiver extends BroadcastReceiver { Log.d("AlarmReceiver", "Resetting alarm for " + MainActivity.debugDate(cal)); } } + + public static void startAlert(final Context context) { + Log.d("AlarmReceiver", "Starting alarm; status is " + alarmStatus); + // Turn off any notifications first + context.stopService(notifyIntent); + + context.startActivity(alertActivityIntent); + AlarmKlaxon.start(context); + + // Turn off the alert activity after a period, and switch to a notification + new Handler().postDelayed(new Runnable() { + public void run() { + // Close the dialogue and switch to notification + // if the Activity has not been closed by the user + // (that is, snoozeAlert and dismissAlert have not been called) + // TODO don't run if we've just snoozed from home/back button, but do run if + // TODO we want to finish the snooze alert activity... + if (alarmStatus.contentEquals(ALARM_DISMISSED) || + alarmStatus.contentEquals(ALARM_SNOOZED)) { + return; + } + // Stop if we're running the snooze alert + if (alarmStatus.contentEquals(ALARM_SNOOZE_RUNNING)) { + stopAlert(context); + } else { + alarmStatus = ALARM_IGNORED; // This is true, although we are about to switch to ALARM_SNOOZED + snoozeAlarm(context); + } + } + }, ALERT_LIFE); + } + public static void stopAlert(final Context context) { + Log.d("AlarmReceiver", "Stopping alarm; status is " + alarmStatus); + AlarmKlaxon.stop(context); + AlarmAlertActivity.alertActivity.finish(); + // Display a notification if the alarm hasn't been dismissed + if (!alarmStatus.contentEquals(ALARM_DISMISSED)) { + context.startService(notifyIntent); + } + } + + public static void dismissAlarm(final Context context) { + Log.d("AlarmReceiver", "Dismissing alarm"); + alarmStatus = ALARM_DISMISSED; + // Close the alert and all notifications + stopAlert(context); + + // Cancel the graceAlarm + AlarmManager graceManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Intent graceIntent = new Intent(context, GraceReceiver.class); + PendingIntent gracePendingIntent = PendingIntent.getBroadcast(context, MainActivity.GRACE_REQUEST, graceIntent, 0); + graceManager.cancel(gracePendingIntent); + } + + // TODO should the snooze reset the time at which the grace period alarm fires? + public static void snoozeAlarm(final Context context) { + Log.d("AlarmReceiver", "Snoozing alarm"); + stopAlert(context); + // Close the alert, stop the klaxon, and start the notification, + // but only if there's time left before the gracePeriod triggers, + // and we haven't snoozed before + if (((System.currentTimeMillis() + SNOOZE_TIME) < graceEndTime) && + (!alarmStatus.contentEquals(ALARM_SNOOZED)) && + (!alarmStatus.contentEquals(ALARM_DISMISSED))) { + new Handler().postDelayed(new Runnable() { + public void run() { + Log.d("AlarmReceiver", "Resuming after snooze; status is " + alarmStatus); + // Don't run if the alarm was dismissed before the timer ran out + // (because a notification was acknowledged) + if (!alarmStatus.contentEquals(ALARM_DISMISSED)) { + alarmStatus = ALARM_SNOOZE_RUNNING; + startAlert(context); + } + } + }, SNOOZE_TIME); + // Change alarm status from ignored to snoozed + alarmStatus = ALARM_SNOOZED; + } + } + + public static void setAlarmStatus (String status) { + Log.d("AlarmReceiver", "Setting alarm status to " + status); + alarmStatus = status; + } } diff --git a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/CancelGraceReceiver.java b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/CancelGraceReceiver.java index 5f23424..d2f5d60 100644 --- a/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/CancelGraceReceiver.java +++ b/HypoAlarm/src/main/java/za/org/treehouse/hypoalarm/CancelGraceReceiver.java @@ -18,6 +18,9 @@ public class CancelGraceReceiver extends BroadcastReceiver { graceManager.cancel(gracePendingIntent); Log.d("CancelGraceReceiver", "Cancelled grace alarm"); + // Ensure that any snoozes that are pending never happen. + AlarmReceiver.setAlarmStatus(AlarmReceiver.ALARM_DISMISSED); + // Display toast Toast.makeText(context, context.getString(R.string.alarmCancelToast), Toast.LENGTH_LONG).show(); diff --git a/HypoAlarm/src/main/res/drawable-hdpi/ic_launcher.png b/HypoAlarm/src/main/res/drawable-hdpi/ic_launcher.png index b21af85..1d371ad 100644 Binary files a/HypoAlarm/src/main/res/drawable-hdpi/ic_launcher.png and b/HypoAlarm/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/HypoAlarm/src/main/res/drawable-mdpi/ic_launcher.png b/HypoAlarm/src/main/res/drawable-mdpi/ic_launcher.png index 015adbe..178ed19 100644 Binary files a/HypoAlarm/src/main/res/drawable-mdpi/ic_launcher.png and b/HypoAlarm/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/HypoAlarm/src/main/res/drawable-xhdpi/ic_launcher.png b/HypoAlarm/src/main/res/drawable-xhdpi/ic_launcher.png index 0982477..fb1f16f 100644 Binary files a/HypoAlarm/src/main/res/drawable-xhdpi/ic_launcher.png and b/HypoAlarm/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher.png b/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher.png index e887989..592272f 100644 Binary files a/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher.png and b/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher_grey.png b/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher_grey.png new file mode 100644 index 0000000..12a2c1b Binary files /dev/null and b/HypoAlarm/src/main/res/drawable-xxhdpi/ic_launcher_grey.png differ diff --git a/HypoAlarm/src/main/res/drawable/hypoalarm.png b/HypoAlarm/src/main/res/drawable/hypoalarm.png index e1f444c..e091112 100644 Binary files a/HypoAlarm/src/main/res/drawable/hypoalarm.png and b/HypoAlarm/src/main/res/drawable/hypoalarm.png differ diff --git a/HypoAlarm/src/main/res/layout/alarm_alert.xml b/HypoAlarm/src/main/res/layout/alarm_alert.xml index a2e1222..124eb91 100644 --- a/HypoAlarm/src/main/res/layout/alarm_alert.xml +++ b/HypoAlarm/src/main/res/layout/alarm_alert.xml @@ -2,6 +2,8 @@ @@ -9,6 +11,34 @@ + + + + + + \ No newline at end of file diff --git a/HypoAlarm/src/main/res/values/dimens.xml b/HypoAlarm/src/main/res/values/dimens.xml index 48963b2..35c33e2 100644 --- a/HypoAlarm/src/main/res/values/dimens.xml +++ b/HypoAlarm/src/main/res/values/dimens.xml @@ -3,4 +3,30 @@ 16dp 16dp + + + + 135dip + + + 75dip + + + 40dip + + + 15dip + + + 270dp + + + 94dp + + + 28dp + \ No newline at end of file diff --git a/HypoAlarm/src/main/res/values/strings.xml b/HypoAlarm/src/main/res/values/strings.xml index c76093b..ca70a12 100644 --- a/HypoAlarm/src/main/res/values/strings.xml +++ b/HypoAlarm/src/main/res/values/strings.xml @@ -57,4 +57,11 @@ All HypoAlarms cancelled + + @drawable/ic_lockscreen_answer + @null + @drawable/ic_lockscreen_decline + @null" + + diff --git a/HypoAlarm/src/main/res/values/styles.xml b/HypoAlarm/src/main/res/values/styles.xml index 00a7ff8..1162a3e 100644 --- a/HypoAlarm/src/main/res/values/styles.xml +++ b/HypoAlarm/src/main/res/values/styles.xml @@ -5,4 +5,13 @@ + + + + + diff --git a/SeekArc/SeekArc.iml b/SeekArc/SeekArc.iml new file mode 100644 index 0000000..2ee141d --- /dev/null +++ b/SeekArc/SeekArc.iml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SeekArc/build.gradle b/SeekArc/build.gradle new file mode 100644 index 0000000..f7e10f4 --- /dev/null +++ b/SeekArc/build.gradle @@ -0,0 +1,19 @@ +apply plugin: 'android-library' + +android { + compileSdkVersion 19 + buildToolsVersion "19.0.1" + + defaultConfig { + minSdkVersion 8 + targetSdkVersion 17 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + runProguard false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + } + } +} \ No newline at end of file diff --git a/SeekArc/build/source/buildConfig/debug/com/triggertrap/seekarc/BuildConfig.java b/SeekArc/build/source/buildConfig/debug/com/triggertrap/seekarc/BuildConfig.java new file mode 100644 index 0000000..ddbe975 --- /dev/null +++ b/SeekArc/build/source/buildConfig/debug/com/triggertrap/seekarc/BuildConfig.java @@ -0,0 +1,13 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.triggertrap.seekarc; + +public final class BuildConfig { + public static final boolean DEBUG = Boolean.parseBoolean("true"); + public static final String PACKAGE_NAME = "com.triggertrap.seekarc"; + public static final String BUILD_TYPE = "debug"; + public static final String FLAVOR = ""; + public static final int VERSION_CODE = 1; + public static final String VERSION_NAME = "1.0"; +} diff --git a/SeekArc/build/source/buildConfig/release/com/triggertrap/seekarc/BuildConfig.java b/SeekArc/build/source/buildConfig/release/com/triggertrap/seekarc/BuildConfig.java new file mode 100644 index 0000000..ba2f829 --- /dev/null +++ b/SeekArc/build/source/buildConfig/release/com/triggertrap/seekarc/BuildConfig.java @@ -0,0 +1,13 @@ +/** + * Automatically generated file. DO NOT MODIFY + */ +package com.triggertrap.seekarc; + +public final class BuildConfig { + public static final boolean DEBUG = false; + public static final String PACKAGE_NAME = "com.triggertrap.seekarc"; + public static final String BUILD_TYPE = "release"; + public static final String FLAVOR = ""; + public static final int VERSION_CODE = 1; + public static final String VERSION_NAME = "1.0"; +} diff --git a/SeekArc/build/source/r/debug/com/triggertrap/seekarc/R.java b/SeekArc/build/source/r/debug/com/triggertrap/seekarc/R.java new file mode 100644 index 0000000..ff2b5eb --- /dev/null +++ b/SeekArc/build/source/r/debug/com/triggertrap/seekarc/R.java @@ -0,0 +1,419 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package com.triggertrap.seekarc; + +public final class R { + public static final class attr { + /**

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int arcColor=0x7f010009; + /**

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int arcWidth=0x7f010004; + /**

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int clockwise=0x7f01000d; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int max=0x7f010002; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int progress=0x7f010005; + /**

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int progressColor=0x7f01000a; + /**

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int progressWidth=0x7f010003; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int rotation=0x7f010006; + /**

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int roundEdges=0x7f01000b; + /**

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + */ + public static int seekArcStyle=0x7f01000e; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int startAngle=0x7f010007; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int sweepAngle=0x7f010008; + /**

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + */ + public static int thumb=0x7f010000; + /**

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int thumbOffset=0x7f010001; + /**

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int touchInside=0x7f01000c; + } + public static final class color { + public static int progress_gray=0x7f030000; + public static int progress_gray_dark=0x7f030001; + } + public static final class drawable { + public static int ic_launcher=0x7f020000; + public static int scrubber_control_disabled_holo=0x7f020001; + public static int scrubber_control_focused_holo=0x7f020002; + public static int scrubber_control_normal_holo=0x7f020003; + public static int scrubber_control_pressed_holo=0x7f020004; + public static int seek_arc_control_selector=0x7f020005; + } + public static final class string { + public static int app_name=0x7f040000; + } + public static final class style { + public static int SeekArc=0x7f050000; + public static int SeekArcLight=0x7f050001; + } + public static final class styleable { + /** Attributes that can be used with a SeekArc. +

Includes the following attributes:

+ + + + + + + + + + + + + + + + + + +
AttributeDescription
{@link #SeekArc_arcColor com.triggertrap.seekarc:arcColor}
{@link #SeekArc_arcWidth com.triggertrap.seekarc:arcWidth}
{@link #SeekArc_clockwise com.triggertrap.seekarc:clockwise}
{@link #SeekArc_max com.triggertrap.seekarc:max}
{@link #SeekArc_progress com.triggertrap.seekarc:progress}
{@link #SeekArc_progressColor com.triggertrap.seekarc:progressColor}
{@link #SeekArc_progressWidth com.triggertrap.seekarc:progressWidth}
{@link #SeekArc_rotation com.triggertrap.seekarc:rotation}
{@link #SeekArc_roundEdges com.triggertrap.seekarc:roundEdges}
{@link #SeekArc_startAngle com.triggertrap.seekarc:startAngle}
{@link #SeekArc_sweepAngle com.triggertrap.seekarc:sweepAngle}
{@link #SeekArc_thumb com.triggertrap.seekarc:thumb}
{@link #SeekArc_thumbOffset com.triggertrap.seekarc:thumbOffset}
{@link #SeekArc_touchInside com.triggertrap.seekarc:touchInside}
+ @see #SeekArc_arcColor + @see #SeekArc_arcWidth + @see #SeekArc_clockwise + @see #SeekArc_max + @see #SeekArc_progress + @see #SeekArc_progressColor + @see #SeekArc_progressWidth + @see #SeekArc_rotation + @see #SeekArc_roundEdges + @see #SeekArc_startAngle + @see #SeekArc_sweepAngle + @see #SeekArc_thumb + @see #SeekArc_thumbOffset + @see #SeekArc_touchInside + */ + public static final int[] SeekArc = { + 0x7f010000, 0x7f010001, 0x7f010002, 0x7f010003, + 0x7f010004, 0x7f010005, 0x7f010006, 0x7f010007, + 0x7f010008, 0x7f010009, 0x7f01000a, 0x7f01000b, + 0x7f01000c, 0x7f01000d + }; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#arcColor} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:arcColor + */ + public static final int SeekArc_arcColor = 9; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#arcWidth} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:arcWidth + */ + public static final int SeekArc_arcWidth = 4; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#clockwise} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:clockwise + */ + public static final int SeekArc_clockwise = 13; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#max} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:max + */ + public static final int SeekArc_max = 2; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#progress} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:progress + */ + public static final int SeekArc_progress = 5; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#progressColor} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:progressColor + */ + public static final int SeekArc_progressColor = 10; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#progressWidth} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:progressWidth + */ + public static final int SeekArc_progressWidth = 3; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#rotation} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:rotation + */ + public static final int SeekArc_rotation = 6; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#roundEdges} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:roundEdges + */ + public static final int SeekArc_roundEdges = 11; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#startAngle} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:startAngle + */ + public static final int SeekArc_startAngle = 7; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#sweepAngle} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:sweepAngle + */ + public static final int SeekArc_sweepAngle = 8; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#thumb} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + @attr name com.triggertrap.seekarc:thumb + */ + public static final int SeekArc_thumb = 0; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#thumbOffset} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:thumbOffset + */ + public static final int SeekArc_thumbOffset = 1; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#touchInside} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:touchInside + */ + public static final int SeekArc_touchInside = 12; + /** Attributes that can be used with a SeekArcTheme. +

Includes the following attributes:

+ + + + + +
AttributeDescription
{@link #SeekArcTheme_seekArcStyle com.triggertrap.seekarc:seekArcStyle}
+ @see #SeekArcTheme_seekArcStyle + */ + public static final int[] SeekArcTheme = { + 0x7f01000e + }; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#seekArcStyle} + attribute's value can be found in the {@link #SeekArcTheme} array. + + +

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + @attr name com.triggertrap.seekarc:seekArcStyle + */ + public static final int SeekArcTheme_seekArcStyle = 0; + }; +} diff --git a/SeekArc/build/source/r/release/com/triggertrap/seekarc/R.java b/SeekArc/build/source/r/release/com/triggertrap/seekarc/R.java new file mode 100644 index 0000000..b7b12f4 --- /dev/null +++ b/SeekArc/build/source/r/release/com/triggertrap/seekarc/R.java @@ -0,0 +1,418 @@ +/* AUTO-GENERATED FILE. DO NOT MODIFY. + * + * This class was automatically generated by the + * aapt tool from the resource data it found. It + * should not be modified by hand. + */ + +package com.triggertrap.seekarc; + +public final class R { + public static final class attr { + /**

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int arcColor=0x7f010009; + /**

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int arcWidth=0x7f010004; + /**

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int clockwise=0x7f01000d; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int max=0x7f010002; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int progress=0x7f010005; + /**

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int progressColor=0x7f01000a; + /**

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int progressWidth=0x7f010003; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int rotation=0x7f010006; + /**

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int roundEdges=0x7f01000b; + /**

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + */ + public static int seekArcStyle=0x7f01000e; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int startAngle=0x7f010007; + /**

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int sweepAngle=0x7f010008; + /**

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + */ + public static int thumb=0x7f010000; + /**

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int thumbOffset=0x7f010001; + /**

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + */ + public static int touchInside=0x7f01000c; + } + public static final class color { + public static int progress_gray=0x7f030000; + public static int progress_gray_dark=0x7f030001; + } + public static final class drawable { + public static int scrubber_control_disabled_holo=0x7f020000; + public static int scrubber_control_focused_holo=0x7f020001; + public static int scrubber_control_normal_holo=0x7f020002; + public static int scrubber_control_pressed_holo=0x7f020003; + public static int seek_arc_control_selector=0x7f020004; + } + public static final class string { + public static int app_name=0x7f040000; + } + public static final class style { + public static int SeekArc=0x7f050000; + public static int SeekArcLight=0x7f050001; + } + public static final class styleable { + /** Attributes that can be used with a SeekArc. +

Includes the following attributes:

+ + + + + + + + + + + + + + + + + + +
AttributeDescription
{@link #SeekArc_arcColor com.triggertrap.seekarc:arcColor}
{@link #SeekArc_arcWidth com.triggertrap.seekarc:arcWidth}
{@link #SeekArc_clockwise com.triggertrap.seekarc:clockwise}
{@link #SeekArc_max com.triggertrap.seekarc:max}
{@link #SeekArc_progress com.triggertrap.seekarc:progress}
{@link #SeekArc_progressColor com.triggertrap.seekarc:progressColor}
{@link #SeekArc_progressWidth com.triggertrap.seekarc:progressWidth}
{@link #SeekArc_rotation com.triggertrap.seekarc:rotation}
{@link #SeekArc_roundEdges com.triggertrap.seekarc:roundEdges}
{@link #SeekArc_startAngle com.triggertrap.seekarc:startAngle}
{@link #SeekArc_sweepAngle com.triggertrap.seekarc:sweepAngle}
{@link #SeekArc_thumb com.triggertrap.seekarc:thumb}
{@link #SeekArc_thumbOffset com.triggertrap.seekarc:thumbOffset}
{@link #SeekArc_touchInside com.triggertrap.seekarc:touchInside}
+ @see #SeekArc_arcColor + @see #SeekArc_arcWidth + @see #SeekArc_clockwise + @see #SeekArc_max + @see #SeekArc_progress + @see #SeekArc_progressColor + @see #SeekArc_progressWidth + @see #SeekArc_rotation + @see #SeekArc_roundEdges + @see #SeekArc_startAngle + @see #SeekArc_sweepAngle + @see #SeekArc_thumb + @see #SeekArc_thumbOffset + @see #SeekArc_touchInside + */ + public static final int[] SeekArc = { + 0x7f010000, 0x7f010001, 0x7f010002, 0x7f010003, + 0x7f010004, 0x7f010005, 0x7f010006, 0x7f010007, + 0x7f010008, 0x7f010009, 0x7f01000a, 0x7f01000b, + 0x7f01000c, 0x7f01000d + }; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#arcColor} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:arcColor + */ + public static final int SeekArc_arcColor = 9; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#arcWidth} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:arcWidth + */ + public static final int SeekArc_arcWidth = 4; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#clockwise} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:clockwise + */ + public static final int SeekArc_clockwise = 13; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#max} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:max + */ + public static final int SeekArc_max = 2; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#progress} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:progress + */ + public static final int SeekArc_progress = 5; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#progressColor} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a color value, in the form of "#rgb", "#argb", +"#rrggbb", or "#aarrggbb". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:progressColor + */ + public static final int SeekArc_progressColor = 10; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#progressWidth} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:progressWidth + */ + public static final int SeekArc_progressWidth = 3; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#rotation} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:rotation + */ + public static final int SeekArc_rotation = 6; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#roundEdges} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:roundEdges + */ + public static final int SeekArc_roundEdges = 11; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#startAngle} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:startAngle + */ + public static final int SeekArc_startAngle = 7; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#sweepAngle} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be an integer value, such as "100". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:sweepAngle + */ + public static final int SeekArc_sweepAngle = 8; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#thumb} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + @attr name com.triggertrap.seekarc:thumb + */ + public static final int SeekArc_thumb = 0; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#thumbOffset} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a dimension value, which is a floating point number appended with a unit such as "14.5sp". +Available units are: px (pixels), dp (density-independent pixels), sp (scaled pixels based on preferred font size), +in (inches), mm (millimeters). +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:thumbOffset + */ + public static final int SeekArc_thumbOffset = 1; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#touchInside} + attribute's value can be found in the {@link #SeekArc} array. + + +

Must be a boolean value, either "true" or "false". +

This may also be a reference to a resource (in the form +"@[package:]type:name") or +theme attribute (in the form +"?[package:][type:]name") +containing a value of this type. + @attr name com.triggertrap.seekarc:touchInside + */ + public static final int SeekArc_touchInside = 12; + /** Attributes that can be used with a SeekArcTheme. +

Includes the following attributes:

+ + + + + +
AttributeDescription
{@link #SeekArcTheme_seekArcStyle com.triggertrap.seekarc:seekArcStyle}
+ @see #SeekArcTheme_seekArcStyle + */ + public static final int[] SeekArcTheme = { + 0x7f01000e + }; + /** +

This symbol is the offset where the {@link com.triggertrap.seekarc.R.attr#seekArcStyle} + attribute's value can be found in the {@link #SeekArcTheme} array. + + +

Must be a reference to another resource, in the form "@[+][package:]type:name" +or to a theme attribute in the form "?[package:][type:]name". + @attr name com.triggertrap.seekarc:seekArcStyle + */ + public static final int SeekArcTheme_seekArcStyle = 0; + }; +} diff --git a/SeekArc/libs/android-support-v4.jar b/SeekArc/libs/android-support-v4.jar new file mode 100644 index 0000000..cf12d28 Binary files /dev/null and b/SeekArc/libs/android-support-v4.jar differ diff --git a/SeekArc/licence.txt b/SeekArc/licence.txt new file mode 100644 index 0000000..d52685c --- /dev/null +++ b/SeekArc/licence.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2013 Triggertrap Ltd +Author Neil Davies + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/SeekArc/proguard-project.txt b/SeekArc/proguard-project.txt new file mode 100644 index 0000000..f2fe155 --- /dev/null +++ b/SeekArc/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/SeekArc/project.properties b/SeekArc/project.properties new file mode 100644 index 0000000..484dab0 --- /dev/null +++ b/SeekArc/project.properties @@ -0,0 +1,15 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system edit +# "ant.properties", and override values to adapt the script to your +# project structure. +# +# To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): +#proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt + +# Project target. +target=android-17 +android.library=true diff --git a/SeekArc/src/main/AndroidManifest.xml b/SeekArc/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c2ddadd --- /dev/null +++ b/SeekArc/src/main/AndroidManifest.xml @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/SeekArc/src/main/java/com/triggertrap/seekarc/SeekArc.java b/SeekArc/src/main/java/com/triggertrap/seekarc/SeekArc.java new file mode 100644 index 0000000..3317523 --- /dev/null +++ b/SeekArc/src/main/java/com/triggertrap/seekarc/SeekArc.java @@ -0,0 +1,528 @@ +/******************************************************************************* + * The MIT License (MIT) + * + * Copyright (c) 2013 Triggertrap Ltd + * Author Neil Davies + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + ******************************************************************************/ +package com.triggertrap.seekarc; + +import android.content.Context; +import android.content.res.Resources; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; + +/** + * + * SeekArc.java + * + * This is a class that functions much like a SeekBar but + * follows a circle path instead of a straight line. + * + * @author Neil Davies + * + */ +public class SeekArc extends View { + + private static final String TAG = SeekArc.class.getSimpleName(); + private static int INVALID_PROGRESS_VALUE = -1; + // The initial rotational offset -90 means we start at 12 o'clock + private final int mAngleOffset = -90; + + /** + * The Drawable for the seek arc thumbnail + */ + private Drawable mThumb; + + /** + * The Maximum value that this SeekArc can be set to + */ + private int mMax = 100; + + /** + * The Current value that the SeekArc is set to + */ + private int mProgress = 0; + + /** + * The width of the progress line for this SeekArc + */ + private int mProgressWidth = 4; + + /** + * The Width of the background arc for the SeekArc + */ + private int mArcWidth = 2; + + /** + * The Angle to start drawing this Arc from + */ + private int mStartAngle = 0; + + /** + * The Angle through which to draw the arc (Max is 360) + */ + private int mSweepAngle = 360; + + /** + * The rotation of the SeekArc- 0 is twelve o'clock + */ + private int mRotation = 0; + + /** + * Give the SeekArc rounded edges + */ + private boolean mRoundedEdges = false; + + /** + * Enable touch inside the SeekArc + */ + private boolean mTouchInside = true; + + /** + * Will the progress increase clockwise or anti-clockwise + */ + private boolean mClockwise = true; + + // Internal variables + private int mArcRadius = 0; + private float mProgressSweep = 0; + private RectF mArcRect = new RectF(); + private Paint mArcPaint; + private Paint mProgressPaint; + private int mTranslateX; + private int mTranslateY; + private int mThumbXPos; + private int mThumbYPos; + private double mTouchAngle; + private float mTouchIgnoreRadius; + private OnSeekArcChangeListener mOnSeekArcChangeListener; + + public interface OnSeekArcChangeListener { + + /** + * Notification that the progress level has changed. Clients can use the + * fromUser parameter to distinguish user-initiated changes from those + * that occurred programmatically. + * + * @param seekArc + * The SeekArc whose progress has changed + * @param progress + * The current progress level. This will be in the range + * 0..max where max was set by + * {@link ProgressArc#setMax(int)}. (The default value for + * max is 100.) + * @param fromUser + * True if the progress change was initiated by the user. + */ + void onProgressChanged(SeekArc seekArc, int progress, boolean fromUser); + + /** + * Notification that the user has started a touch gesture. Clients may + * want to use this to disable advancing the seekbar. + * + * @param seekArc + * The SeekArc in which the touch gesture began + */ + void onStartTrackingTouch(SeekArc seekArc); + + /** + * Notification that the user has finished a touch gesture. Clients may + * want to use this to re-enable advancing the seekarc. + * + * @param seekArc + * The SeekArc in which the touch gesture began + */ + void onStopTrackingTouch(SeekArc seekArc); + } + + public SeekArc(Context context) { + super(context); + init(context, null, 0); + } + + public SeekArc(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs, R.attr.seekArcStyle); + } + + public SeekArc(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs, defStyle); + } + + private void init(Context context, AttributeSet attrs, int defStyle) { + + Log.d(TAG, "Initialising SeekArc"); + final Resources res = getResources(); + float density = context.getResources().getDisplayMetrics().density; + + // Defaults, may need to link this into theme settings + int arcColor = res.getColor(R.color.progress_gray); + int progressColor = res.getColor(android.R.color.holo_blue_light); + int thumbHalfheight = 0; + int thumbHalfWidth = 0; + mThumb = res.getDrawable(R.drawable.seek_arc_control_selector); + // Convert progress width to pixels for current density + mProgressWidth = (int) (mProgressWidth * density); + + + if (attrs != null) { + // Attribute initialization + final TypedArray a = context.obtainStyledAttributes(attrs, + R.styleable.SeekArc, defStyle, 0); + + Drawable thumb = a.getDrawable(R.styleable.SeekArc_thumb); + if (thumb != null) { + mThumb = thumb; + } + + + + thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; + thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; + mThumb.setBounds(-thumbHalfWidth, -thumbHalfheight, thumbHalfWidth, + thumbHalfheight); + + mMax = a.getInteger(R.styleable.SeekArc_max, mMax); + mProgress = a.getInteger(R.styleable.SeekArc_progress, mProgress); + mProgressWidth = (int) a.getDimension( + R.styleable.SeekArc_progressWidth, mProgressWidth); + mArcWidth = (int) a.getDimension(R.styleable.SeekArc_arcWidth, + mArcWidth); + mStartAngle = a.getInt(R.styleable.SeekArc_startAngle, mStartAngle); + mSweepAngle = a.getInt(R.styleable.SeekArc_sweepAngle, mSweepAngle); + mRotation = a.getInt(R.styleable.SeekArc_rotation, mRotation); + mRoundedEdges = a.getBoolean(R.styleable.SeekArc_roundEdges, + mRoundedEdges); + mTouchInside = a.getBoolean(R.styleable.SeekArc_touchInside, + mTouchInside); + mClockwise = a.getBoolean(R.styleable.SeekArc_clockwise, + mClockwise); + + arcColor = a.getColor(R.styleable.SeekArc_arcColor, arcColor); + progressColor = a.getColor(R.styleable.SeekArc_progressColor, + progressColor); + + a.recycle(); + } + + mProgress = (mProgress > mMax) ? mMax : mProgress; + mProgress = (mProgress < 0) ? 0 : mProgress; + + mSweepAngle = (mSweepAngle > 360) ? 360 : mSweepAngle; + mSweepAngle = (mSweepAngle < 0) ? 0 : mSweepAngle; + + mStartAngle = (mStartAngle > 360) ? 0 : mStartAngle; + mStartAngle = (mStartAngle < 0) ? 0 : mStartAngle; + + mArcPaint = new Paint(); + mArcPaint.setColor(arcColor); + mArcPaint.setAntiAlias(true); + mArcPaint.setStyle(Paint.Style.STROKE); + mArcPaint.setStrokeWidth(mArcWidth); + //mArcPaint.setAlpha(45); + + mProgressPaint = new Paint(); + mProgressPaint.setColor(progressColor); + mProgressPaint.setAntiAlias(true); + mProgressPaint.setStyle(Paint.Style.STROKE); + mProgressPaint.setStrokeWidth(mProgressWidth); + + if (mRoundedEdges) { + mArcPaint.setStrokeCap(Paint.Cap.ROUND); + mProgressPaint.setStrokeCap(Paint.Cap.ROUND); + } + } + + @Override + protected void onDraw(Canvas canvas) { + if(!mClockwise) { + canvas.scale(-1, 1, mArcRect.centerX(), mArcRect.centerY() ); + } + + // Draw the arcs + final int arcStart = mStartAngle + mAngleOffset + mRotation; + final int arcSweep = mSweepAngle; + canvas.drawArc(mArcRect, arcStart, arcSweep, false, mArcPaint); + canvas.drawArc(mArcRect, arcStart, mProgressSweep, false, + mProgressPaint); + + // Draw the thumb nail + canvas.translate(mTranslateX -mThumbXPos, mTranslateY -mThumbYPos); + mThumb.draw(canvas); + } + + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + + final int height = getDefaultSize(getSuggestedMinimumHeight(), + heightMeasureSpec); + final int width = getDefaultSize(getSuggestedMinimumWidth(), + widthMeasureSpec); + final int min = Math.min(width, height); + float top = 0; + float left = 0; + int arcDiameter = 0; + + mTranslateX = (int) (width * 0.5f); + mTranslateY = (int) (height * 0.5f); + + arcDiameter = min - getPaddingLeft(); + mArcRadius = arcDiameter / 2; + top = height / 2 - (arcDiameter / 2); + left = width / 2 - (arcDiameter / 2); + mArcRect.set(left, top, left + arcDiameter, top + arcDiameter); + + int arcStart = (int)mProgressSweep + mStartAngle + mRotation + 90; + mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(arcStart))); + mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(arcStart))); + + setTouchInSide(mTouchInside); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + onStartTrackingTouch(); + updateOnTouch(event); + break; + case MotionEvent.ACTION_MOVE: + updateOnTouch(event); + break; + case MotionEvent.ACTION_UP: + onStopTrackingTouch(); + setPressed(false); + break; + case MotionEvent.ACTION_CANCEL: + onStopTrackingTouch(); + setPressed(false); + + break; + } + + return true; + } + + @Override + protected void drawableStateChanged() { + super.drawableStateChanged(); + if (mThumb != null && mThumb.isStateful()) { + int[] state = getDrawableState(); + mThumb.setState(state); + } + invalidate(); + } + + private void onStartTrackingTouch() { + if (mOnSeekArcChangeListener != null) { + mOnSeekArcChangeListener.onStartTrackingTouch(this); + } + } + + private void onStopTrackingTouch() { + if (mOnSeekArcChangeListener != null) { + mOnSeekArcChangeListener.onStopTrackingTouch(this); + } + } + + private void updateOnTouch(MotionEvent event) { + boolean ignoreTouch = ignoreTouch(event.getX(), event.getY()); + if (ignoreTouch) { + return; + } + setPressed(true); + mTouchAngle = getTouchDegrees(event.getX(), event.getY()); + int progress = getProgressForAngle(mTouchAngle); + onProgressRefresh(progress, true); + } + + private boolean ignoreTouch(float xPos, float yPos) { + boolean ignore = false; + float x = xPos - mTranslateX; + float y = yPos - mTranslateY; + + float touchRadius = (float) Math.sqrt(((x * x) + (y * y))); + if (touchRadius < mTouchIgnoreRadius) { + ignore = true; + } + return ignore; + } + + private double getTouchDegrees(float xPos, float yPos) { + float x = xPos - mTranslateX; + float y = yPos - mTranslateY; + //invert the x-coord if we are rotating anti-clockwise + x= (mClockwise) ? x:-x; + // convert to arc Angle + double angle = Math.toDegrees(Math.atan2(y, x) + (Math.PI / 2) + - Math.toRadians(mRotation)); + if (angle < 0) { + angle = 360 + angle; + } + angle -= mStartAngle; + return angle; + } + + private int getProgressForAngle(double angle) { + int touchProgress = (int) Math.round(valuePerDegree() * angle); + + touchProgress = (touchProgress < 0) ? INVALID_PROGRESS_VALUE + : touchProgress; + touchProgress = (touchProgress > mMax) ? INVALID_PROGRESS_VALUE + : touchProgress; + return touchProgress; + } + + private float valuePerDegree() { + return (float) mMax / mSweepAngle; + } + + private void onProgressRefresh(int progress, boolean fromUser) { + updateProgress(progress, fromUser); + } + + private void updateThumbPosition() { + int thumbAngle = (int) (mStartAngle + mProgressSweep + mRotation + 90); + mThumbXPos = (int) (mArcRadius * Math.cos(Math.toRadians(thumbAngle))); + mThumbYPos = (int) (mArcRadius * Math.sin(Math.toRadians(thumbAngle))); + } + + private void updateProgress(int progress, boolean fromUser) { + + if (progress == INVALID_PROGRESS_VALUE) { + return; + } + + if (mOnSeekArcChangeListener != null) { + mOnSeekArcChangeListener + .onProgressChanged(this, progress, fromUser); + } + + progress = (progress > mMax) ? mMax : progress; + progress = (mProgress < 0) ? 0 : progress; + + mProgress = progress; + mProgressSweep = (float) progress / mMax * mSweepAngle; + + updateThumbPosition(); + + invalidate(); + } + + /** + * Sets a listener to receive notifications of changes to the SeekArc's + * progress level. Also provides notifications of when the user starts and + * stops a touch gesture within the SeekArc. + * + * @param l + * The seek bar notification listener + * + * @see SeekArc.OnSeekBarChangeListener + */ + public void setOnSeekArcChangeListener(OnSeekArcChangeListener l) { + mOnSeekArcChangeListener = l; + } + + public void setProgress(int progress) { + updateProgress(progress, false); + } + + public int getProgressWidth() { + return mProgressWidth; + } + + public void setProgressWidth(int mProgressWidth) { + this.mProgressWidth = mProgressWidth; + mProgressPaint.setStrokeWidth(mProgressWidth); + } + + public int getArcWidth() { + return mArcWidth; + } + + public void setArcWidth(int mArcWidth) { + this.mArcWidth = mArcWidth; + mArcPaint.setStrokeWidth(mArcWidth); + } + public int getArcRotation() { + return mRotation; + } + + public void setArcRotation(int mRotation) { + this.mRotation = mRotation; + updateThumbPosition(); + } + + public int getStartAngle() { + return mStartAngle; + } + + public void setStartAngle(int mStartAngle) { + this.mStartAngle = mStartAngle; + updateThumbPosition(); + } + + public int getSweepAngle() { + return mSweepAngle; + } + + public void setSweepAngle(int mSweepAngle) { + this.mSweepAngle = mSweepAngle; + updateThumbPosition(); + } + + public void setRoundedEdges(boolean isEnabled) { + mRoundedEdges = isEnabled; + if (mRoundedEdges) { + mArcPaint.setStrokeCap(Paint.Cap.ROUND); + mProgressPaint.setStrokeCap(Paint.Cap.ROUND); + } else { + mArcPaint.setStrokeCap(Paint.Cap.SQUARE); + mProgressPaint.setStrokeCap(Paint.Cap.SQUARE); + } + } + + public void setTouchInSide(boolean isEnabled) { + int thumbHalfheight = (int) mThumb.getIntrinsicHeight() / 2; + int thumbHalfWidth = (int) mThumb.getIntrinsicWidth() / 2; + mTouchInside = isEnabled; + if (mTouchInside) { + mTouchIgnoreRadius = (float) mArcRadius / 4; + } else { + // Don't use the exact radius makes interaction too tricky + mTouchIgnoreRadius = mArcRadius + - Math.min(thumbHalfWidth, thumbHalfheight); + } + } + + public void setClockwise(boolean isClockwise) { + mClockwise = isClockwise; + } +} diff --git a/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_disabled_holo.png b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_disabled_holo.png new file mode 100644 index 0000000..62be77c Binary files /dev/null and b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_disabled_holo.png differ diff --git a/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_focused_holo.png b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_focused_holo.png new file mode 100644 index 0000000..754dd2f Binary files /dev/null and b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_focused_holo.png differ diff --git a/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_normal_holo.png b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_normal_holo.png new file mode 100644 index 0000000..d546a73 Binary files /dev/null and b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_normal_holo.png differ diff --git a/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_pressed_holo.png b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_pressed_holo.png new file mode 100644 index 0000000..0b62072 Binary files /dev/null and b/SeekArc/src/main/res/drawable-xhdpi/scrubber_control_pressed_holo.png differ diff --git a/SeekArc/src/main/res/drawable/seek_arc_control_selector.xml b/SeekArc/src/main/res/drawable/seek_arc_control_selector.xml new file mode 100644 index 0000000..3414842 --- /dev/null +++ b/SeekArc/src/main/res/drawable/seek_arc_control_selector.xml @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/SeekArc/src/main/res/values/attrs.xml b/SeekArc/src/main/res/values/attrs.xml new file mode 100644 index 0000000..a2bac7c --- /dev/null +++ b/SeekArc/src/main/res/values/attrs.xml @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SeekArc/src/main/res/values/colors.xml b/SeekArc/src/main/res/values/colors.xml new file mode 100644 index 0000000..9e86417 --- /dev/null +++ b/SeekArc/src/main/res/values/colors.xml @@ -0,0 +1,28 @@ + + + + #FFD8D8D8 + #FF383838 + diff --git a/SeekArc/src/main/res/values/strings.xml b/SeekArc/src/main/res/values/strings.xml new file mode 100644 index 0000000..cf0db82 --- /dev/null +++ b/SeekArc/src/main/res/values/strings.xml @@ -0,0 +1,28 @@ + + + + Seekarc_library + + diff --git a/SeekArc/src/main/res/values/styles.xml b/SeekArc/src/main/res/values/styles.xml new file mode 100644 index 0000000..38838de --- /dev/null +++ b/SeekArc/src/main/res/values/styles.xml @@ -0,0 +1,34 @@ + + + + + + + + diff --git a/settings.gradle b/settings.gradle index cef31ed..d090641 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':HypoAlarm', ':GlowPadBackport' +include ':HypoAlarm', ':GlowPadBackport', ':SeekArc'