I set a matrix to my canvas in the onDraw
method of a custom view via
canvas.setMatrix(matrix);
then I just draw a grid using predefined paints:
canvas.drawRect(0,0,viewWidth,viewHeight, background);
for(int i=0; i <= nRows; i++){
canvas.drawLine(0,i*cellHeight, nCols*cellWidth,i*cellHeight,lines);
canvas.drawLine(i*cellWidth, 0, i*cellWidth, nRows*cellHeight, lines);
if(i != nRows)
canvas.drawText(""+i, i*cellWidth, (i+1)*cellHeight, text);
}
and for some reason the whole canvas is offset by about 1.5 times the height of a cell. Any idea why this is happening or how to fix it? The matrix was not initialized and according to the documentation is supposed to therefore be the identity, by the way.
Thanks very much!
I've narrowed this behavior down to that the original Matrix
of a View
's canvas is already translated by the position of the view. This is not apparent, however, if you get the Matrix
using Canvas.getMatrix()
, or View.getMatrix()
. You'll get the identity matrix from those calls.
The canvas offset you're seeing is most likely exactly the same height as the View
's offset from the top of the screen (Status Bar, Title Bar etc).
You are correct in using canvas.concat(matrix)
instead of canvas.setMatrix(matrix)
in this use case, and most use cases. If you really need the original matrix, I did when debugging, you must transform it manually by the View
's translation in its own Window
:
int[] viewLocation = new int[2];
mView.getLocationInWindow(viewLocation);
mOriginalMatrix.setTranslate(viewLocation[0], viewLocation[1]);
EDIT to answer the additional question in comments:
To transform touch coordinates (or any screen coordinates) to match those of a Canvas
, simply make all the transformations to a Matrix
instead, and Canvas.concat()
with that matrix each frame before drawing. (Or you could keep doing all the transformations directly to Canvas
like you're doing now, and use Canvas.getMatrix(mMyMatrix)
to retrieve the matrix after each draw. It's deprecated but it works.)
The matrix can then be used to convert your original grid bounds to those that are drawn on screen. You're essentially doing the exact same thing as Canvas
is doing when it draws the grid, transforming the corner points of the grid to screen coordinates. The grid will now be in the same coordinate system as your touch events:
private final Matrix mMyMatrix = new Matrix();
// Assumes that the grid covers the whole View.
private final float[] mOriginalGridCorners = new float[] {
0, 0, // top left (x, y)
getWidth(), getHeight() // bottom right (x, y)
};
private final float[] mTransformedGridCorners = new float[4];
@Override
public boolean onTouchEvent(MotionEvent event) {
if (/* User pans the screen */) {
mMyMatrix.postTranslate(deltaX, deltaY);
}
if (/* User zooms the screen */) {
mMyMatrix.postScale(deltaScale, deltaScale);
}
if (/* User taps the grid */) {
// Transform the original grid corners to where they
// are on the screen (panned and zoomed).
mMyMatrix.mapPoints(mTransformedGridCorners, mOriginalGridCorners);
float gridWidth = mTransformedGridCorners[2] - mTransformedGridCorners[0];
float gridHeight = mTransformedGridCorners[3] - mTransformedGridCorners[1];
// Get the x and y coordinate of the tap inside the
// grid, between 0 and 1.
float x = (event.getX() - mTransformedGridCorners[0]) / gridWidth;
float y = (event.getY() - mTransformedGridCorners[1]) / gridHeight;
// To get the tapped grid cell.
int column = (int)(x * nbrColumns);
int row = (int)(y * nbrRows);
// Or to get the tapped exact pixel in the original grid.
int pixelX = (int)(x * getWidth());
int pixelY = (int)(y * getHeight());
}
return true;
}
@Override
protected void onDraw(Canvas canvas) {
// Each frame, transform your canvas with the matrix.
canvas.save();
canvas.concat(mMyMatrix);
// Draw grid.
grid.draw(canvas);
canvas.restore();
}
Or the deprecated way to get the matrix, which still works and would perhaps require less changes:
@Override
protected void onDraw(Canvas canvas) {
canvas.save();
// Transform canvas and draw the grid.
grid.draw(canvas);
// Get the matrix from canvas. Can be used to transform
// corners on the next touch event.
canvas.getMatrix(mMyMatrix);
canvas.restore();
}
I found a solution to this problem, if not the cause of the problem.
Replacing the line canvas.setMatrix(matrix);
with canvas.concat(matrix);
did the trick. I think it partly had to do with the fact that setting the matrix changes the origin of the canvas to be calculated with respect to the screen height and width rather than the view's. But there are mysteries because this also magically solved some other problems I had.
EDIT
It turns out that one of the consequences of doing things this way is that event.getX()
and event.getY()
in the onTouchEvent(MotionEvent event)
method start returning integers which are hard to deal with. My custom view is for making a zoomable, pannable grid of squares which I can click on and get the coordinate of the square. Using this method I try to get the (x,y) coordinate of a click event (MotionEvent.ACTION_UP
case in onTouchEvent
method).
I try to get the x coordinate, for instance, via
int x = (int) ((start.x - dspl.x)/(cellWidth*scaleFactor));
I account for how much I have panned the screen (dspl
gives the total displacement that has occurred thus far) and the amount I have zoomed it (scaleFactor
gives the total scaling that has occurred). But this does not account for the points about which successive zooms have occurred.
Any good ideas about how to get the right coordinates?