package com.applite.androidallinone; import android.animation.Animator; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.content.res.Resources; import android.content.res.TypedArray; import android.graphics.*; import android.os.Build; import android.util.AttributeSet; import android.view.*; import android.widget.Scroller; import com.applite.androidallinone.R; import java.lang.Math; import java.lang.Override; import java.lang.String; import java.util.ArrayList; import java.util.List; /** * Custom view that shows a pie chart and, optionally, a label. */ @TargetApi(Build.VERSION_CODES.HONEYCOMB) public class PieChart extends ViewGroup { private List<Item> mData = new ArrayList<Item>(); private float mTotal = 0.0f; private RectF mPieBounds = new RectF(); private Paint mPiePaint; private Paint mTextPaint; private Paint mShadowPaint; private boolean mShowText = false; private float mTextX = 0.0f; private float mTextY = 0.0f; private float mTextWidth = 0.0f; private float mTextHeight = 0.0f; private int mTextPos = TEXTPOS_LEFT; private float mHighlightStrength = 1.15f; private float mPointerRadius = 2.0f; private float mPointerX; private float mPointerY; private int mPieRotation; private OnCurrentItemChangedListener mCurrentItemChangedListener = null; private int mTextColor; private PieView mPieView; private Scroller mScroller; private ValueAnimator mScrollAnimator; private GestureDetector mDetector; private PointerView mPointerView; // The angle at which we measure the current item. This is // where the pointer points. private int mCurrentItemAngle; // the index of the current item. private int mCurrentItem = 0; private boolean mAutoCenterInSlice; private ObjectAnimator mAutoCenterAnimator; private RectF mShadowBounds = new RectF(); /** * Draw text to the left of the pie chart */ public static final int TEXTPOS_LEFT = 0; /** * Draw text to the right of the pie chart */ public static final int TEXTPOS_RIGHT = 1; /** * The initial fling velocity is divided by this amount. */ public static final int FLING_VELOCITY_DOWNSCALE = 4; /** * */ public static final int AUTOCENTER_ANIM_DURATION = 250; /** * Interface definition for a callback to be invoked when the current * item changes. */ public interface OnCurrentItemChangedListener { void OnCurrentItemChanged(PieChart source, int currentItem); } /** * Class constructor taking only a context. Use this constructor to create * {@link PieChart} objects from your own code. * * @param context */ public PieChart(Context context) { super(context); init(); } /** * Class constructor taking a context and an attribute set. This constructor * is used by the layout engine to construct a {@link PieChart} from a set of * XML attributes. * * @param context * @param attrs An attribute set which can contain attributes from * {@link com.example.android.customviews.R.styleable.PieChart} as well as attributes inherited * from {@link android.view.View}. */ public PieChart(Context context, AttributeSet attrs) { super(context, attrs); // attrs contains the raw values for the XML attributes // that were specified in the layout, which don't include // attributes set by styles or themes, and which may have // unresolved references. Call obtainStyledAttributes() // to get the final values for each attribute. // // This call uses R.styleable.PieChart, which is an array of // the custom attributes that were declared in attrs.xml. TypedArray a = context.getTheme().obtainStyledAttributes( attrs, R.styleable.PieChart, 0, 0 ); try { // Retrieve the values from the TypedArray and store into // fields of this class. // // The R.styleable.PieChart_* constants represent the index for // each custom attribute in the R.styleable.PieChart array. mShowText = a.getBoolean(R.styleable.PieChart_showText, false); mTextY = a.getDimension(R.styleable.PieChart_labelY, 0.0f); mTextWidth = a.getDimension(R.styleable.PieChart_labelWidth, 0.0f); mTextHeight = a.getDimension(R.styleable.PieChart_labelHeight, 0.0f); mTextPos = a.getInteger(R.styleable.PieChart_labelPosition, 0); mTextColor = a.getColor(R.styleable.PieChart_labelColor, 0xff000000); mHighlightStrength = a.getFloat(R.styleable.PieChart_highlightStrength, 1.0f); mPieRotation = a.getInt(R.styleable.PieChart_pieRotation, 0); mPointerRadius = a.getDimension(R.styleable.PieChart_pointerRadius, 2.0f); mAutoCenterInSlice = a.getBoolean(R.styleable.PieChart_autoCenterPointerInSlice, false); } finally { // release the TypedArray so that it can be reused. a.recycle(); } init(); } /** * Returns true if the text label should be visible. * * @return True if the text label should be visible, false otherwise. */ public boolean getShowText() { return mShowText; } /** * Controls whether the text label is visible or not. Setting this property to * false allows the pie chart graphic to take up the entire visible area of * the control. * * @param showText true if the text label should be visible, false otherwise */ public void setShowText(boolean showText) { mShowText = showText; invalidate(); } /** * Returns the Y position of the label text, in pixels. * * @return The Y position of the label text, in pixels. */ public float getTextY() { return mTextY; } /** * Set the Y position of the label text, in pixels. * * @param textY the Y position of the label text, in pixels. */ public void setTextY(float textY) { mTextY = textY; invalidate(); } /** * Returns the width reserved for label text, in pixels. * * @return The width reserved for label text, in pixels. */ public float getTextWidth() { return mTextWidth; } /** * Set the width of the area reserved for label text. This width is constant; it does not * change based on the actual width of the label as the label text changes. * * @param textWidth The width reserved for label text, in pixels. */ public void setTextWidth(float textWidth) { mTextWidth = textWidth; invalidate(); } /** * Returns the height of the label font, in pixels. * * @return The height of the label font, in pixels. */ public float getTextHeight() { return mTextHeight; } /** * Set the height of the label font, in pixels. * * @param textHeight The height of the label font, in pixels. */ public void setTextHeight(float textHeight) { mTextHeight = textHeight; invalidate(); } /** * Returns a value that specifies whether the label text is to the right * or the left of the pie chart graphic. * * @return One of TEXTPOS_LEFT or TEXTPOS_RIGHT. */ public int getTextPos() { return mTextPos; } /** * Set a value that specifies whether the label text is to the right * or the left of the pie chart graphic. * * @param textPos TEXTPOS_LEFT to draw the text to the left of the graphic, * or TEXTPOS_RIGHT to draw the text to the right of the graphic. */ public void setTextPos(int textPos) { if (textPos != TEXTPOS_LEFT && textPos != TEXTPOS_RIGHT) { throw new IllegalArgumentException( "TextPos must be one of TEXTPOS_LEFT or TEXTPOS_RIGHT"); } mTextPos = textPos; invalidate(); } /** * Returns the strength of the highlighting applied to each pie segment. * * @return The highlight strength. */ public float getHighlightStrength() { return mHighlightStrength; } /** * Set the strength of the highlighting that is applied to each pie segment. * This number is a floating point number that is multiplied by the base color of * each segment to get the highlight color. A value of exactly one produces no * highlight at all. Values greater than one produce highlights that are lighter * than the base color, while values less than one produce highlights that are darker * than the base color. * * @param highlightStrength The highlight strength. */ public void setHighlightStrength(float highlightStrength) { if (highlightStrength < 0.0f) { throw new IllegalArgumentException( "highlight strength cannot be negative"); } mHighlightStrength = highlightStrength; invalidate(); } /** * Returns the radius of the filled circle that is drawn at the tip of the current-item * pointer. * * @return The radius of the pointer tip, in pixels. */ public float getPointerRadius() { return mPointerRadius; } /** * Set the radius of the filled circle that is drawn at the tip of the current-item * pointer. * * @param pointerRadius The radius of the pointer tip, in pixels. */ public void setPointerRadius(float pointerRadius) { mPointerRadius = pointerRadius; invalidate(); } /** * Returns the current rotation of the pie graphic. * * @return The current pie rotation, in degrees. */ public int getPieRotation() { return mPieRotation; } /** * Set the current rotation of the pie graphic. Setting this value may change * the current item. * * @param rotation The current pie rotation, in degrees. */ public void setPieRotation(int rotation) { rotation = (rotation % 360 + 360) % 360; mPieRotation = rotation; mPieView.rotateTo(rotation); calcCurrentItem(); } /** * Returns the index of the currently selected data item. * * @return The zero-based index of the currently selected data item. */ public int getCurrentItem() { return mCurrentItem; } /** * Set the currently selected item. Calling this function will set the current selection * and rotate the pie to bring it into view. * * @param currentItem The zero-based index of the item to select. */ public void setCurrentItem(int currentItem) { setCurrentItem(currentItem, true); } /** * Set the current item by index. Optionally, scroll the current item into view. This version * is for internal use--the scrollIntoView option is always true for external callers. * * @param currentItem The index of the current item. * @param scrollIntoView True if the pie should rotate until the current item is centered. * False otherwise. If this parameter is false, the pie rotation * will not change. */ private void setCurrentItem(int currentItem, boolean scrollIntoView) { mCurrentItem = currentItem; if (mCurrentItemChangedListener != null) { mCurrentItemChangedListener.OnCurrentItemChanged(this, currentItem); } if (scrollIntoView) { centerOnCurrentItem(); } invalidate(); } /** * Register a callback to be invoked when the currently selected item changes. * * @param listener Can be null. * The current item changed listener to attach to this view. */ public void setOnCurrentItemChangedListener(OnCurrentItemChangedListener listener) { mCurrentItemChangedListener = listener; } /** * Add a new data item to this view. Adding an item adds a slice to the pie whose * size is proportional to the item's value. As new items are added, the size of each * existing slice is recalculated so that the proportions remain correct. * * @param label The label text to be shown when this item is selected. * @param value The value of this item. * @param color The ARGB color of the pie slice associated with this item. * @return The index of the newly added item. */ public int addItem(String label, float value, int color) { Item it = new Item(); it.mLabel = label; it.mColor = color; it.mValue = value; // Calculate the highlight color. Saturate at 0xff to make sure that high values // don't result in aliasing. it.mHighlight = Color.argb( 0xff, Math.min((int) (mHighlightStrength * (float) Color.red(color)), 0xff), Math.min((int) (mHighlightStrength * (float) Color.green(color)), 0xff), Math.min((int) (mHighlightStrength * (float) Color.blue(color)), 0xff) ); mTotal += value; mData.add(it); onDataChanged(); return mData.size() - 1; } @Override public boolean onTouchEvent(MotionEvent event) { // Let the GestureDetector interpret this event boolean result = mDetector.onTouchEvent(event); // If the GestureDetector doesn't want this event, do some custom processing. // This code just tries to detect when the user is done scrolling by looking // for ACTION_UP events. if (!result) { if (event.getAction() == MotionEvent.ACTION_UP) { // User is done scrolling, it's now safe to do things like autocenter stopScrolling(); result = true; } } return result; } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { // Do nothing. Do not call the superclass method--that would start a layout pass // on this view's children. PieChart lays out its children in onSizeChanged(). } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); // Draw the shadow canvas.drawOval(mShadowBounds, mShadowPaint); // Draw the label text if (getShowText()) { canvas.drawText(mData.get(mCurrentItem).mLabel, mTextX, mTextY, mTextPaint); } // If the API level is less than 11, we can't rely on the view animation system to // do the scrolling animation. Need to tick it here and call postInvalidate() until the scrolling is done. if (Build.VERSION.SDK_INT < 11) { tickScrollAnimation(); if (!mScroller.isFinished()) { postInvalidate(); } } } // // Measurement functions. This example uses a simple heuristic: it assumes that // the pie chart should be at least as wide as its label. // @Override protected int getSuggestedMinimumWidth() { return (int) mTextWidth * 2; } @Override protected int getSuggestedMinimumHeight() { return (int) mTextWidth; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // Try for a width based on our minimum int minw = getPaddingLeft() + getPaddingRight() + getSuggestedMinimumWidth(); int w = Math.max(minw, MeasureSpec.getSize(widthMeasureSpec)); // Whatever the width ends up being, ask for a height that would let the pie // get as big as it can int minh = (w - (int) mTextWidth) + getPaddingBottom() + getPaddingTop(); int h = Math.min(MeasureSpec.getSize(heightMeasureSpec), minh); setMeasuredDimension(w, h); } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); // // Set dimensions for text, pie chart, etc // // Account for padding float xpad = (float) (getPaddingLeft() + getPaddingRight()); float ypad = (float) (getPaddingTop() + getPaddingBottom()); // Account for the label if (mShowText) xpad += mTextWidth; float ww = (float) w - xpad; float hh = (float) h - ypad; // Figure out how big we can make the pie. float diameter = Math.min(ww, hh); mPieBounds = new RectF( 0.0f, 0.0f, diameter, diameter); mPieBounds.offsetTo(getPaddingLeft(), getPaddingTop()); mPointerY = mTextY - (mTextHeight / 2.0f); float pointerOffset = mPieBounds.centerY() - mPointerY; // Make adjustments based on text position if (mTextPos == TEXTPOS_LEFT) { mTextPaint.setTextAlign(Paint.Align.RIGHT); if (mShowText) mPieBounds.offset(mTextWidth, 0.0f); mTextX = mPieBounds.left; if (pointerOffset < 0) { pointerOffset = -pointerOffset; mCurrentItemAngle = 225; } else { mCurrentItemAngle = 135; } mPointerX = mPieBounds.centerX() - pointerOffset; } else { mTextPaint.setTextAlign(Paint.Align.LEFT); mTextX = mPieBounds.right; if (pointerOffset < 0) { pointerOffset = -pointerOffset; mCurrentItemAngle = 315; } else { mCurrentItemAngle = 45; } mPointerX = mPieBounds.centerX() + pointerOffset; } mShadowBounds = new RectF( mPieBounds.left + 10, mPieBounds.bottom + 10, mPieBounds.right - 10, mPieBounds.bottom + 20); // Lay out the child view that actually draws the pie. mPieView.layout((int) mPieBounds.left, (int) mPieBounds.top, (int) mPieBounds.right, (int) mPieBounds.bottom); mPieView.setPivot(mPieBounds.width() / 2, mPieBounds.height() / 2); mPointerView.layout(0, 0, w, h); onDataChanged(); } /** * Calculate which pie slice is under the pointer, and set the current item * field accordingly. */ private void calcCurrentItem() { int pointerAngle = (mCurrentItemAngle + 360 + mPieRotation) % 360; for (int i = 0; i < mData.size(); ++i) { Item it = mData.get(i); if (it.mStartAngle <= pointerAngle && pointerAngle <= it.mEndAngle) { if (i != mCurrentItem) { setCurrentItem(i, false); } break; } } } /** * Do all of the recalculations needed when the data array changes. */ private void onDataChanged() { // When the data changes, we have to recalculate // all of the angles. int currentAngle = 0; for (Item it : mData) { it.mStartAngle = currentAngle; it.mEndAngle = (int) ((float) currentAngle + it.mValue * 360.0f / mTotal); currentAngle = it.mEndAngle; // Recalculate the gradient shaders. There are // three values in this gradient, even though only // two are necessary, in order to work around // a bug in certain versions of the graphics engine // that expects at least three values if the // positions array is non-null. // it.mShader = new SweepGradient( mPieBounds.width() / 2.0f, mPieBounds.height() / 2.0f, new int[]{ it.mHighlight, it.mHighlight, it.mColor, it.mColor, }, new float[]{ 0, (float) (360 - it.mEndAngle) / 360.0f, (float) (360 - it.mStartAngle) / 360.0f, 1.0f } ); } calcCurrentItem(); onScrollFinished(); } /** * Initialize the control. This code is in a separate method so that it can be * called from both constructors. */ private void init() { // Force the background to software rendering because otherwise the Blur // filter won't work. setLayerToSW(this); // Set up the paint for the label text mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG); mTextPaint.setColor(mTextColor); if (mTextHeight == 0) { mTextHeight = mTextPaint.getTextSize(); } else { mTextPaint.setTextSize(mTextHeight); } // Set up the paint for the pie slices mPiePaint = new Paint(Paint.ANTI_ALIAS_FLAG); mPiePaint.setStyle(Paint.Style.FILL); mPiePaint.setTextSize(mTextHeight); // Set up the paint for the shadow mShadowPaint = new Paint(0); mShadowPaint.setColor(0xff101010); mShadowPaint.setMaskFilter(new BlurMaskFilter(8, BlurMaskFilter.Blur.NORMAL)); // Add a child view to draw the pie. Putting this in a child view // makes it possible to draw it on a separate hardware layer that rotates // independently mPieView = new PieView(getContext()); addView(mPieView); mPieView.rotateTo(mPieRotation); // The pointer doesn't need hardware acceleration, but in order to show up // in front of the pie it also needs to be on a separate view. mPointerView = new PointerView(getContext()); addView(mPointerView); // Set up an animator to animate the PieRotation property. This is used to // correct the pie's orientation after the user lets go of it. if (Build.VERSION.SDK_INT >= 11) { mAutoCenterAnimator = ObjectAnimator.ofInt(PieChart.this, "PieRotation", 0); // Add a listener to hook the onAnimationEnd event so that we can do // some cleanup when the pie stops moving. mAutoCenterAnimator.addListener(new Animator.AnimatorListener() { public void onAnimationStart(Animator animator) { } public void onAnimationEnd(Animator animator) { mPieView.decelerate(); } public void onAnimationCancel(Animator animator) { } public void onAnimationRepeat(Animator animator) { } }); } // Create a Scroller to handle the fling gesture. if (Build.VERSION.SDK_INT < 11) { mScroller = new Scroller(getContext()); } else { mScroller = new Scroller(getContext(), null, true); } // The scroller doesn't have any built-in animation functions--it just supplies // values when we ask it to. So we have to have a way to call it every frame // until the fling ends. This code (ab)uses a ValueAnimator object to generate // a callback on every animation frame. We don't use the animated value at all. if (Build.VERSION.SDK_INT >= 11) { mScrollAnimator = ValueAnimator.ofFloat(0, 1); mScrollAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() { public void onAnimationUpdate(ValueAnimator valueAnimator) { tickScrollAnimation(); } }); } // Create a gesture detector to handle onTouch messages mDetector = new GestureDetector(PieChart.this.getContext(), new GestureListener()); // Turn off long press--this control doesn't use it, and if long press is enabled, // you can't scroll for a bit, pause, then scroll some more (the pause is interpreted // as a long press, apparently) mDetector.setIsLongpressEnabled(false); // In edit mode it's nice to have some demo data, so add that here. if (this.isInEditMode()) { Resources res = getResources(); addItem("Annabelle", 3, res.getColor(R.color.bluegrass)); addItem("Brunhilde", 4, res.getColor(R.color.chartreuse)); addItem("Carolina", 2, res.getColor(R.color.emerald)); addItem("Dahlia", 3, res.getColor(R.color.seafoam)); addItem("Ekaterina", 1, res.getColor(R.color.slate)); } } private void tickScrollAnimation() { if (!mScroller.isFinished()) { mScroller.computeScrollOffset(); setPieRotation(mScroller.getCurrY()); } else { if (Build.VERSION.SDK_INT >= 11) { mScrollAnimator.cancel(); } onScrollFinished(); } } @SuppressLint("NewApi") private void setLayerToSW(View v) { if (!v.isInEditMode() && Build.VERSION.SDK_INT >= 11) { setLayerType(View.LAYER_TYPE_SOFTWARE, null); } } private void setLayerToHW(View v) { if (!v.isInEditMode() && Build.VERSION.SDK_INT >= 11) { setLayerType(View.LAYER_TYPE_HARDWARE, null); } } /** * Force a stop to all pie motion. Called when the user taps during a fling. */ @SuppressLint("NewApi") private void stopScrolling() { mScroller.forceFinished(true); if (Build.VERSION.SDK_INT >= 11) { mAutoCenterAnimator.cancel(); } onScrollFinished(); } /** * Called when the user finishes a scroll action. */ private void onScrollFinished() { if (mAutoCenterInSlice) { centerOnCurrentItem(); } else { mPieView.decelerate(); } } /** * Kicks off an animation that will result in the pointer being centered in the * pie slice of the currently selected item. */ @SuppressLint("NewApi") private void centerOnCurrentItem() { Item current = mData.get(getCurrentItem()); int targetAngle = current.mStartAngle + (current.mEndAngle - current.mStartAngle) / 2; targetAngle -= mCurrentItemAngle; if (targetAngle < 90 && mPieRotation > 180) targetAngle += 360; if (Build.VERSION.SDK_INT >= 11) { // Fancy animated version mAutoCenterAnimator.setIntValues(targetAngle); mAutoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION).start(); } else { // Dull non-animated version //mPieView.rotateTo(targetAngle); } } /** * Internal child class that draws the pie chart onto a separate hardware layer * when necessary. */ private class PieView extends View { // Used for SDK < 11 private float mRotation = 0; private Matrix mTransform = new Matrix(); private PointF mPivot = new PointF(); /** * Construct a PieView * * @param context */ public PieView(Context context) { super(context); } /** * Enable hardware acceleration (consumes memory) */ public void accelerate() { setLayerToHW(this); } /** * Disable hardware acceleration (releases memory) */ public void decelerate() { setLayerToSW(this); } @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (Build.VERSION.SDK_INT < 11) { mTransform.set(canvas.getMatrix()); mTransform.preRotate(mRotation, mPivot.x, mPivot.y); canvas.setMatrix(mTransform); } for (Item it : mData) { mPiePaint.setShader(it.mShader); canvas.drawArc(mBounds, 360 - it.mEndAngle, it.mEndAngle - it.mStartAngle, true, mPiePaint); } } @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { mBounds = new RectF(0, 0, w, h); } RectF mBounds; public void rotateTo(float pieRotation) { mRotation = pieRotation; if (Build.VERSION.SDK_INT >= 11) { setRotation(pieRotation); } else { invalidate(); } } public void setPivot(float x, float y) { mPivot.x = x; mPivot.y = y; if (Build.VERSION.SDK_INT >= 11) { setPivotX(x); setPivotY(y); } else { invalidate(); } } } /** * View that draws the pointer on top of the pie chart */ private class PointerView extends View { /** * Construct a PointerView object * * @param context */ public PointerView(Context context) { super(context); } @Override protected void onDraw(Canvas canvas) { canvas.drawLine(mTextX, mPointerY, mPointerX, mPointerY, mTextPaint); canvas.drawCircle(mPointerX, mPointerY, mPointerRadius, mTextPaint); } } /** * Maintains the state for a data item. */ private class Item { public String mLabel; public float mValue; public int mColor; // computed values public int mStartAngle; public int mEndAngle; public int mHighlight; public Shader mShader; } /** * Extends {@link GestureDetector.SimpleOnGestureListener} to provide custom gesture * processing. */ private class GestureListener extends GestureDetector.SimpleOnGestureListener { @Override public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { // Set the pie rotation directly. float scrollTheta = vectorToScalarScroll( distanceX, distanceY, e2.getX() - mPieBounds.centerX(), e2.getY() - mPieBounds.centerY()); setPieRotation(getPieRotation() - (int) scrollTheta / FLING_VELOCITY_DOWNSCALE); return true; } @Override public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { // Set up the Scroller for a fling float scrollTheta = vectorToScalarScroll( velocityX, velocityY, e2.getX() - mPieBounds.centerX(), e2.getY() - mPieBounds.centerY()); mScroller.fling( 0, (int) getPieRotation(), 0, (int) scrollTheta / FLING_VELOCITY_DOWNSCALE, 0, 0, Integer.MIN_VALUE, Integer.MAX_VALUE); // Start the animator and tell it to animate for the expected duration of the fling. if (Build.VERSION.SDK_INT >= 11) { mScrollAnimator.setDuration(mScroller.getDuration()); mScrollAnimator.start(); } return true; } @Override public boolean onDown(MotionEvent e) { // The user is interacting with the pie, so we want to turn on acceleration // so that the interaction is smooth. mPieView.accelerate(); if (isAnimationRunning()) { stopScrolling(); } return true; } } private boolean isAnimationRunning() { return !mScroller.isFinished() || (Build.VERSION.SDK_INT >= 11 && mAutoCenterAnimator.isRunning()); } /** * Helper method for translating (x,y) scroll vectors into scalar rotation of the pie. * * @param dx The x component of the current scroll vector. * @param dy The y component of the current scroll vector. * @param x The x position of the current touch, relative to the pie center. * @param y The y position of the current touch, relative to the pie center. * @return The scalar representing the change in angular position for this scroll. */ private static float vectorToScalarScroll(float dx, float dy, float x, float y) { // get the length of the vector float l = (float) Math.sqrt(dx * dx + dy * dy); // decide if the scalar should be negative or positive by finding // the dot product of the vector perpendicular to (x,y). float crossX = -y; float crossY = x; float dot = (crossX * dx + crossY * dy); float sign = Math.signum(dot); return l * sign; } }