Search code examples
javaandroidmatrixandroid-canvasandroid-custom-view

canvas is offset by setting identity matrix in onDraw of custom view


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!


Solution

  • 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();
    }