Android Canvas Coordinate System - android

I'm trying to find information on how to change the coordinate system for the canvas. I have some vector data I'd like to draw to a canvas using things like circles and lines, but the data's coordinate system doesn't match the canvas coordinate system.
Is there a way to map the units I'm using to the screen's units?
I'm drawing to an ImageView which isn't taking up the entire display.
If I have to do my own calculations prior to each drawing call, how to I find the width and height of my ImageView?
The getWidth() and getHeight() calls I tried seem to be returning the entire canvas size and not the size of the ImageView which isn't helpful.
I see some matrix stuff, is that something that will work for me?
I tried to use the "public void scale(float sx, float sy)", but that works more like a pixel level zoom rather than a vector scale function by expanding each pixel. This means if the dimensions are increased to fit the screen, the line thickness is also increased.
Update:
After some research I'm starting to think there's no way to change coordinate systems to something else. I'll need to map all my coordinates to the screen's pixel coordinates and do so by modifying each vector. The getWidth() and getHeight() seem to be working better for me now. I can say what was wrong, but I suspect I can't use these methods inside the constructor.

Thanks for the reply. I have pretty much given up on getting this to work in the way I think it should. Of course how I think things should happen isn't how they do happen. :)
Here's basically how it works, but it seems to be off by a pixel in some cases and the circles seem to be missing sections when things land on some boundary conditions I have yet to figure out. Personally I think this is unacceptable to be inside application code and should be in the Android libraries... wink wink, nudge nudge if you work for Google. :)
private class LinearMapCanvas
{
private final Canvas canvas_; // hold a wrapper to the actual canvas.
// scaling and translating values:
private double scale_;
private int translateX_;
private int translateY_;
// linear mapping from my coordinate system to the display's:
private double mapX(final double x)
{
final double result = translateX_ + scale_*x;
return result;
}
private double mapY(final double y)
{
final double result = translateY_ - scale_*y;
return result;
}
public LinearMapCanvas(final Canvas canvas)
{
canvas_ = canvas;
// Find the extents of your drawing coordinates:
final double minX = extentArray_[0];
final double minY = extentArray_[1];
final double maxX = extentArray_[2];
final double maxY = extentArray_[3];
// deltas:
final double dx = maxX - minX;
final double dy = maxY - minY;
// The display's available pixels, accounting for margins and pen stroke width:
final int width = width_ - strokeWidth_ - 2*margin_;
final int height = height_ - strokeWidth_ - 2*margin_;
final double scaleX = width / dx;
final double scaleY = height / dy;
scale_ = Math.min(scaleX , scaleY); // Pick the worst case, so the drawing fits
// Translate so the image is centered:
translateX_ = (int)((width_ - (int)(scale_*dx))/2.0 - scale_*minX);
translateY_ = (int)((height_ - (int)(scale_*dy))/2.0 + scale_*maxY);
}
// wrappers around the canvas functions you use. These are only two of many that would need to be wrapped. Annoying and error prone, but beats any alternative I can find.
public void drawCircle(final float cx, final float cy, final float radius, final Paint paint)
{
canvas_.drawCircle((float)mapX(cx), (float)mapY(cy), (float)(scale_*radius), paint);
}
public void drawLine(final float startX, final float startY, final float stopX, final float stopY, final Paint paint)
{
canvas_.drawLine((float)mapX(startX), (float)mapY(startY), (float)mapX(stopX), (float)mapY(stopY), paint);
}
...
}

You can scale the canvas co-ordinates to your own units using the preScale() method of the canvas' matrix. Be aware though that this also scales the Paint's stroke width, which may not be what you want.

