Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Drawing to canvas with user interaction is a bit laggy

I started to study canvas drawing with Android, and i would like to make a simple app.

On app start a so called 'snake' starting to move on the screen, when the user taps the screen, the 'snake' changes direction.

I achived this easily but there is a little issue:

When the user taps on the screen, the snake sometimes changes direction on that particular moment, sometimes just after some milliseconds. So the user can clearly feels that the interaction is not as responsive as it should, the snake's exact moment of turning is pretty unpredictable even if you concentrate very hard. There must be some other way to do this better than i did.

Please check my code, I use a Handler with Runnable to move the snake. (Drawing on a canvas and each time setting it as the background of a view, that is each time setting with setContentView to my Activity.

Code:

public class MainActivity extends Activity implements View.OnClickListener {

    Paint paint = new Paint();
    Canvas canvas;


    View contentView; ///<The view to set by setContentView

    int moveSize; ///<Sze in px to move drawer to draw another square


    int leftIndex; ///<Indexes for square drawing
    int topIndex;
    int rightIndex;
    int bottomIndex;

    int maxBoundsX; ///<The max number of squares in X axis
    int maxBoundsY; ///<The max number of squares in Y axis

    int moveSpeedInMillis = 25; ///<One movement square per milliseconds

    Bitmap bitmapToDrawOn; ///<We draw the squares to this bitmap

    Direction currentDirection = Direction.RIGHT; ///< RIGHT,LEFT,UP,DOWN directions. default is RIGHT

    Handler  handler  = new Handler(); ///<Handler for running a runnable that actually 'moves' the snake by drawing squares to shifted positions
    Runnable runnable = new Runnable() {

        @Override
        public void run() {

            Log.i("runnable", "ran");

            //Drawing a square to the current destination
            drawRectPls(currentDirection);
            //After some delay we call again and again and again
            handler.postDelayed(runnable, moveSpeedInMillis);
        }
    };


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);


        /*getting area properties like moveSize, and bounds*/

        moveSize = searchForOptimalMoveSize();

        maxBoundsX = ScreenSizer.getScreenWidth(this) / moveSize;
        maxBoundsY = ScreenSizer.getScreenHeight(this) / moveSize;

        Log.i("moveSize", "moveSize: " + moveSize);
        Log.i("maxBounds: ", "x: " + maxBoundsX + " ; " + "y: " + maxBoundsY);


        /*setting start pos*/

        //We start on the lower left part of the screen

        leftIndex = moveSize * (-1);
        rightIndex = 0;
        bottomIndex = moveSize * maxBoundsY;
        topIndex = moveSize * (maxBoundsY - 1);


        //Setting contentView, bitmap, and canvas

        contentView = new View(this);
        contentView.setOnClickListener(this);

        bitmapToDrawOn = Bitmap.createBitmap(ScreenSizer.getScreenWidth(this), ScreenSizer.getScreenHeight(this), Bitmap.Config.ARGB_8888);
        canvas = new Canvas(bitmapToDrawOn);

        contentView.setBackground(new BitmapDrawable(getResources(), bitmapToDrawOn));
        setContentView(contentView);

        /*starts drawing*/
        handler.post(runnable);


    }

    /**
     * Draws a square to the next direction
     *
     * @param direction the direction to draw the next square
     */
    private void drawRectPls(Direction direction) {

        if (direction.equals(Direction.RIGHT)) {
            leftIndex += moveSize;
            rightIndex += moveSize;
        } else if (direction.equals(Direction.UP)) {
            topIndex -= moveSize;
            bottomIndex -= moveSize;
        } else if (direction.equals(Direction.LEFT)) {
            leftIndex -= moveSize;
            rightIndex -= moveSize;
        } else if (direction.equals(Direction.DOWN)) {
            topIndex += moveSize;
            bottomIndex += moveSize;
        }


        addRectToCanvas();
        contentView.setBackground(new BitmapDrawable(getResources(), bitmapToDrawOn));
        Log.i("drawRect", "direction: " + currentDirection);

    }


    private void addRectToCanvas() {
        paint.setColor(Color.argb(255, 100, 100, 255));
        canvas.drawRect(leftIndex, topIndex, rightIndex, bottomIndex, paint);
    }

    /**
     * Upon tapping the screen the the snake is changing direction, one way simple interaction
     */
    @Override
    public void onClick(View v) {

        if (v.equals(contentView)) {

            if (currentDirection.equals(Direction.RIGHT)) {
                currentDirection = Direction.UP;
            } else if (currentDirection.equals(Direction.UP)) {
                currentDirection = Direction.LEFT;
            } else if (currentDirection.equals(Direction.LEFT)) {
                currentDirection = Direction.DOWN;
            } else if (currentDirection.equals(Direction.DOWN)) {
                currentDirection = Direction.RIGHT;
            }
        }
    }


    /**
     * Just getting the size of a square. Searching for an integer that divides both screen's width and height
     * @return
     */
    private int searchForOptimalMoveSize() {
        int i;
        for (i = 16; i <= 64; i++) {
            Log.i("iter", "i= " + i);
            if (ScreenSizer.getScreenWidth(this) % i == 0) {
                Log.i("iter", ScreenSizer.getScreenWidth(this) + "%" + i + " =0 !");
                if (ScreenSizer.getScreenHeight(this) % i == 0) {
                    Log.i("iter", ScreenSizer.getScreenHeight(this) + "%" + i + " =0 !");
                    return i;
                }
            }
        }
        return -1;
    }


    /**
     * Stops the handler
     */
    @Override
    protected void onDestroy() {
        super.onDestroy();
        handler.removeCallbacks(runnable);
    }
}

