diff options
Diffstat (limited to 'src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java')
-rw-r--r-- | src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java | 570 |
1 files changed, 570 insertions, 0 deletions
diff --git a/src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java b/src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java new file mode 100644 index 0000000..0e4cbb2 --- /dev/null +++ b/src/org/cyanogenmod/audiofx/audiofx/knobs/RadialKnob.java @@ -0,0 +1,570 @@ +/* + * Copyright (c) 2013, The Linux Foundation. All rights reserved. + * Copyright (c) 2015, The CyanogenMod Project. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * * Neither the name of The Linux Foundation nor the names of its + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS + * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR + * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE + * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.cyngn.audiofx.knobs; + +import android.animation.Animator; +import android.animation.ValueAnimator; +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.PathMeasure; +import android.graphics.RectF; +import android.graphics.Typeface; +import android.os.Vibrator; +import android.util.AttributeSet; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.Toast; +import com.cyngn.audiofx.R; +import com.cyngn.audiofx.stats.UserSession; + +public class RadialKnob extends View { + + private static final String TAG = RadialKnob.class.getSimpleName(); + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + public static final float REGULAR_SCALE = 0.8f; + + public static final float TOUCHING_SCALE = 1f; + private static final int DO_NOT_VIBRATE_THRESHOLD = 100; + + private static final int DEGREE_OFFSET = -225; + private static final int START_ANGLE = 360 + DEGREE_OFFSET; + private static final int MAX_DEGREES = 270; + + private final Paint mPaint, mTextPaint; + + ValueAnimator mAnimator; + float mOffProgress; + boolean mAnimating = false; + long mDownTime; + long mUpTime; + private OnKnobChangeListener mOnKnobChangeListener = null; + private float mProgress = 0.0f; + private float mTouchProgress = 0.0f; + private int mMax = 100; + private boolean mOn = false; + private boolean mEnabled = true; + private float mLastX; + private float mLastY; + private boolean mMoved; + private int mWidth = 0; + private RectF mRectF, mOuterRect = new RectF(), mInnerRect = new RectF(); + private float mLastAngle; + private Long mLastVibrateTime; + private int mHighlightColor; + private int mBackgroundArcColor; + private int mBackgroundArcColorDisabled; + private int mRectPadding; + private int mStrokeWidth; + private float mHandleWidth; // little square indicator where user touches + private float mTextOffset; + + Path mPath = new Path(); + PathMeasure mPathMeasure = new PathMeasure(); + float[] mTmp = new float[2]; + float mStartX, mStopX, mStartY, mStopY; + + public RadialKnob(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + + Resources res = getResources(); + mBackgroundArcColor = res.getColor(R.color.radial_knob_arc_bg); + mBackgroundArcColorDisabled = res.getColor(R.color.radial_knob_arc_bg_disabled); + mHighlightColor = res.getColor(R.color.highlight); + + mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD)); + mTextPaint.setTextAlign(Paint.Align.CENTER); + mTextPaint.setElegantTextHeight(true); + mTextPaint.setFakeBoldText(true); + mTextPaint.setTextSize(res.getDimension(R.dimen.radial_text_size)); + mTextPaint.setColor(Color.LTGRAY); + + mTextOffset = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 2, + getResources().getDisplayMetrics()); + + mHandleWidth = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 5, + getResources().getDisplayMetrics()); + + mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + mPaint.setColor(mHighlightColor); + mPaint.setStrokeWidth(mStrokeWidth = res.getDimensionPixelSize(R.dimen.radial_knob_stroke)); + mPaint.setStrokeCap(Paint.Cap.BUTT); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setShadowLayer(2, 1, -2, getResources().getColor(R.color.black)); + + setScaleX(REGULAR_SCALE); + setScaleY(REGULAR_SCALE); + + mRectPadding = res.getDimensionPixelSize(R.dimen.radial_rect_padding); + invalidate(); + } + + public RadialKnob(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public RadialKnob(Context context) { + this(context, null); + } + + public void setValue(int value) { + if (mMax != 0) { + setProgress(((float) value) / mMax); + mTouchProgress = mProgress; + mLastAngle = mProgress * MAX_DEGREES; + } + } + + public void setProgress(float progress, boolean fromUser) { + if (progress > 1.0f) { + progress = 1.0f; + } + if (progress < 0.0f) { + progress = 0.0f; + } + + mProgress = progress; + + invalidate(); + + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onValueChanged(this, (int) (progress * mMax), fromUser); + } + } + + public void setMax(int max) { + mMax = max; + } + + public float getProgress() { + return mProgress; + } + + public void setProgress(float progress) { + setProgress(progress, false); + } + + @Override + public boolean isEnabled() { + return mEnabled; + } + + @Override + public void setEnabled(boolean enabled) { + mEnabled = enabled; + if (enabled) { + setOn(mOn); + } + invalidate(); + } + + public void setOn(final boolean on) { + mOn = on; + if (mAnimator != null) { + mAnimator.cancel(); + } + invalidate(); + } + + public void setHighlightColor(int color) { + mPaint.setColor(mHighlightColor = color); + invalidate(); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + mPaint.setStrokeWidth(mStrokeWidth); + + mPaint.setColor(mEnabled ? mBackgroundArcColor : mBackgroundArcColorDisabled); + canvas.drawArc(mRectF, START_ANGLE, MAX_DEGREES, false, mPaint); + + final float sweepAngle = mEnabled ? mProgress * MAX_DEGREES : 0; + if (mOn) { + mPaint.setColor(mHighlightColor); + canvas.drawArc(mRectF, START_ANGLE, sweepAngle, false, mPaint); + } + + final float indicatorSweepAngle = Math.max(1f, sweepAngle); + + // render the indicator + mPath.reset(); + mPath.arcTo(mInnerRect, START_ANGLE, indicatorSweepAngle, true); + + mPathMeasure.setPath(mPath, false); + mPathMeasure.getPosTan(mPathMeasure.getLength(), mTmp, null); + + mStartX = mTmp[0]; + mStartY = mTmp[1]; + + mPath.reset(); + mPath.arcTo(mOuterRect, START_ANGLE, indicatorSweepAngle, true); + + mPathMeasure.setPath(mPath, false); + mPathMeasure.getPosTan(mPathMeasure.getLength(), mTmp, null); + + mStopX = mTmp[0]; + mStopY = mTmp[1]; + + mPaint.setStrokeWidth(mHandleWidth); + mPaint.setColor(Color.WHITE); + canvas.drawLine(mStartX, mStartY, mStopX, mStopY, mPaint); + + canvas.drawText(getProgressText(), + mOuterRect.centerX(), + mOuterRect.centerY() + (mTextPaint.getTextSize() / 2.f) - mTextOffset, + mTextPaint); + } + + private String getProgressText() { + if (mEnabled) { + return ((int) (mProgress * 100)) + "%"; + } else { + return "--"; + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldW, int oldH) { + super.onSizeChanged(w, h, oldW, oldH); + + int size = w > h ? h : w; + mWidth = size; + int diff; + if (w > h) { + diff = (w - h) / 2; + mRectF = new RectF(mRectPadding + diff, mRectPadding, + w - mRectPadding - diff, h - mRectPadding); + } else { + diff = (h - w) / 2; + mRectF = new RectF(mRectPadding, mRectPadding + diff, + w - mRectPadding, h - mRectPadding - diff); + } + mOuterRect.set(mRectF); + mOuterRect.inset(-mRectPadding, -mRectPadding); + mInnerRect.set(mRectF); + mInnerRect.inset(mRectPadding, mRectPadding); + } + + private boolean isUserSelected() { + return getScaleX() == TOUCHING_SCALE && getScaleY() == TOUCHING_SCALE; + } + + private void animateTo(float progress) { + if (DEBUG) Log.w(TAG, "animateTo(" + progress + ")"); + if (mAnimator != null) { + mAnimator.cancel(); + } + mAnimator = ValueAnimator.ofFloat(mProgress, progress); + mAnimator.setDuration(100); + mAnimator.setInterpolator(new AccelerateInterpolator()); + mAnimator.addListener(new Animator.AnimatorListener() { + @Override + public void onAnimationStart(Animator animation) { + mAnimating = true; + } + + @Override + public void onAnimationEnd(Animator animation) { + mAnimating = false; + postInvalidate(); + } + + @Override + public void onAnimationCancel(Animator animation) { + mAnimating = false; + postInvalidate(); + } + + @Override + public void onAnimationRepeat(Animator animation) { + + } + }); + mAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + float progress = (Float) animation.getAnimatedValue(); + mProgress = progress; + mLastAngle = mProgress * MAX_DEGREES; + if (DEBUG) Log.i(TAG, "onAnimationUpdate(): mProgress: " + + mProgress + ", mLastAngle: " + mLastAngle); + + setProgress(mProgress); + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onValueChanged(RadialKnob.this, + (int) (progress * mMax), true); + } + postInvalidate(); + } + }); + mAnimator.start(); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final float x = event.getX(); + final float y = event.getY(); + + if (!mEnabled) { + return false; + } + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + mDownTime = System.currentTimeMillis(); + mOffProgress = 0; + + getParent().requestDisallowInterceptTouchEvent(true); + vibrate(); + mLastX = event.getX(); + mLastY = event.getY(); + break; + case MotionEvent.ACTION_MOVE: + // we can be animating while moving + if (mAnimating) { + return true; + } + final float center = getWidth() / 2; + final float radius = (center / 2) - (mRectPadding * 2); + + final boolean inDeadzone = inCircle(x, y, center, center, radius); + final boolean inOuterCircle = inCircle(x, y, center, center, radius + 70); + if (DEBUG) + Log.d(TAG, "inOuterCircle: " + inOuterCircle + ", inDeadzone: " + inDeadzone); + final float delta = getDelta(x, y); + final float angle = angleWithOffset(x, y, DEGREE_OFFSET); + + if (mOn) { + if (isUserSelected() && (!inDeadzone)) { + float angleDiff = Math.abs(mLastAngle - angle); + if (mProgress == 1 && angle < (MAX_DEGREES / 2)) { + // oh jeez. no jumping from 100! + return true; + } + if (angleDiff < 90) { + // jump! + //Log.w(TAG, "using angle"); + mLastAngle = angle; + mTouchProgress = angle / MAX_DEGREES; + mMoved = true; + if (DEBUG) Log.v(TAG, "ANGLE setProgress: " + mTouchProgress); + setProgress(mTouchProgress, true); + } else if (angle > 0 && angle < MAX_DEGREES) { + if (DEBUG) Log.v(TAG, "ANGLE animateTo: " + angle); + mMoved = true; + animateTo(angle / MAX_DEGREES); + } + } + // if it's less than one degree, turn it off + // 1% ~= 2.7 degrees, pick something slightly higher + if (mTouchProgress < (2.71f / MAX_DEGREES) && mOn && mMoved) { + mTouchProgress = (2.71f / MAX_DEGREES); + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onSwitchChanged(this, !mOn); + } + setOn(!mOn); + } + } else { + // off + if (isUserSelected() && (!inDeadzone)) { + if (delta > 0) { + mOffProgress += delta; + } else if (angle > 90) { + mOffProgress = 0; + } + if (DEBUG) + Log.d(TAG, "OFF, touching angle: " + angle + + ", mOffProgress: " + mOffProgress + ", delta " + delta); + // we want at least 1%, how many degrees = 1%? + a little padding + final float onePercentInDegrees = (MAX_DEGREES / 100) + 1f; + if (mOffProgress > 15 && angle < MAX_DEGREES + && angle >= onePercentInDegrees) { + if (DEBUG) Log.w(TAG, "delta: " + delta); + if (angle <= MAX_DEGREES) { + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onSwitchChanged(this, !mOn); + } + + setOn(!mOn); + if (angle > 30) { + animateTo(angle / MAX_DEGREES); + } else { + setProgress(angle / MAX_DEGREES, true); + } + mLastAngle = angle; + mMoved = false; + } else { + if (DEBUG) Log.w(TAG, "off, angle > 300, ignoring"); + } + } + } + mLastX = x; + mLastY = y; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mUpTime = System.currentTimeMillis(); + final float finalAngle = angleWithOffset(x, y, DEGREE_OFFSET); + if (DEBUG) Log.d(TAG, "angle at death: " + finalAngle); + if (mUpTime - mDownTime < 100 && mMoved && finalAngle < MAX_DEGREES) { + if (mOn) { + animateTo(finalAngle / MAX_DEGREES); + } else { + if (mOnKnobChangeListener != null) { + mOnKnobChangeListener.onSwitchChanged(this, !mOn); + } + + setOn(!mOn); + } + } + if (mMoved) { + UserSession.getInstance() + .knobOptionsAdjusted(((KnobContainer.KnobInfo)getTag()).whichKnob); + } + mLastX = -1; + mLastY = -1; + mOffProgress = 0; + mMoved = false; + break; + default: + break; + } + return true; + } + + private void vibrate() { + if (mLastVibrateTime == null || System.currentTimeMillis() - mLastVibrateTime + > DO_NOT_VIBRATE_THRESHOLD) { + Vibrator vibrator = (Vibrator) mContext.getSystemService(Context.VIBRATOR_SERVICE); + vibrator.vibrate(40); + mLastVibrateTime = System.currentTimeMillis(); + } + } + + public void resize(boolean selected) { + if (!mEnabled) { + return; + } + if (selected) { + animate() + .scaleY(RadialKnob.TOUCHING_SCALE) + .scaleX(RadialKnob.TOUCHING_SCALE) + .setDuration(100); + } else { + animate() + .scaleY(RadialKnob.REGULAR_SCALE) + .scaleX(RadialKnob.REGULAR_SCALE) + .setDuration(100); + } + } + + private float getDelta(float x, float y) { + float angle = angle(x, y); + float oldAngle = angle(mLastX, mLastY); + float delta = angle - oldAngle; + if (delta >= 180.0f) { + delta = -oldAngle; + } else if (delta <= -180.0f) { + delta = 360 - oldAngle; + } + return delta; + } + + private float angle(float x, float y) { + float center = mWidth / 2.0f; + x -= center; + y -= center; + + if (x == 0.0f) { + if (y > 0.0f) { + return 180.0f; + } else { + return 0.0f; + } + } + + float angle = (float) (Math.atan(y / x) / Math.PI * 180.0); + if (x > 0.0f) { + angle += 90; + } else { + angle += 270; + } + return angle; + } + + private float angleWithOffset(float x, float y, int degreeOffset) { + float angle = angle(x, y); + if (angle > 180) { + angle += degreeOffset; + } else { + angle += (360 + degreeOffset); + } + return angle; + } + + + private static boolean inCircle(float x, float y, float circleCenterX, float circleCenterY, + float circleRadius) { + double dx = Math.pow(x - circleCenterX, 2); + double dy = Math.pow(y - circleCenterY, 2); + + if ((dx + dy) < Math.pow(circleRadius, 2)) { + return true; + } else { + return false; + } + } + + @Override + public boolean hasOverlappingRendering() { + return false; + } + + public void setOnKnobChangeListener(OnKnobChangeListener l) { + mOnKnobChangeListener = l; + } + + public interface OnKnobChangeListener { + void onValueChanged(RadialKnob knob, int value, boolean fromUser); + + boolean onSwitchChanged(RadialKnob knob, boolean on); + } +} |