The only way I know of to do custom vector graphics on the fly in Android is to draw everything into an image file and then put that into an ImageView. I'm not sure I understand exactly what you are asking, but if the only issue is scaling, the ImageView will scale whatever image it is given to its own size using android:scaleType="center" as a property of the ImageView.
As far as changing the coordinate system, I doubt that is possible (although I haven't researched it). It might be fairly trivial to write a function that would map your data's system to the standard Android coordinate system, though.

Related

Best way to draw a path traveled

I'm making a application to track a veicle based in GPS coordinates.
I created a SurfaceView to draw the field, vehicle and the path (route) for him.
The result looked like this:
The black dots represent the coming of GPS coordinates, and blue rectangles would be the area covered by the path traveled. (the width of the path is configurable)
The way I'm drawing with blue rectangles (this is my question) which are the area covered by the path traveled. (the width of the path is configurable)
With that I need to overcome some situation.
I need to calculate the field's rotation angle so that the path always get left behind. (completed)
I need to calculate the angle of rotation of each rectangle so they are facing towards the vehicle. (completed)
In the future I will need:
Detect when the vehicle passes twice in the same place. (based on the path traveled)
Calculate the area (m²) all traveled by the vehicle.
I would like some tips for draw this path.
My code:
public void draw(Canvas canvas) {
Log.d(getClass().getSimpleName(), "draw");
canvas.save();
// translate canvas to vehicle positon
canvas.translate((float) center.cartesian(0), (float) center.cartesian(1));
float fieldRotation = 0;
if (trackerHistory.size() > 1) {
/*
Before drawing the way, only takes the last position and finds the angle of rotation of the field.
*/
Vector lastPosition = new Vector(convertToTerrainCoordinates(lastPoint));
Vector preLastPosition = new Vector(convertToTerrainCoordinates(preLastPoint));
float shift = (float) lastPosition.distanceTo(preLastPosition);
/*
Having the last coordinate as a triangle, 'preLastCoord' saves the values of the legs, while 'shift' is the hypotenuse
*/
// If the Y offset is negative, then the opposite side is the Y displacement
if (preLastPosition.cartesian(1) < 0) {
// dividing the opposite side by hipetenusa, we have the sine of the angle that must be rotated.
double sin = preLastPosition.cartesian(1) / shift;
// when Y is negative, it is necessary to add or subtract 90 degrees depending on the value of X
// The "Math.asin()" calculates the radian arc to the sine previously calculated.
// And the "Math.toDegress()" converts degrees to radians from 0 to 360.
if (preLastPosition.cartesian(0) < 0) {
fieldRotation = (float) (Math.toDegrees(Math.asin(sin)) - 90d);
} else {
fieldRotation = (float) (Math.abs(Math.toDegrees(Math.asin(sin))) + 90d);
}
}
// if not, the opposite side is the X offset
else {
// dividing the opposite side by hipetenusa have the sine of the angle that must be rotated.
double senAngulo = preLastPosition.cartesian(0) / shift;
// The "Math.asin()" calculates the radian arc to the sine previously calculated.
// And the "Math.toDegress()" converts degrees to radians from 0 to 360.
fieldRotation = (float) Math.toDegrees(Math.asin(senAngulo));
}
}
final float dpiTrackerWidth = Navigator.meterToDpi(trackerWidth); // width of rect
final Path positionHistory = new Path(); // to draw the route
final Path circle = new Path(); // to draw the positions
/*
Iterate the historical positions and draw the path
*/
for (int i = 1; i < trackerHistory.size(); i++) {
Vector currentPosition = new Vector(convertToTerrainCoordinates(trackerHistory.get(i))); // vector with X and Y position
Vector lastPosition = new Vector(convertToTerrainCoordinates(trackerHistory.get(i - 1))); // vector with X and Y position
circle.addCircle((float) currentPosition.cartesian(0), (float) currentPosition.cartesian(1), 3, Path.Direction.CW);
circle.addCircle((float) lastPosition.cartesian(0), (float) lastPosition.cartesian(1), 3, Path.Direction.CW);
if (isInsideOfScreen(currentPosition.cartesian(0), currentPosition.cartesian(1)) ||
isInsideOfScreen(lastPosition.cartesian(0), lastPosition.cartesian(1))) {
/*
Calcule degree by triangle sides
*/
float shift = (float) currentPosition.distanceTo(lastPosition);
Vector dif = lastPosition.minus(currentPosition);
float sin = (float) (dif.cartesian(0) / shift);
float degress = (float) Math.toDegrees(Math.asin(sin));
/*
Create a Rect to draw displacement between two coordinates
*/
RectF rect = new RectF();
rect.left = (float) (currentPosition.cartesian(0) - (dpiTrackerWidth / 2));
rect.right = rect.left + dpiTrackerWidth;
rect.top = (float) currentPosition.cartesian(1);
rect.bottom = rect.top - shift;
Path p = new Path();
Matrix m = new Matrix();
p.addRect(rect, Path.Direction.CCW);
m.postRotate(-degress, (float) currentPosition.cartesian(0), (float) currentPosition.cartesian(1));
p.transform(m);
positionHistory.addPath(p);
}
}
// rotates the map to make the route down.
canvas.rotate(fieldRotation);
canvas.drawPath(positionHistory, paint);
canvas.drawPath(circle, paint2);
canvas.restore();
}
My goal is to have something like this application: https://play.google.com/store/apps/details?id=hu.zbertok.machineryguide (but only in 2D for now)
EDIT:
To clarify a bit more my doubts:
I do not have much experience about it. I would like a better way to draw the path. With rectangles it was not very good. Note that the curves are some empty spaces.
Another point is the rotation of rectangles, I'm rotating them at the time of drawing. I believe this will make it difficult to detect overlaps
I believe I need math help for the rotation of objects and overlapping detection. And it also helps to draw the path of a filled shape.
After some research time, I came to a successful outcome. I will comment on my thoughts and how was the solution.
As I explained in question, along the way I have the coordinates traveled by the vehicle, and also a setting for the width of the path should be drawn.
Using LibGDX library is ready a number of features, such as the implementation of a "orthographic camera" to work with positioning, rotation, etc.
With LibGDX I converted GPS coordinates on my side points to the road traveled. Like this:
The next challenge was to fill the path traveled. First I tried using rectangles, but the result was as shown in my question.
So the solution was to trace triangles using the side of the path as vertices. Like this:
Then simply fill in the triangles. Like this:
Finally, using Stencil, I set up OpenGL to highlight overlaps. Like this:
Other issues fixed:
To calculate the covered area, simply calculate the area of existing triangles along the path.
To detect overlapping, just check if the current position of the vehicle is within a triangle.
Thanks to:
AlexWien for the attention and for their time.
Conner Anderson by videos of LibGDX
And a special thanks to Luis Eduardo for knowledge, helped me a lot.
The sample source code.
Usually such a path is drawn using a "path" method from the graphics lib.
In that lib you can create a polyline, and give a line width.
You further specify how corners are filled. (BEVEL_JOIN, MITTER_JOIN)
The main question is wheter the path is drawn while driving or afterwords.
Afterwords is no problem.
To draw while driving might be a bit tricky to avoid to redraw the path each second.
When using the Path with moveTo and lineTo to create a polyline, then you can set a line width, and the graphics lib will do that all for you.
Then there will be no gaps, since it is a poly line.

Get draw bounds/rect of a view

I'm developing an app where a lot of views can be rotated - it's something like a map of physical objects. I have to detect when 2 objects (all objects are rectangles/squares) are overlapping and if a user has performed a single/double/long tap on an object. For this reason I need to know the drawing bounds of a view.
Let's look at the example image bellow - the green rectangle is rotated 45 degrees. I need to get the coordinates of the 4 corners of the green rectangle. If I use view.getHitRect() it returns the bounding box (marked in red) of the view, which is of no use to me.
Do you know how could I get the coordinates of the edges of a view?
The only solution I could think of is to subclass a View, manually store the initial coordinates of the corners and calculate their new values on every modification to the view - translation, scale and rotation but I was wondering if there is a better method.
P.S. The app should be working on Android 2.3 but 4.0+ solutions are also welcomed.
Thanks to pskink I explored again the Matrix.mapPoints method and managed to get the proper coordinates of the corners of the rectangle.
If you are running on Android 3.0+ you can easily get the view's matrix by calling myView.getMatrix() and map the points of interest. I had to use 0,0 for the upper left corner and getWidth(),getHeight() for the bottom right corner and map these coordinates to the matrix. After that add view's X and Y values to get the real values of the corners.
Something like:
float points[] = new float[2];
points[0] = myView.getWidth();
points[1] = myView.getHeight();
myView.getViewMatrix().mapPoints(points);
Paint p = new Paint();
p.setColor(Color.RED);
//offset the point and draw it on the screen
canvas.drawCircle(center.getX() + points[0], center.getY() + points[1], 5f, p);
If you have to support lower versions of Android you can use NineOldAndroids. Then I've copied and modified one of its internal methods to get the view's matrix:
public Matrix getViewMatrix()
{
Matrix m = new Matrix();
Camera mCamera = new Camera();
final float w = this.getWidth();
final float h = this.getHeight();
final float pX = ViewHelper.getPivotX(this);
final float pY = ViewHelper.getPivotY(this);
final float rX = ViewHelper.getRotationX(this);;
final float rY = ViewHelper.getRotationY(this);
final float rZ = ViewHelper.getRotation(this);
if ((rX != 0) || (rY != 0) || (rZ != 0))
{
final Camera camera = mCamera;
camera.save();
camera.rotateX(rX);
camera.rotateY(rY);
camera.rotateZ(-rZ);
camera.getMatrix(m);
camera.restore();
m.preTranslate(-pX, -pY);
m.postTranslate(pX, pY);
}
final float sX = ViewHelper.getScaleX(this);
final float sY = ViewHelper.getScaleY(this);;
if ((sX != 1.0f) || (sY != 1.0f)) {
m.postScale(sX, sY);
final float sPX = -(pX / w) * ((sX * w) - w);
final float sPY = -(pY / h) * ((sY * h) - h);
m.postTranslate(sPX, sPY);
}
m.postTranslate(ViewHelper.getTranslationX(this), ViewHelper.getTranslationY(this));
return m;
}
I've put this method in an overloaded class of a view (in my case - extending TextView). From there on it's the same as in Android 3.0+ but instead of calling myView.getMatrix() you call myView.getViewMatrix().

how to get bounds of a scaled view?

It seems that setScaleX or setScaleY don't actually change left,top,right,bottom properties. getX and getY remain unchanged too.
So if I scale a view whats the easiest way to get 4 corner coordinates of the newly scaled view?
I tried getHitRect but that doesn't give me the right answer. I am trying to avoid manually calculating the new bounds based on existing transformations (rotation and scale with pivots factored in).
After exploring the view api, it looks like there is no direct API method that does this.
However you can easily get the new points by grabbing the transform matrix of the view and using that to get the new bounds.
Something like this:
Matrix m = view.getMatrix();
Rect bbox = new Rect();
view.getDrawingRect(bbox);
m.mapRect(bbox);
If you want to operate on (x,y) coordiantes directly there is a matrix.mapPoints that will achieve the same result.
I believe if you get the width and height and multiply it by the scales, you'll get the scaled width and height.
int scaledWidth = getWidth() * getScaleX();
int scaledHeight = getHeight() * getScaleY();
int newLeft = getLeft() + (scaledWidth / 2);
int newRight = newLeft + scaledWidth;
int newTop = getTop() + (scaledHeight / 2);
int newBottom = newTop + scaledHeight;
This is assuming that you scaled with a pivot x and y at the center of the view. Things gets far more complicated if you have pivots in strange areas.

Scrollable ImageView Android Problem (overscrolling)

I got my image to at least SCROLL, but I don't want it to scroll past the image itself. I have variables called maxLeft, maxRight, etc that I have that I currently just set to
ImageView img = (ImageView)findViewById(R.id.mapimg);
DisplayMetrics dm = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(dm);
int maxX = (int)????;
int maxY = (int)????;
// set scroll limits
final int maxLeft = (maxX * -1);
final int maxRight = maxX;
final int maxTop = (maxY * -1);
final int maxBottom = maxY;
I've been messing around with what I could in place of the question marks I put there, but I seem to be stuck, especially when I try on different emulators. Any help would really be appreciated! Thanks
If I understand your question correctly, you want to extend ImageView to add scrolling. If that's the case, you don't want to use getWindowmanager() as that returns the dimensions of the entire screen (including the title bar). Rather, you want to extend ImageView and get the view's dimensions from onMeasure. You can check out my answer here where I added zoom functionality and panning to ImageView.
I used setImageMatrix and postTranslate to set a matrix equal to the image and move it. To track the image's location, I used the following:
float f[] = new float[9];
matrix.getValues(m); //see matrix documentation. inserts matrix values into f.
float x = m[Matrix.MTRANS_X];
float y = m[Matrix.MTRANS_Y];
Float x and y will track the top left corner of the image. Make sure if the next event will cause the image to scroll out of bounds, you adjust postTranslate to equal the border of the image. The answer i linked above should give you a good place to start, and if you also want zoom functionality, then you're in luck, because you don't have to do any additional work.

Android: measureText() Return Pixels Based on Scaled Pixels

So I use Paint's measureText() method to measure the width of a segment of text, but I wanted to measure text based on a certain text size. Say I wanted to get the width of a text segment that will be 20 scaled pixels when it occupies a certain TextView. I tried the following:
Paint paint = new Paint();
paint.setTextSize(20);
paint.measureText("sample text");
However, it does not seem to be working. I believe it is returning a width with respect to a smaller text size. I feel like I'm missing something that will make me slap myself in the face and yell herp derp.
You need to get the densityMultiplier like so:
final float densityMultiplier = getContext().getResources().getDisplayMetrics().density;
final float scaledPx = 20 * densityMultiplier;
paint.setTextSize(scaledPx);
final float size = paint.measureText("sample text");
I do something like this when I have to.
int textSize = 20;
for(int i = 2; i<18 && curTextSize< textSize;i+=2)
{
this.label.setTextSize(i);
curTextSize = this.label.getPaint().measureText(this.label.getText().toString());
}
I don't have enough reputation points to comment on answers but in reference to the comments by #schwiz and #AndreasEK on the accepted answer:
measureText(), along with getTextBounds(), does not include padding so it's possible that the solution to their problem is to add the left and right padding (or start and end padding)
final float scaleFactor = getContext().getResources().getDisplayMetrics().density;
final float scaledPx = 20 * scaleFactor;
paint.setTextSize(scaledPx);
final float padding = scaleFactor * (textView.getPaddingStart() + textView.getPaddingEnd());
final float size = paint.measureText("sample text") + padding;
Try
Paint.SetLinearText(true)
That solved the problem for me.

Categories

Resources