E D I T:

I have modified my code, now the view contains all the details and i use the onDraw and invalidate methods just like Philipp suggested.

The result is a little better but i can still clearly feel that the user interaction is results in a laggy direction change.

Perhaps something i should do with threads?

public class SpiralView extends View implements View.OnClickListener {

    int leftIndex; ///<Indexes for square drawing
    int topIndex;
    int rightIndex;
    int bottomIndex;

    int speedInMillis = 50;

    int moveSize; ///<Sze in px to move drawer to draw another square

    int maxBoundsX; ///<The max number of squares in X axis
    int maxBoundsY; ///<The max number of squares in Y axis


    Paint paint = new Paint();
    Direction currentDirection = Direction.RIGHT; ///< RIGHT,LEFT,UP,DOWN directions. default is RIGHT


    public void setUp(int moveSize, int maxBoundsX, int maxBoundsY) {
        this.moveSize = moveSize;
        this.maxBoundsX = maxBoundsX;
        this.maxBoundsY = maxBoundsY;
        this.leftIndex = moveSize * (-1);
        this.rightIndex = 0;
        this.bottomIndex = moveSize * (maxBoundsY);
        this.topIndex = moveSize * (maxBoundsY - 1);

    }

    public SpiralView(Context context, AttributeSet attrs) {
        super(context, attrs);
        setOnClickListener(this);


    }

    /**
     * Draws a square to the next direction
     *
     * @param direction the direction to draw the next square
     */
    private void drawOnPlease(Direction direction, Canvas canvas) {

        if (direction.equals(Direction.RIGHT)) {
            leftIndex += moveSize;
            rightIndex += moveSize;
        } else if (direction.equals(Direction.UP)) {
            topIndex -= moveSize;
            bottomIndex -= moveSize;
        } else if (direction.equals(Direction.LEFT)) {
            leftIndex -= moveSize;
            rightIndex -= moveSize;
        } else if (direction.equals(Direction.DOWN)) {
            topIndex += moveSize;
            bottomIndex += moveSize;
        }


        Log.i("drawRect", "direction: " + currentDirection);
        Log.i("drawRect", "indexes: "+topIndex+" , "+rightIndex+" ," +bottomIndex+" , "+leftIndex);

        addRectToCanvas(canvas);

    }

    private void addRectToCanvas(Canvas canvas) {
        paint.setColor(Color.argb(255, 100, 100, 255));

       canvas.drawRect(leftIndex, topIndex, rightIndex, bottomIndex, paint);
    }


    @Override
    protected void onDraw(Canvas canvas) {

        try {

            drawOnPlease(currentDirection, canvas);

            synchronized (this) {
                wait(speedInMillis);
            }


            invalidate();

        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

    @Override
    public void onClick(View v) {

        if (currentDirection.equals(Direction.RIGHT)) {
            currentDirection = Direction.UP;
        } else if (currentDirection.equals(Direction.UP)) {
            currentDirection = Direction.LEFT;
        } else if (currentDirection.equals(Direction.LEFT)) {
            currentDirection = Direction.DOWN;
        } else if (currentDirection.equals(Direction.DOWN)) {
            currentDirection = Direction.RIGHT;
        }

        //..?
        invalidate();

    }

}
like image 617
Adam Varhegyi Avatar asked Oct 30 '22 23:10

Adam Varhegyi


1 Answers

The magic number is 16 milliseconds for android to redraw the view without having framedrops.

Checkout this video from android developers wich explains this. https://www.youtube.com/watch?v=CaMTIgxCSqU&index=25&list=PLWz5rJ2EKKc9CBxr3BVjPTPoDPLdPIFCE

Especially check this video https://youtu.be/vkTn3Ule4Ps?t=54

It explains how to use canvas clipping in order not to draw the whole surface in every cicle, nut draw only what is needed to be draw.

like image 191
georgeok Avatar answered Nov 15 '22 04:11

georgeok