Related
I have the following code
float angle = (-1) * 360 * progress / max;
canvas.drawArc(rectF, startAngle, angle, false, foregroundPaint);
double dotX = Math.cos(startAngle+angle) * rectF.centerX() + rectF.width()/2.0;
double dotY = Math.sin(startAngle+angle) * rectF.centerY() + rectF.height()/2.0;
canvas.drawCircle((float)dotX, (float)dotY,strokeWidth*1.5f, dotPaint);
This is intended to draw an arc and a red circle at the end of the orc. however my circle is not on the arc depending on the "progress" value, ie the angle.
For example on 40 it is on arc, on 47 it is outside ..etc
What is it I am doing wrong here?
if I understanding your question true, just use a library for creating arc layout and then at the end of it add your circle.
arc layout:
https://android-arsenal.com/details/1/4823
I currently have this snippet generating the ticks around the outside of and android wear watchface
float innerMainTickRadius = mCenterX - 35;
for(int tickIndex = 0; tickIndex < 12; tickIndex++) {
float tickRot = (float) (tickIndex * Math.PI * 2 / 12);
float innerX = (float) Math.sin(tickRot) * innerMainTickRadius;
float innerY = (float) -Math.cos(tickRot) * innerMainTickRadius;
float outerX = (float) Math.sin(tickRot) * mCenterX;
float outerY = (float) -Math.cos(tickRot) * mCenterX;
canvas.drawLine(mCenterX + innerX, mCenterY + innerY, mCenterX + outerX, mCenterY + outerY, mTickPaint);
}
Which generates the ticks well on a round watchface but on a square it turns out like this:
but I'd like them to not be circular, but instead fit the shape a bit more suitably, e.g:
Is there a standard way to do this? I'm guessing I can't use trig again...
Of course you use geometry and trig. For example any line you put on the clock face you want to point to the center so one part will be the given (x,y) and the other will be arctan2(cy-y,cx-x) giving you the angle from the point you have towards the center (cx,cy) then simply draw the line in the direction of the center of a given length r, by drawing the line from x,y to cos(angle) * r, sin(angle) * r.
However, given your sample image you might want to draw the line from x,y to x+r,y then rotate the canvas by angle so that you can draw those numbers tweaked like that. Be sure to do canvas.save() before tweaking the canvas' matrix and canvas.restore() after the tweak.
This leaves the math of whatever shape you want to draw your ticks from and the positions thereto. You can do this within a Path. So define the path for a rounded rectangle and then use the PathMeasure class to get the getPosTan() and then ignore the tangent and just use the position it gives you to find your position around a rounded rectangle. That or simply calculate those positions as the positions through either a line segment or a bezier section depending on the decided shape.
For example:
static final int TICKS = 12;
static final float TICKLENGTH = 20;
In the draw routine,
float left = cx - 50;
float top = cy - 50;
float right = cx + 50;
float bottom = cy + 50;
float ry = 20;
float rx = 20;
float width = right-left;
float height = bottom-top;
Path path = new Path();
path.moveTo(right, top + ry);
path.rQuadTo(0, -ry, -rx, -ry);
path.rLineTo(-(width - (2 * rx)), 0);
path.rQuadTo(-rx, 0, -rx, ry);
path.rLineTo(0, (height - (2 * ry)));
path.rQuadTo(0, ry, rx, ry);
path.rLineTo((width - (2 * rx)), 0);
path.rQuadTo(rx, 0, rx, -ry);
path.rLineTo(0, -(height - (2 * ry)));
path.close();
PathMeasure pathMeasure = new PathMeasure();
pathMeasure.setPath(path,true);
float length = pathMeasure.getLength();
float[] pos = new float[2];
float r = TICKLENGTH;
for (int i = 0; i < TICKS; i++) {
pathMeasure.getPosTan(i * (length/TICKS),pos,null);
double angle = Math.atan2(cy - pos[1], cx - pos[0]); //yes, y then x.
double cos = Math.cos(angle);
double sin = Math.sin(angle);
canvas.drawLine(pos[0], pos[1], (float)(pos[0] + cos * r), (float)(pos[1] + sin * r), paint);
}
Admittedly it looks like:
So it would take a lot more work to get it looking like your image. But, it's totally doable. The path measure trick thing will work for any shape. I avoided using path.addRoundRect because of the Lollipop+ restriction. You can see my answer to that question here. And the other answers which are plenty fine to how to draw a rounded rectangle-esque shape. You can, if you would like to write an envelope function simply scale your current picture to the envelope of the rectangle according to the factor t, as it goes around the clock.
The angle is a function of the position now. I'm not immediately seeing the trick for getting a closed form in this case. But in the most general case, you could end up just storing the position of each tickmark, then you're just drawing the line that goes through that point and the center. so the angle at second i is just
theta(i)=arctan(y_pos(i) / x_pos(i))
assuming the center has coordinates (0,0). In this case, you only need to store the positions for 8 consecutive ticks because the face is periodic every 90 degrees and symmetric about the diagonals as well.
I now draw a dot on the canvas, which may be zoomed in or out.
As far as I know, the drawing function, canvas.drawCircle() takes in the coordinates in canvas coordinate system. Furthermore, the co-ordinates remain unchanged when the canvas is zoomed.
E.g. previously you draw a dot at (50, 50) in the canvas coordinate system, and then you zoom in the canvas, the dot's coordinates in the canvas still remain (50, 50). But obviously, the dot has been moved w.r.t. the screen.
When the canvas is zoomed, the dot should be kept at the same position on the screen. *i.e. After the dot moves w.r.t. to the screen, I want to move it back to its original position w.r.t. the screen.*
My onDraw() function is as follows:
#Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvasWidth = canvas.getWidth();
canvasHeight = canvas.getHeight();
canvas.save();
canvas.translate(mPosX, mPosY);
canvas.scale(mScaleFactor, mScaleFactor);
mImage.draw(canvas); // draw the map as the background
Paint PointStyle = new Paint();
PointStyle.setColor(Color.BLUE);
PointStyle.setStyle(Paint.Style.FILL_AND_STROKE);
PointStyle.setStrokeWidth(2);
canvas.drawCircle(Constant.INITIAL_X, Constant.INITIAL_Y, 3, PointStyle);
canvas.restore();
}
I tried the following method to move the dot back on screen after the scaling.
private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {
#Override
public boolean onScale(ScaleGestureDetector detector) {
mScaleFactor *= detector.getScaleFactor(); // accumulate the scale factors
// Don't let the object get too small or too large.
mScaleFactor = Math.max(1f, Math.min(mScaleFactor, 10.0f)); // 1 ~ 10
float pinToCornerOnScreenXDistance = 0;
float pinToCornerOnScreenYDistance = 0;
float canvasToScreenDiffRatioX = 0;
float canvasToScreenDiffRatioY = 0;
float pinOnCanvasX = 0;
float pinOnCanvasY = 0;
// pin's location on the screen --> S:(336, 578)
pinToCornerOnScreenXDistance = 336 - canvasLeftTopCornerOnScreenX;
pinToCornerOnScreenYDistance = 578 - canvasLeftTopCornerOnScreenY;
Log.d("Screen Diff", "X: " + pinToCornerOnScreenXDistance + " Y: " + pinToCornerOnScreenYDistance);
canvasToScreenDiffRatioX = canvasWidth * mScaleFactor / 720; // screen of HTC One X --> 720*1280
canvasToScreenDiffRatioY = canvasHeight * mScaleFactor / 1280;
Log.d("Ratio", canvasToScreenDiffRatioX + " " + canvasToScreenDiffRatioY);
pinOnCanvasX = 0 + pinToCornerOnScreenXDistance * canvasToScreenDiffRatioX; // canvas left top corner is the origin (0, 0)
pinOnCanvasY = 0 + pinToCornerOnScreenYDistance * canvasToScreenDiffRatioY;
Log.d("Pin on Canvas", "X: " + pinOnCanvasX + " Y: " + pinOnCanvasY);
Constant.setInitialX(pinOnCanvasX);
Constant.setInitialY(pinOnCanvasY);
historyXSeries.set(0, Constant.INITIAL_X);
historyYSeries.set(0, Constant.INITIAL_Y);
invalidate();
return true;
}
}
The idea is based on the fact that I notice when I zoom in or out the canvas, its left top corner never moves. That is, the canvas' let top corner is the zooming center.
Then I successfully keep updating the zooming center's co-ordinates when the canvas is moved. So in this way, no matter I move the canvas or zoom it, I always have the zooming center's coordinates in my canvasLeftTopCornerOnScreenX and canvasLeftTopCornerOnScreenY.
Then I try to utilize the distance between the never-moved-during-scaling zooming center and the desired position where I hope to place my dot, (336, 578) here. I calculate it as pinToCornerOnScreenDistance.
As its name suggests, it is the on-screen distance. I have to scale it into canvas distance so that I can draw the dot, since the drawing function is based on the canvas coordinate system instead of the screen coordinate system. Currently, the code is device-specific, i.e. it is only for HTC One X, which has a 1280*720 screen, for now. So I do the scaling as follows:
canvasToScreenDiffRatioX = canvasWidth * mScaleFactor / 720;
canvasToScreenDiffRatioY = canvasHeight * mScaleFactor / 1280;
Then, finally I calculate the new on-canvas coordinates of on-screen point (336, 578) and then draw the dot there.
But the result is not correct. When I zoom the canvas, the dot I draw fails to remain at (336, 578) on screen.
Can anybody tell me where goes wrong?
Or propose another way of doing this?
Any comment or Answer will be greatly appreciated!
Is your app using full-screen? Otherwise, it is not 1280 for the height. But I don't think that is your problem.
Based on my understanding, I will change the code to
pinOnCanvasX = 336 / mScaleFactor;
pinOnCanvasY = 578 / mScaleFactor;
which I assume when mScaleFactor == 1, the pin that on Canvas are the same on screen
Try this one,
I think this can be useful for your issue,
pinOnCanvasX = (336 * mScaleFactor) + trans_x;
pinOnCanvasY = (578 * mScaleFactor) + trans_y;
here, trans_x and trans_y is translation of canvas on x and y axis. here in your case should be mPosX, mPosY but not sure what these points means for.
The way I would like to share you is scale and translate your starting
point of the circle as much as canvas does. That means if your canvas
scale and translate with some value then apply the same for
your starting point too.
hope this will solve your problem.
I am using Canvas.drawCircle to draw a circle in Android laout.
Method gets 3 parameters - first two are position - x and y.
Is it possible to skip hardcoded position of the circle and draw it centered ?
Following code can be used to get the width and height of the screen.
int width = this.getWidth();
int height = this.getHeight();
To draw circle in the middle of screen you can call :
Canvas.drawCircle(width/2, height/2)
You can paint a circle centered to the screen like this:
Display disp = ((WindowManager)this.getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay();
canvas.drawCircle(disp.getWidth()/2, disp.getHeight()/2, radius, paint);
Assuming you are extending the View class:
int CentreX = (this.getWidth() / 2);
int CentreY = (this.getHeight() / 2);
I'm trying to draw a rectangle with rounded corners. I have a javascript path that does this, but the javascript arcTo method takes a rectangle (to define its oval) and then one param which sets the sweep.
However, in the Android version there are three params. the rectangle oval (which I think I have defined correctly) and then the startAngle and sweepAngle (which I'm not understanding the usage of), but my arcs don't look anything like what I'm expecting when I noodle with how I'm guessing they should work.
Does anyone know of a good tutorial on this?
Specifically I'm trying to understand what would the two params look like if I was trying to draw an arc (on a clock face) from 12 - 3, and then assuming I had a line that ran down from the 3 and then needed to round the corner from 3 to 6 and so forth.
Here's my code (disregard the arc numbers in there now... that's just the latest iteration of my guessing at how this may work, having failed on the previous, more sensible attempts):
Path ctx = new Path();
ctx.moveTo(X+5,Y); //A
ctx.lineTo(X+W-5,Y);//B
ctx.arcTo(new RectF(X+W, Y, X+W, Y+5), -180, 90); //B arc
ctx.lineTo(X+W,Y+H-5); //C
ctx.arcTo(new RectF(X+W,Y+H,X+W-5,Y+H),90,180); //C arc
ctx.lineTo(X+W/2 +6,Y+H);
ctx.lineTo(X+W/2,Y+H+8);
ctx.lineTo(X+W/2-6,Y+H);
ctx.lineTo(X+5,Y+H);
ctx.arcTo(new RectF(X,Y+H,X,Y+H-5),180,270);
ctx.lineTo(X,Y+5);
ctx.arcTo(new RectF(X,Y,X+5,Y),270,0);
Paint p = new Paint();
p.setColor(0xffff00ff);
canvas.drawPath(ctx, p);
much obliged.
odd that no one piped in with an answer, once I found it (it wasn't easy to find) it was really straight forward.
So, the way it works is this:
Assuming you want to draw a rounded corner at 12 - 3 (using clock reference):
you start your path and when you need the line to arc you define a rectangle whose upper left corner is the place where your line is currently terminated and whose lower right corner is the place that you want the arc to go to, so if you imagine a square whose X,Y is 12 (on the clock) and whose X+W,Y+H is 3 that's the square you need.
Now, imagine that you have an oval in that square (in this example it's a circular oval, if you want your curve to be more oval-ish, then define your square as a rectangle), you can take any slice of that circle using the last two params of the method. The first param defines the angle where you want to start cutting. If we're using a compass, 0 degrees is East (not sure why, I'm not a geometry expert... is this normal? I always think of 0 being North, but all the programming geometry examples I see have 0 as East, maybe someone will comment on why that is).
The second param defines how much of the circle you want. If you want the whole circle you put 360 if you want half the circle you put 180 etc.
So, in our case since we want to round the corner from 12 to 3, we put 270 as our starting degree and grab 90 degrees of the circle.
Lastly, when you're done with this process, the line now thinks of itself as being at 3pm so you can continue lineTo(ing) from there.
So... here's my fixed code for my shape (it has a little triangle in it, but that's neither here nor there, the actual rounded parts are B-C, D-E, I-J, and K-A. All the rest are straight lines.
int arc = 25;
public Cursor(int X, int Y, int W, int H){
/*
* A B
* K C
* J D
* I H F E
G
*/
int Ax = X+ arc;
int Ay = Y;
int Bx = X + W - arc;
int By = Y;
int Cx = X + W;
int Cy = Y + arc;
int Dx = Cx;
int Dy = (Y + arc) + (H - arc*2);
int Ex = Bx;
int Ey = Y + H;
int Fx = X+W/2 +6;
int Fy = Ey;
int Gx = X+W/2;
int Gy = Y+H+8;
int Hx = X+W/2-6;
int Hy = Ey;
int Ix = Ax;
int Iy = Hy;
int Jx = X;
int Jy = Dy;
int Kx = X;
int Ky = Cy;
Path ctx = new Path();
ctx.moveTo(Ax,Ay); //A
ctx.lineTo(Bx,By);//B
ctx.arcTo(new RectF(Bx, By, Cx, Cy), 270, 90); //B-C arc
ctx.lineTo(Dx,Dy); //D
ctx.arcTo(new RectF(Dx - arc, Dy, Ex + arc, Ey),0,90); //D-E arc
ctx.lineTo(Fx, Fy); //E-F
ctx.lineTo(Gx, Gy); //F-G
ctx.lineTo(Hx, Hy); //G-H
ctx.lineTo(Ix, Iy); //H - I
ctx.arcTo(new RectF(Jx, Jy, Ix, Iy),90,90);// I = J arc
ctx.lineTo(Kx, Ky); //K
ctx.arcTo(new RectF(Ax - arc, Ay, Kx + arc, Ky),180,90); //K - A arc
ctx.lineTo(Ax, Ay); //K
Paint p = new Paint();
p.setAntiAlias(true);
p.setColor(0xffffffff);
p.setStyle(Style.FILL);
canvas.drawPath(ctx, p);
p.setColor(0xff000000);
p.setStyle(Style.STROKE);
p.setStrokeWidth(3);
canvas.drawPath(ctx, p);
}
This answer visually explains all arcTo parameters using four examples.
arcTo takes the following parameters:
public void arcTo(RectF oval,
float startAngle,
float sweepAngle,
boolean forceMoveTo)
where RectF's constructor takes:
RectF(float left, float top, float right, float bottom)
(Hopefully this visualization is less painful and less mystifying than reading the official arcTo documentation.)
Thanks for this example, it makes the parameters very clear to understand.
From what I read in the dev docs of Android you can probably spare yourself some of the "lineTo()" calls (except those to points F,G,H), since arcTo automatically adds a lineTo when the first point of the arc is not the last point drawn...
As for why 0 starts East, it is so because of math and trigonometry lessons generally assume that the 0 degrees mark is the point where the trigonometric circle (circle with center 0,0 and radius 1) intersects with the X-axis, which is East (these same lessons however generally count the angles counter-clockwise, so 90 degrees becomes north and 270 is south, whereas on Android it seems the angles are counted clockwise)
Here's some sample code (pieced together from one of my classes) to draw a filled, rounded corner rectangle and then adding a stroked rectangle to give it a border:
//Initializing some stuff
_paint = new Paint();
_rect = new RectF();
_radius = 10;
_bgColor = 0xFFFFFFFF;
_borderColor = 0xFFCCCCCC;
//Doing dimension calculations
_rect.left = 0;
_rect.top = 0;
_rect.right = this.getWidth() - 1;
_rect.bottom = this.getHeight() - 1;
//painting
//draw the background
_paint.setColor(_bgColor);
_paint.setStyle(Style.FILL_AND_STROKE);
canvas.drawRoundRect(_rect, _radius, _radius, _paint);
//draw the border
_paint.setStrokeWidth(1);
_paint.setColor(_borderColor);
_paint.setStyle(Style.STROKE);
canvas.drawRoundRect(_rect, _radius, _radius, _paint);