Related
When using scaling, MotionEvent coordinates can be corrected by dividing by the ScaleFactor.
Further, when scaling and paning, divide by scalefactor and subtract offset.
When dealing with zoom, however, it isn't as easy. Dividing does get the correct relative coordinates, but because pan is involved, 0 isn't 0. 0 can be -2000 in offset.
So how can I correct the TouchEvents to give the correct coordinates after zoom and pan?
Code:
Zoom:
class Scaler extends ScaleGestureDetector {
public Scaler(Context context, OnScaleGestureListener listener) {
super(context, listener);
}
#Override
public float getScaleFactor() {
return super.getScaleFactor();
}
}
class ScaleListener implements ScaleGestureDetector.OnScaleGestureListener{
#Override
public boolean onScale(ScaleGestureDetector detector) {
scaleFactor *= detector.getScaleFactor();
if(scaleFactor > 2) scaleFactor = 2;
else if(scaleFactor < 0.3f) scaleFactor = 0.3f;
scaleFactor = ((float)((int)(scaleFactor * 100))) / 100;//jitter-protection
scaleMatrix.setScale(scaleFactor, scaleFactor, detector.getFocusX(), detector.getFocusY());
return true;
}
#Override
public boolean onScaleBegin(ScaleGestureDetector detector) {return true;}
#Override
public void onScaleEnd(ScaleGestureDetector detector) {
System.out.println("ScaleFactor: " + scaleFactor);
}
}
TouchEvent:
#Override
public boolean onTouchEvent(MotionEvent ev) {
int pointers = ev.getPointerCount();
if(pointers == 2 ) {
zoom = true;
s.onTouchEvent(ev);
}else if(pointers == 1 && zoom){
if(ev.getAction() == MotionEvent.ACTION_UP)
zoom = false;
return true;
}else {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//scaled physical coordinates
x = ev.getX() /*/ mScaleFactorX*/;//unscaled
y = ev.getY() /*/ mScaleFactorY*/;
sx = ev.getX() / scaleFactor;//scaled
sy = ev.getY() / scaleFactor;
//////////////////////////////////////////
tox = toy = true;
} else if (ev.getAction() == MotionEvent.ACTION_UP) {
if (tox && toy) {
x = ev.getX() /*/ mScaleFactorX*/;
y = ev.getY() /*/ mScaleFactorY*/;
sx = ev.getX() / scaleFactor;
sy = ev.getY() / scaleFactor;
System.out.println("XY: " + sx + "/" + sy);
Rect cursor = new Rect((int) x, (int) y, (int) x + 1, (int) y + 1);
Rect scaledCursor = new Rect((int)sx, (int)sy, (int)sx+1, (int)sy+1);
...
}
} else if (ev.getAction() == MotionEvent.ACTION_MOVE) {
//This is where the pan happens.
float currX = ev.getX() / scaleFactor;
float currY = ev.getY() / scaleFactor;
float newOffsetX = (sx - currX),
newOffsetY = (sy - currY);
if (newOffsetY < Maths.convertDpToPixel(1, c) && newOffsetY > -Maths.convertDpToPixel(1, c))
newOffsetY = 0;
else tox = false;
if (newOffsetX < Maths.convertDpToPixel(1, c) && newOffsetX > -Maths.convertDpToPixel(1, c))
newOffsetX = 0;
else toy = false;
this.newOffsetX = newOffsetX;
this.newOffsetY = newOffsetY;
offsetX += newOffsetX;
offsetY += newOffsetY;
sx = ev.getX() / scaleFactor;
sy = ev.getY() / scaleFactor;
}
}
return true;
}
Implementation of the zooming matrix:
Matrix scaleMatrix = new Matrix();
public void render(Canvas c) {
super.draw(c);
if (c != null) {
backgroundRender(c);
c.setMatrix(scaleMatrix);
//Example rendering:
c.drawRect(0 - offsetX,0 - offsetY,10 - offsetX,10 - offsetY,paint);
c.setMatrix(null);//null the matrix to allow for unscaled rendering after this line. For UI objects.
}
}
What the issue is, is that when zooming 0 shifts but the coordinates of the objects does not. Meaning objects rendered at e.g. -2500, -2500 will appear to be rendered at over 0,0. Their coordinates are different from the TouchEvent. So how can I correct the touch events?
What I have tried:
This causes laggy zoom and the objects flying away. ev = MotionEvent in onTouchEvent. Doesn't correct the coordinates
Matrix invert = new Matrix(scaleMatrix);
invert.invert(invert);
ev.transform();
This doesn't work because the coordinates are wrong compared to objects. Objects with coordinates < 0 show over 0 meaning MotionEvents are wrong no matter what.
int sx = ev.getX() / scaleFactor;//same with y, but ev.getY()
Found a solution after doing a ton more research
Whenever getting the scaled coordinates, get the clipBounds of the canvas and add the top and left coordinates to X/Y coordinates:
sx = ev.getX() / scaleFactor + clip.left;
sy = ev.getY() / scaleFactor + clip.top ;
clip is a Rect defined as the clipBounds of the Canvas.
public void render(Canvas c) {
super.draw(c);
if (c != null) {
c.setMatrix(scaleMatrix);
clip = c.getClipBounds();
(...)
}
}
I'm trying to animate some drawables in Android, I've set a path using PathEvaluator that animates along some curves along a full path.
When I set a duration (e.g. 6 seconds) it splits the duration to the number of curves I've set regardless of their length which causes the animation to be to slow on some segments and too fast on others.
On iOS this can be fixed using
animation.calculationMode = kCAAnimationCubicPaced;
animation.timingFunction = ...;
Which lets iOS to smooth your entire path into mid-points and span the duration according to each segment length.
Is there any way to get the same result in Android?
(besides breaking the path into discrete segments and assigning each segment its own duration manually which is really ugly and unmaintainable).
I don't think that anything can be done with ObjectAnimator because there seems to be no function that can be called to assert the relative duration of a certain fragment of the animation.
I did develop something similar to what you need a while back, but it works slightly differently - it inherits from Animation.
I've modified everything to work with your curving needs, and with the PathPoint class.
Here's an overview:
I supply the list of points to the animation in the constructor.
I calculate the length between all the points using a simple distance calculator. I then sum it all up to get the overall length of the path, and store the segment lengths in a map for future use (this is to improve efficiency during runtime).
When animating, I use the current interpolation time to figure out which 2 points I'm animating between, considering the ratio of time & the ratio of distance traveled.
I calculate the time it should take to animate between these 2 points according to the relative distance between them, compared to the overall distance.
I then interpolate separately between these 2 points using the calculation in the PathAnimator class.
Here's the code:
CurveAnimation.java:
public class CurveAnimation extends Animation
{
private static final float BEZIER_LENGTH_ACCURACY = 0.001f; // Must be divisible by one. Make smaller to improve accuracy, but will increase runtime at start of animation.
private List<PathPoint> mPathPoints;
private float mOverallLength;
private Map<PathPoint, Double> mSegmentLengths = new HashMap<PathPoint, Double>(); // map between the end point and the length of the path to it.
public CurveAnimation(List<PathPoint> pathPoints)
{
mPathPoints = pathPoints;
if (mPathPoints == null || mPathPoints.size() < 2)
{
Log.e("CurveAnimation", "There must be at least 2 points on the path. There will be an exception soon!");
}
calculateOverallLength();
}
#Override
protected void applyTransformation(float interpolatedTime, Transformation t)
{
PathPoint[] startEndPart = getStartEndForTime(interpolatedTime);
PathPoint startPoint = startEndPart[0];
PathPoint endPoint = startEndPart[1];
float startTime = getStartTimeOfPoint(startPoint);
float endTime = getStartTimeOfPoint(endPoint);
float progress = (interpolatedTime - startTime) / (endTime - startTime);
float x, y;
float[] xy;
if (endPoint.mOperation == PathPoint.CURVE)
{
xy = getBezierXY(startPoint, endPoint, progress);
x = xy[0];
y = xy[1];
}
else if (endPoint.mOperation == PathPoint.LINE)
{
x = startPoint.mX + progress * (endPoint.mX - startPoint.mX);
y = startPoint.mY + progress * (endPoint.mY - startPoint.mY);
}
else
{
x = endPoint.mX;
y = endPoint.mY;
}
t.getMatrix().setTranslate(x, y);
super.applyTransformation(interpolatedTime, t);
}
private PathPoint[] getStartEndForTime(float time)
{
double length = 0;
if (time == 1)
{
return new PathPoint[] { mPathPoints.get(mPathPoints.size() - 2), mPathPoints.get(mPathPoints.size() - 1) };
}
PathPoint[] result = new PathPoint[2];
for (int i = 0; i < mPathPoints.size() - 1; i++)
{
length += calculateLengthFromIndex(i);
if (length / mOverallLength >= time)
{
result[0] = mPathPoints.get(i);
result[1] = mPathPoints.get(i + 1);
break;
}
}
return result;
}
private float getStartTimeOfPoint(PathPoint point)
{
float result = 0;
int index = 0;
while (mPathPoints.get(index) != point && index < mPathPoints.size() - 1)
{
result += (calculateLengthFromIndex(index) / mOverallLength);
index++;
}
return result;
}
private void calculateOverallLength()
{
mOverallLength = 0;
mSegmentLengths.clear();
double segmentLength;
for (int i = 0; i < mPathPoints.size() - 1; i++)
{
segmentLength = calculateLengthFromIndex(i);
mSegmentLengths.put(mPathPoints.get(i + 1), segmentLength);
mOverallLength += segmentLength;
}
}
private double calculateLengthFromIndex(int index)
{
PathPoint start = mPathPoints.get(index);
PathPoint end = mPathPoints.get(index + 1);
return calculateLength(start, end);
}
private double calculateLength(PathPoint start, PathPoint end)
{
if (mSegmentLengths.containsKey(end))
{
return mSegmentLengths.get(end);
}
else if (end.mOperation == PathPoint.LINE)
{
return calculateLength(start.mX, end.mX, start.mY, end.mY);
}
else if (end.mOperation == PathPoint.CURVE)
{
return calculateBezeirLength(start, end);
}
else
{
return 0;
}
}
private double calculateLength(float x0, float x1, float y0, float y1)
{
return Math.sqrt(((x0 - x1) * (x0 - x1)) + ((y0 - y1) * (y0 - y1)));
}
private double calculateBezeirLength(PathPoint start, PathPoint end)
{
double result = 0;
float x, y, x0, y0;
float[] xy;
x0 = start.mX;
y0 = start.mY;
for (float progress = BEZIER_LENGTH_ACCURACY; progress <= 1; progress += BEZIER_LENGTH_ACCURACY)
{
xy = getBezierXY(start, end, progress);
x = xy[0];
y = xy[1];
result += calculateLength(x, x0, y, y0);
x0 = x;
y0 = y;
}
return result;
}
private float[] getBezierXY(PathPoint start, PathPoint end, float progress)
{
float[] result = new float[2];
float oneMinusT, x, y;
oneMinusT = 1 - progress;
x = oneMinusT * oneMinusT * oneMinusT * start.mX +
3 * oneMinusT * oneMinusT * progress * end.mControl0X +
3 * oneMinusT * progress * progress * end.mControl1X +
progress * progress * progress * end.mX;
y = oneMinusT * oneMinusT * oneMinusT * start.mY +
3 * oneMinusT * oneMinusT * progress * end.mControl0Y +
3 * oneMinusT * progress * progress * end.mControl1Y +
progress * progress * progress * end.mY;
result[0] = x;
result[1] = y;
return result;
}
}
Here's a sample that shows how to activate the animation:
private void animate()
{
AnimatorPath path = new AnimatorPath();
path.moveTo(0, 0);
path.lineTo(0, 300);
path.curveTo(100, 0, 300, 900, 400, 500);
CurveAnimation animation = new CurveAnimation(path.mPoints);
animation.setDuration(5000);
animation.setInterpolator(new LinearInterpolator());
btn.startAnimation(animation);
}
Now, keep in mind that I'm currently calculating the length of the curve according to an approximation. This will obviously cause some mild inaccuracies in the speed. If you feel it's not accurate enough, feel free to modify the code. Also, if you want to increase the length accuracy of the curve, try decreasing the value of BEZIER_LENGTH_ACCURACY. It must be dividable by 1, so accepted values can be 0.001, 0.000025, etc.
While you might notice some mild fluctuations in speed when using curves, I'm sure it's much better than simply dividing the time equally between all paths.
I hope this helps :)
I tried using Gil's answer, but it didn't fit how I was animating.
Gil wrote an Animation class which is used to animate Views.
I was using ObjectAnimator.ofObject() to animate custom classes using ValueProperties which can't be used with custom Animation.
So this is what I did:
I extend PathEvaluator and override its evaluate method.
I use Gil's logic to calculate path total length, and segmented lengths
Since PathEvaluator.evaluate is called for each PathPoint with t values
0..1, I needed to normalize the interpolated time given to me, so it'll be incremental and won't zero out for each segment.
I ignore the start/end PathPoints given to me so the current position can be
before start or after end along the path depending on the segment's duration.
I pass the current progress calculated to my super
(PathEvaluator) to calc the actual position.
This is the code:
public class NormalizedEvaluator extends PathEvaluator {
private static final float BEZIER_LENGTH_ACCURACY = 0.001f;
private List<PathPoint> mPathPoints;
private float mOverallLength;
private Map<PathPoint, Double> mSegmentLengths = new HashMap<PathPoint, Double>();
public NormalizedEvaluator(List<PathPoint> pathPoints) {
mPathPoints = pathPoints;
if (mPathPoints == null || mPathPoints.size() < 2) {
Log.e("CurveAnimation",
"There must be at least 2 points on the path. There will be an exception soon!");
}
calculateOverallLength();
}
#Override
public PathPoint evaluate(float interpolatedTime, PathPoint ignoredStartPoint,
PathPoint ignoredEndPoint) {
float index = getStartIndexOfPoint(ignoredStartPoint);
float normalizedInterpolatedTime = (interpolatedTime + index) / (mPathPoints.size() - 1);
PathPoint[] startEndPart = getStartEndForTime(normalizedInterpolatedTime);
PathPoint startPoint = startEndPart[0];
PathPoint endPoint = startEndPart[1];
float startTime = getStartTimeOfPoint(startPoint);
float endTime = getStartTimeOfPoint(endPoint);
float progress = (normalizedInterpolatedTime - startTime) / (endTime - startTime);
return super.evaluate(progress, startPoint, endPoint);
}
private PathPoint[] getStartEndForTime(float time) {
double length = 0;
if (time == 1) {
return new PathPoint[] { mPathPoints.get(mPathPoints.size() - 2),
mPathPoints.get(mPathPoints.size() - 1) };
}
PathPoint[] result = new PathPoint[2];
for (int i = 0; i < mPathPoints.size() - 1; i++) {
length += calculateLengthFromIndex(i);
if (length / mOverallLength >= time) {
result[0] = mPathPoints.get(i);
result[1] = mPathPoints.get(i + 1);
break;
}
}
return result;
}
private float getStartIndexOfPoint(PathPoint point) {
for (int ii = 0; ii < mPathPoints.size(); ii++) {
PathPoint current = mPathPoints.get(ii);
if (current == point) {
return ii;
}
}
return -1;
}
private float getStartTimeOfPoint(PathPoint point) {
float result = 0;
int index = 0;
while (mPathPoints.get(index) != point && index < mPathPoints.size() - 1) {
result += (calculateLengthFromIndex(index) / mOverallLength);
index++;
}
return result;
}
private void calculateOverallLength() {
mOverallLength = 0;
mSegmentLengths.clear();
double segmentLength;
for (int i = 0; i < mPathPoints.size() - 1; i++) {
segmentLength = calculateLengthFromIndex(i);
mSegmentLengths.put(mPathPoints.get(i + 1), segmentLength);
mOverallLength += segmentLength;
}
}
private double calculateLengthFromIndex(int index) {
PathPoint start = mPathPoints.get(index);
PathPoint end = mPathPoints.get(index + 1);
return calculateLength(start, end);
}
private double calculateLength(PathPoint start, PathPoint end) {
if (mSegmentLengths.containsKey(end)) {
return mSegmentLengths.get(end);
} else if (end.mOperation == PathPoint.LINE) {
return calculateLength(start.mX, end.mX, start.mY, end.mY);
} else if (end.mOperation == PathPoint.CURVE) {
return calculateBezeirLength(start, end);
} else {
return 0;
}
}
private double calculateLength(float x0, float x1, float y0, float y1) {
return Math.sqrt(((x0 - x1) * (x0 - x1)) + ((y0 - y1) * (y0 - y1)));
}
private double calculateBezeirLength(PathPoint start, PathPoint end) {
double result = 0;
float x, y, x0, y0;
float[] xy;
x0 = start.mX;
y0 = start.mY;
for (float progress = BEZIER_LENGTH_ACCURACY; progress <= 1; progress += BEZIER_LENGTH_ACCURACY) {
xy = getBezierXY(start, end, progress);
x = xy[0];
y = xy[1];
result += calculateLength(x, x0, y, y0);
x0 = x;
y0 = y;
}
return result;
}
private float[] getBezierXY(PathPoint start, PathPoint end, float progress) {
float[] result = new float[2];
float oneMinusT, x, y;
oneMinusT = 1 - progress;
x = oneMinusT * oneMinusT * oneMinusT * start.mX + 3 * oneMinusT * oneMinusT * progress
* end.mControl0X + 3 * oneMinusT * progress * progress * end.mControl1X + progress
* progress * progress * end.mX;
y = oneMinusT * oneMinusT * oneMinusT * start.mY + 3 * oneMinusT * oneMinusT * progress
* end.mControl0Y + 3 * oneMinusT * progress * progress * end.mControl1Y + progress
* progress * progress * end.mY;
result[0] = x;
result[1] = y;
return result;
}
}
This is the usage:
NormalizedEvaluator evaluator = new NormalizedEvaluator((List<PathPoint>) path.getPoints());
ObjectAnimator anim = ObjectAnimator.ofObject(object, "position", evaluator, path.getPoints().toArray());
UPDATE: I just realized that I might have reinvented the wheel, please look at Specifying Keyframes.
It is shocking to see that nothing is available of this kind. Anyways if you don't want to calculate path length at run time then I was able to add functionality of assigning weights to paths. Idea is to assign a weight to your path and run the animation if it feels OK then well and good otherwise just decrease or increase weight assigned to each Path.
Following code is modified code from official Android sample that you pointed in your question:
// Set up the path we're animating along
AnimatorPath path = new AnimatorPath();
path.moveTo(0, 0).setWeight(0);
path.lineTo(0, 300).setWeight(30);// assign arbitrary weight
path.curveTo(100, 0, 300, 900, 400, 500).setWeight(70);// assign arbitrary weight
final PathPoint[] points = path.getPoints().toArray(new PathPoint[] {});
mFirstKeyframe = points[0];
final int numFrames = points.length;
final PathEvaluator pathEvaluator = new PathEvaluator();
final ValueAnimator anim = ValueAnimator.ofInt(0, 1);// dummy values
anim.setDuration(1000);
anim.setInterpolator(new LinearInterpolator());
anim.addUpdateListener(new AnimatorUpdateListener() {
#Override
public void onAnimationUpdate(ValueAnimator animation) {
float fraction = animation.getAnimatedFraction();
// Special-case optimization for the common case of only two
// keyframes
if (numFrames == 2) {
PathPoint nextPoint = pathEvaluator.evaluate(fraction,
points[0], points[1]);
setButtonLoc(nextPoint);
} else {
PathPoint prevKeyframe = mFirstKeyframe;
for (int i = 1; i < numFrames; ++i) {
PathPoint nextKeyframe = points[i];
if (fraction < nextKeyframe.getFraction()) {
final float prevFraction = prevKeyframe
.getFraction();
float intervalFraction = (fraction - prevFraction)
/ (nextKeyframe.getFraction() - prevFraction);
PathPoint nextPoint = pathEvaluator.evaluate(
intervalFraction, prevKeyframe,
nextKeyframe);
setButtonLoc(nextPoint);
break;
}
prevKeyframe = nextKeyframe;
}
}
}
});
And that's it !!!.
Of course I modified other classes as well but nothing big was added. E.g. in PathPoint I added this:
float mWeight;
float mFraction;
public void setWeight(float weight) {
mWeight = weight;
}
public float getWeight() {
return mWeight;
}
public void setFraction(float fraction) {
mFraction = fraction;
}
public float getFraction() {
return mFraction;
}
In AnimatorPath I modified getPoints() method like this:
public Collection<PathPoint> getPoints() {
// calculate fractions
float totalWeight = 0.0F;
for (PathPoint p : mPoints) {
totalWeight += p.getWeight();
}
float lastWeight = 0F;
for (PathPoint p : mPoints) {
p.setFraction(lastWeight = lastWeight + p.getWeight() / totalWeight);
}
return mPoints;
}
And thats pretty much it. Oh and for better readability I added Builder Pattern in AnimatorPath, so all 3 methods were changed like this:
public PathPoint moveTo(float x, float y) {// same for lineTo and curveTo method
PathPoint p = PathPoint.moveTo(x, y);
mPoints.add(p);
return p;
}
NOTE: To handle Interpolators that can give fraction less then 0 or greater than 1 (e.g. AnticipateOvershootInterpolator) look at com.nineoldandroids.animation.KeyframeSet.getValue(float fraction) method and implement the logic in onAnimationUpdate(ValueAnimator animation).
I have a sprite in Android OpenGL. This sprite (a small beetlebug) is always moving in a forward direction and I use:
sprite.setPosition(posX, posY);
Now I have a rotation method, when the user gestures left or right the bug rotates:
private void applyRotation() {
for(int i=0;i<beetleBug.size;i++) {
Sprite s = beetleBug.get(i);
s.setOrigin(s.getWidth() / 2, s.getHeight() / 2);
s.setRotation(angle);
}
}
Now when the bug is moving forward which he always does the new x and y coordinates have to be calculated which depend on the rotation-angle, so that the bug is always moving forward. Does anybody have an algorithm to calculate the direction by the rotation-angle?
Here is the whole Bug-class:
public class Bug {
private SpriteBatch spriteBatch = null;
private TextureAtlas spriteSheet;
private Array<Sprite> beetleBug;
private int currentFrame = 0;
private final float frameLength = 0.10f; //in seconds, how long a frame last
private float animationElapsed = 0.0f;
private float angle = 0.0f;
private float posX = 0.0f;
private float posY = 0.0f;
private float sizeX = 100.0f;
private float sizeY = 100.0f;
private float offSet = 50.0f;
public Bug() {
spriteBatch = new SpriteBatch();
spriteSheet = new TextureAtlas("assets/data/bug.txt");
beetleBug = spriteSheet.createSprites("bug");
// dont forget to set the size of your sprites!
for(int i=0; i<beetleBug.size; i++){
beetleBug.get(i).setSize(sizeX, sizeY);
}
applyPosition();
}
public void handleInput() {
boolean leftKey = Gdx.input.isKeyPressed(Input.Keys.LEFT);
boolean rightKey = Gdx.input.isKeyPressed(Input.Keys.RIGHT);
if(rightKey) {
if(angle <= 0) {
angle = 360;
}
angle -= 2f;
applyRotation();
}
if(leftKey) {
if(angle >= 360) {
angle = 0;
}
angle += 2f;
applyRotation();
}
applyPosition();
}
private void applyPosition() {
float x = (float) Math.cos(angle);
float y = (float) Math.sin(angle);
posX = posX + x;
posY = posY + y;
for(int i=0; i<beetleBug.size; i++){
beetleBug.get(i).setPosition(posX - offSet, posY -offSet); // optional: center the sprite to screen
}
}
private void applyRotation() {
for(int i=0;i<beetleBug.size;i++) {
Sprite s = beetleBug.get(i);
s.setOrigin(s.getWidth() / 2, s.getHeight() / 2);
s.setRotation(angle);
}
}
public void render(OrthographicCamera cam) {
float dt = Gdx.graphics.getDeltaTime();
animationElapsed += dt;
while(animationElapsed > frameLength){
animationElapsed -= frameLength;
currentFrame = (currentFrame == beetleBug.size - 1) ? 0 : ++currentFrame;
}
spriteBatch.setProjectionMatrix(cam.combined);
spriteBatch.begin();
beetleBug.get(currentFrame).draw(spriteBatch);
spriteBatch.end();
}
}
Works perfectly now:
Converted degrees to radians
Set x-coordintae to -
private void applyPosition() {
float radians = (float) Math.toRadians(angle);
float x = -(float) Math.sin(radians);
float y = (float) Math.cos(radians);
posX = posX + x;
posY = posY + y;
for(int i=0; i<beetleBug.size; i++){
beetleBug.get(i).setPosition(posX - offSet, posY -offSet);
}
}
Create a normalized vector to represent the beetle's direction, then multiply by the speed. Add that vector to the beetle's current position and you've got his new position.
Create the normalized vector (i.e. has a length of 1) using your angle. vx = cos(angle), vy = sin(angle)
Multiply by your beetle's speed. vx = vx*speed, vy = vy*speed
Add it to the current position. x = x + vx, y = y + vy
Repeat
Some gotchas: Watch out that your sprite's graphical rotation and your own internal representation of rotation go the same way. Some frameworks flip which way they rotate graphics. The above [cos(angle), sin(angle)] is for an angle of zero pointing towards the positive x axis. Many implementations of cos/sin/tan use radians instead of degrees for their calculations, so convert as appropriate.
[cos angle, sin angle]is for zero to the right (positive x), counterclockwise. [-sin angle, cos angle]is for zero pointing up (positive y), counterclockwise.
This might work:
int currentX = 100; //beetleCurrentX
int currentY = 100; //beetleCurrentY
int angle = 200; //beetleAngle
int len = 2; //Step that the beetle makes (jumps 2 in this case)
int x2Pos = sin(angle)*len + currentX;
int y2Pos = cos(angle)*len + currentY;
sprite.setPosition(x2Pos,y2Pos);
If you execute this each frame you will have your beetle moving in the angles direction.
I am currently trying to create a meter that can be adjusted in the percentage of fill. The problem I have is I'm not good at math at all. I want to start drawing an arc in the 'north' (first image), as opposed to a normal arc having its 0 deg point in the 'east' (as shown in second image).
I want to be able to increase the blue area in image 1 in size (angle) by dragging/touching it along the screen. Now these are things I am able to do in some kind of fashion now. The real problem I am facing is this:
I use the following code to draw the blue area:
mStart = -90;
int degree = (int)((theta + Math.PI) * 180 / Math.PI);
mSweep = degree;
RectF mOvals = new RectF(c.x - outerRadius + circleThickness, c.y - outerRadius + circleThickness, c.x + outerRadius - circleThickness, c.y + outerRadius - circleThickness );
mArcSetLevel = new Path();
if(mArcSetLevel != null ) {
canvas.drawArc(mOvals, mStart, mSweep, true, arcPaint);
}
Setting the start at -90 makes it start 90 deg earlier. To track the angle of the touch I use this formula, but this is where it goes wrong:
int py = (int)event.getY() - c.y;
int px = (int)event.getX() - c.x;
theta = (float) ((float) Math.atan2(py, px) - (Math.PI / 2)); // - Math.PI / 2 to correct -90 start
When I go further than exactly 270 degrees the blue area gets reset and draws itself from north to west in a much smaller angle (because of the 'false' start of -90, shown in third image). My math skills are simply not good enough for me to be able to solve this, although I can think of why it is happening I cannot seem to find the solution.
The (very messy) code to the entire view I made is as follows:
private Canvas canvas;
//Canvas width and height
private int h = -1;
private int w = -1;
//circle properties
private Paint paint;
private Paint arcPaint;
private Path circle;
private Point c;
private int outerRadius;
private int circleThickness = 20;
//point click in wheel
private float theta = 0;
private float mStart;
private float mSweep;
private Paint mBgPaints = new Paint();
private Path mArcSetLevel;
int padding = 10;
OnMeterWheelChangeListener onMeterWheelChangeListener = null;
public MeterWheel(Context context){
super(context);
initCircleSeekBar();
}
public MeterWheel(Context context, AttributeSet attrs) {
super(context, attrs);
initCircleSeekBar();
}
private void initCircleSeekBar() {
canvas = new Canvas();
circle = new Path();
paint = new Paint();
arcPaint = new Paint();
c = new Point();
mBgPaints.setAntiAlias(true);
mBgPaints.setStyle(Paint.Style.FILL);
mBgPaints.setColor(0x88FF0000);
mBgPaints.setStrokeWidth(0.5f);
mArcSetLevel = new Path();
this.draw(canvas);
}
#Override
protected void onSizeChanged(int width, int height, int oldw, int oldh) {
// TODO Auto-generated method stub
super.onSizeChanged(width, height, oldw, oldh);
w = width;
h = height;
Log.i("POWERWHEEL", String.valueOf(w) + " " + String.valueOf(h));
c.set(w/2, h/2);
drawCircle();
}
private void drawCircle() {
outerRadius = Math.min(h,w)/2;
circleThickness = (int) (outerRadius*0.15);
circle.addArc(new RectF(c.x - outerRadius + circleThickness/2, c.y - outerRadius + circleThickness/2, c.x + outerRadius - circleThickness/2, c.y + outerRadius - circleThickness/2 ), 0, 360);
circle.moveTo(c.x, c.y);
//paint.setShader(new SweepGradient(w/2,h/2, colourarry, null));
paint.setColor(Color.GRAY);
paint.setStyle(Style.STROKE);
paint.setStrokeWidth(circleThickness);
paint.setAntiAlias(true);
arcPaint.setColor(Color.BLUE);
arcPaint.setStyle(Style.FILL);
arcPaint.setStrokeWidth(circleThickness);
arcPaint.setAntiAlias(true);
}
#SuppressLint("DrawAllocation")
#Override
protected void onDraw(Canvas canvas) {
// TODO Auto-generated method stub
super.onDraw(canvas);
if(circle != null){
//draw circle
canvas.drawPath(circle, paint);
mStart = -90;
int degree = (int)((theta + Math.PI) * 180 / Math.PI);
Log.d("POWERWHEEL", "" + degree);
mSweep = degree;
RectF mOvals = new RectF(c.x - outerRadius + circleThickness, c.y - outerRadius + circleThickness, c.x + outerRadius - circleThickness, c.y + outerRadius - circleThickness );
mArcSetLevel = new Path();
if(mArcSetLevel != null ) {
canvas.drawArc(mOvals, mStart, mSweep, true, arcPaint);
}
}
}
#Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) {
return false;
}
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
setPressed(true);
onStartTrackingTouch(event);
trackTouchEvent(event);
break;
case MotionEvent.ACTION_MOVE:
trackTouchEvent(event);
break;
case MotionEvent.ACTION_UP:
trackTouchEvent(event);
onStopTrackingTouch();
setPressed(false);
invalidate();
break;
case MotionEvent.ACTION_CANCEL:
onStopTrackingTouch();
setPressed(false);
invalidate();
break;
}
return true;
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width,height);
}
private void onStartTrackingTouch(MotionEvent event) {
}
private void onStopTrackingTouch() {
}
private void trackTouchEvent(MotionEvent event) {
int py = (int)event.getY() - c.y;
int px = (int)event.getX() - c.x;
theta = (float) ((float) Math.atan2(py, px) - (Math.PI / 2));
Log.d("POWERWHEEL", "theta: " + theta);
this.invalidate();
}
public void setSize(int x, int y){
h = y;
w = x;
}
public void setCirleThickness(int t){
circleThickness = t;
}
public void setOnMeterWheelChangeListener (OnMeterWheelChangeListener listener) {
onMeterWheelChangeListener = listener;
}
public interface OnMeterWheelChangeListener{
public void onStartTrackingTouch (MeterWheel colourWheel);
public void onStopTrackingTouch (MeterWheel colourWheel);
}
Thanks a million in advance!
When calculating theta, you use atan2 which returns the angle in +/- pi. So when being in the upper left quadrant it will return a value in the range -pi/2 to -pi (asuming y is positive downwards and x is positve rightwards). You substract pi/2 directly with gives a range of -pi to -3pi/2. In onDraw you then add pi again (confusing) giving a range of 0 to -pi/2 of the sweep for this quadrant. This means it will paint the arc 0 to pi/2 (or 0 to 90 degrees) counterclockwise from your starting position at the top. You must make sure your sweep always keeps in the range 0 to pi. Nicest solution is to shift the coordinates by -pi/2, so that instead of Math.atan2(py, px), you do Math.atan2(px, -py) and then if theta is negative you add 2*pi. Something like (I don't write android)
theta = (float) Math.atan2(px, -py);
if (theta < 0) theta += 2 * Math.PI;
and then in onDraw
int degree = (int)(theta * 180 / Math.PI);
Log.d("POWERWHEEL", "" + degree);
mSweep = degree;
If you are still experiencing problems check that mSweep is always in the range 0 to 360 degrees.
i was draw a pie chart using canvas in android and using the below code i draw a text on each slice of that pie chart (draw arc on path), now i want to draw the text length wise i.e. from center to end of the each slice,so how to rotate the arc using start and sweep angle.
p.addArc(mEventsRect, fStartAngle, fSweepAngle);
mBgPaints.setColor(iTextColor);
canvas.drawTextOnPath(sTextValue, p, fHOffSet, fVOffSet, mBgPaints);
You can try this snippet: (from: http://www.helloandroid.com/tutorials/how-use-canvas-your-android-apps-part-2)
int x = 75;
int y = 185;
paint.setColor(Color.GRAY);
paint.setTextSize(25);
String rotatedtext = "Rotated helloandroid :)";
//Draw bounding rect before rotating text:
Rect rect = new Rect();
paint.getTextBounds(rotatedtext, 0, rotatedtext.length(), rect);
canvas.translate(x, y);
paint.setStyle(Paint.Style.FILL);
canvas.drawText(rotatedtext , 0, 0, paint);
paint.setStyle(Paint.Style.STROKE);
canvas.drawRect(rect, paint);
canvas.translate(-x, -y);
paint.setColor(Color.RED);
canvas.rotate(-45, x + rect.exactCenterX(),y + rect.exactCenterY());
paint.setStyle(Paint.Style.FILL);
canvas.drawText(rotatedtext, x, y, paint);
A bit late to the party but I had to figure this one out and it's a bit simpler than what I found around. You'll already have the x and y for your text, use these to rotate the canvas
canvas.rotate(yourDegrees, x, y)
canvas.drawText(yourText, x, y, yourPaint)
canvas.rotate(-yourDegrees, x, y)
The negative sign negates the first rotation. You could swap it around to rotate in the opposite direction.
You could do this in a loop but the rotation cycle must be done each time either coordinate changes.
may be this will help you,,
here 39.5 is radius,, this will perfectly show result on mdpi screen
protected void onDraw(){
canvas.save();
PointF pf = PointOnCircle(35f, 45f, new PointF(39.5f, 39.5f));
canvas.rotate(-45, pf.x, pf.y);
canvas.drawText("67%", pf.x, pf.y, red);//23.5
canvas.restore();
canvas.save();
PointF pfa = PointOnCircle(35f, 135f, new PointF(39.5f, 39.5f));
canvas.rotate(45, pfa.x, pfa.y);
canvas.drawText("33%", pfa.x, pfa.y, red);//23.5
canvas.restore();
canvas.save();
pfa = PointOnCircle(27.5f, 225f, new PointF(39.5f, 39.5f));
canvas.rotate(-45, pfa.x, pfa.y);
canvas.drawText("45%", pfa.x, pfa.y, red);//23.5
canvas.restore();
canvas.save();
pfa = PointOnCircle(27.5f, 315f, new PointF(39.5f, 39.5f));
canvas.rotate(45, pfa.x, pfa.y);
canvas.drawText("55%", pfa.x, pfa.y, red);//23.5
canvas.restore();}
protected static final PointF PointOnCircle(float radius, float angleInDegrees, PointF origin) {
// Convert from degrees to radians via multiplication by PI/180
float x = (float) (radius * Math.cos(angleInDegrees * Math.PI / 180F)) + origin.x;
float y = (float) (radius * Math.sin(angleInDegrees * Math.PI / 180F)) + origin.y;
return new PointF(x, y);
}
Here's how i finally did it after two days of search with help of this library https://github.com/Ken-Yang/AndroidPieChart
And equations to center text done with help of my friends and alot of search
on MainActivity onCreate or oncreateView if you are using fragments:
PieChart pie = (PieChart) rootView.findViewById(R.id.pieChart);
ArrayList<Float> alPercentage = new ArrayList<Float>();
alPercentage.add(2.0f);
alPercentage.add(8.0f);
alPercentage.add(20.0f);
alPercentage.add(10.0f);
alPercentage.add(10.0f);
alPercentage.add(10.0f);
alPercentage.add(10.0f);
alPercentage.add(10.0f);
alPercentage.add(10.85f);
alPercentage.add(9.15f);
try {
// setting data
pie.setAdapter(alPercentage);
// setting a listener
pie.setOnSelectedListener(new OnSelectedLisenter() {
#Override
public void onSelected(int iSelectedIndex) {
Toast.makeText(getActivity(),
"Select index:" + iSelectedIndex,
Toast.LENGTH_SHORT).show();
}
});
} catch (Exception e) {
if (e.getMessage().equals(PieChart.ERROR_NOT_EQUAL_TO_100)) {
Log.e("kenyang", "percentage is not equal to 100");
}
}
public class PieChart extends View {
public interface OnSelectedLisenter {
public abstract void onSelected(int iSelectedIndex);
}
private OnSelectedLisenter onSelectedListener = null;
private static final String TAG = PieChart.class.getName();
public static final String ERROR_NOT_EQUAL_TO_100 = "NOT_EQUAL_TO_100";
private static final int DEGREE_360 = 360;
private static String[] PIE_COLORS = null;
private static int iColorListSize = 0;
ArrayList<Float> array;
private Paint paintPieFill;
private Paint paintPieBorder;
private Paint paintCenterCircle;
private ArrayList<Float> alPercentage = new ArrayList<Float>();
private int mCenterX = 320;
private int mCenterY = 320;
private int iDisplayWidth, iDisplayHeight;
private int iSelectedIndex = -1;
private int iCenterWidth = 0;
private int iShift = 0;
private int iMargin = 0; // margin to left and right, used for get Radius
private int iDataSize = 0;
private Canvas canvas1;
private RectF r = null;
private RectF centerCircle = null;
private float fDensity = 0.0f;
private float fStartAngle = 0.0f;
private float fEndAngle = 0.0f;
float fX;
float fY;
public PieChart(Context context, AttributeSet attrs) {
super(context, attrs);
PIE_COLORS = getResources().getStringArray(R.array.colors);
iColorListSize = PIE_COLORS.length;
array = new ArrayList<Float>();
fnGetDisplayMetrics(context);
iShift = (int) fnGetRealPxFromDp(30);
iMargin = (int) fnGetRealPxFromDp(40);
centerCircle = new RectF(200, 200, 440, 440);
// used for paint circle
paintPieFill = new Paint(Paint.ANTI_ALIAS_FLAG);
paintPieFill.setStyle(Paint.Style.FILL);
// used for paint centerCircle
paintCenterCircle = new Paint(Paint.ANTI_ALIAS_FLAG);
paintCenterCircle.setStyle(Paint.Style.FILL);
paintCenterCircle.setColor(Color.WHITE);
// used for paint border
paintPieBorder = new Paint(Paint.ANTI_ALIAS_FLAG);
paintPieBorder.setStyle(Paint.Style.STROKE);
paintPieBorder.setStrokeWidth(fnGetRealPxFromDp(3));
paintPieBorder.setColor(Color.WHITE);
Log.i(TAG, "PieChart init");
}
// set listener
public void setOnSelectedListener(OnSelectedLisenter listener) {
this.onSelectedListener = listener;
}
float temp = 0;
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
Log.i(TAG, "onDraw");
float centerX = (r.left + r.right) / 2;
float centerY = (r.top + r.bottom) / 2;
float radius1 = (r.right - r.left) / 2;
radius1 *= 0.5;
float startX = mCenterX;
float startY = mCenterY;
float radius = mCenterX;
float medianAngle = 0;
Path path = new Path();
for (int i = 0; i < iDataSize; i++) {
// check whether the data size larger than color list size
if (i >= iColorListSize) {
paintPieFill.setColor(Color.parseColor(PIE_COLORS[i
% iColorListSize]));
} else {
paintPieFill.setColor(Color.parseColor(PIE_COLORS[i]));
}
fEndAngle = alPercentage.get(i);
// convert percentage to angle
fEndAngle = fEndAngle / 100 * DEGREE_360;
// if the part of pie was selected then change the coordinate
if (iSelectedIndex == i) {
canvas.save(Canvas.MATRIX_SAVE_FLAG);
float fAngle = fStartAngle + fEndAngle / 2;
double dxRadius = Math.toRadians((fAngle + DEGREE_360)
% DEGREE_360);
fY = (float) Math.sin(dxRadius);
fX = (float) Math.cos(dxRadius);
canvas.translate(fX * iShift, fY * iShift);
}
canvas.drawArc(r, fStartAngle, fEndAngle, true, paintPieFill);
float angle = (float) ((fStartAngle + fEndAngle / 2) * Math.PI / 180);
float stopX = (float) (startX + (radius/2) * Math.cos(angle));
float stopY = (float) (startY + (radius/2) * Math.sin(angle));
// if the part of pie was selected then draw a border
if (iSelectedIndex == i) {
canvas.drawArc(r, fStartAngle, fEndAngle, true, paintPieBorder);
canvas.drawLine(startX, startY, stopX, stopY, paintPieFill);
canvas.restore();
}
fStartAngle = fStartAngle + fEndAngle;
}
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// get screen size
iDisplayWidth = MeasureSpec.getSize(widthMeasureSpec);
iDisplayHeight = MeasureSpec.getSize(heightMeasureSpec);
if (iDisplayWidth > iDisplayHeight) {
iDisplayWidth = iDisplayHeight;
}
/*
* determine the rectangle size
*/
iCenterWidth = iDisplayWidth / 2;
int iR = iCenterWidth - iMargin;
if (r == null) {
r = new RectF(iCenterWidth - iR, // top
iCenterWidth - iR, // left
iCenterWidth + iR, // right
iCenterWidth + iR); // bottom
}
if (centerCircle == null) {
// centerCircle=new RectF(left, top, right, bottom);
}
setMeasuredDimension(iDisplayWidth, iDisplayWidth);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
// get degree of the touch point
double dx = Math.atan2(event.getY() - iCenterWidth, event.getX()
- iCenterWidth);
float fDegree = (float) (dx / (2 * Math.PI) * DEGREE_360);
fDegree = (fDegree + DEGREE_360) % DEGREE_360;
// get the percent of the selected degree
float fSelectedPercent = fDegree * 100 / DEGREE_360;
// check which pie was selected
float fTotalPercent = 0;
for (int i = 0; i < iDataSize; i++) {
fTotalPercent += alPercentage.get(i);
if (fTotalPercent > fSelectedPercent) {
iSelectedIndex = i;
break;
}
}
if (onSelectedListener != null) {
onSelectedListener.onSelected(iSelectedIndex);
}
invalidate();
return super.onTouchEvent(event);
}
private void fnGetDisplayMetrics(Context cxt) {
final DisplayMetrics dm = cxt.getResources().getDisplayMetrics();
fDensity = dm.density;
}
private float fnGetRealPxFromDp(float fDp) {
return (fDensity != 1.0f) ? fDensity * fDp : fDp;
}
public void setAdapter(ArrayList<Float> alPercentage) throws Exception {
this.alPercentage = alPercentage;
iDataSize = alPercentage.size();
float fSum = 0;
for (int i = 0; i < iDataSize; i++) {
fSum += alPercentage.get(i);
}
if (fSum != 100) {
Log.e(TAG, ERROR_NOT_EQUAL_TO_100);
iDataSize = 0;
throw new Exception(ERROR_NOT_EQUAL_TO_100);
}
}
in your Layout:
<com.example.piecharts.PieChart
android:id="#+id/pieChart"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</com.example.piecharts.PieChart>
This question is pretty old, but I figured I would write a general answer.Here I assume you want to draw your pie chart in the middle of the canvas and that you have your start and seep angles in an array.
x = canvas.getWidth/2 //Horizontal center of canvas view
y = canvas.getHeight/2 //Vertical center of canvas view
canvas.rotate(fStartAngle[i]+ fSweepAngle[i]/2, x ,y ); //Rotates canvas to a line in the middle
//of start and end of arc
canvas.translate(50f,0);//Moves the text a little out of the center of the circle (50f is arbitrary)
paintText.setStyle(Paint.Style.FILL);
canvas.drawText(rotatedtext, x, y, paintText);
//Undo the translations and rotations so that next arc can be drawn normally
canvas.translate(-50f,0);
canvas.rotate(-(temp+ value_degree[i]/2), x ,y );
it's 2023 there might be other answers out there but here is one that is sure to work
//the path where your text/paint will be drawn across
Path path = new Path();
path.addArc(mEventsRect, fStartAngle, fSweepAngle);//add this if you want your path to be drawn across the arc of your sector
//if you are using a text get the width
float textWidth = mTextPaint.measureText("text");
//this is the y co-ordinate your text will start from
int hOffset = 100;
//this is the x co-ordinate your text will start from
int vOffset = 100;
//we will be using the matrix to rotate the bunds of our current path
Matrix matrix = new Matrix();
//we will use this to get the bounds of our current path
RectF bounds = new RectF();
path.computeBounds(bounds,true);
//we are using the matrix to rotate the bound (with is the bound of the path) by 90 degrees
matrix.setRotate(90,bounds.centerX(),bounds.centerY());
the we transform the points in the path using the matrix
path.transform(matrix);
//you can now draw the text on the path
canvas.drawTextOnPath("text", path, hOffset, vOffset , mBgPaints);