Drawing to canvas with user interaction is a bit laggy
Asked Answered
C

3

6

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

    }

}
Cammie answered 22/7, 2015 at 7:37 Comment(4)
Invalidate() inside onDraw() is a bad idea - it will cause recursive onDraw() calls. Just remove it and probably you'll be fine.Lo
@ruan65 Then what will draw the snake periodicly?Cammie
wait on the ui thread is always a bad ideaBelgium
There is always the surface view route with a dedicated rendering thread (shouldn't be needed for snake though)Starshaped
P
1

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.

Pomology answered 1/8, 2015 at 19:28 Comment(0)
P
1

Do not use a Handler to draw with Canvas. Instead you should create a Custom View and use the onDraw(Canvas canvas) method. In this method you can draw on the Canvas object like you already did. By calling the invalidate() method you trigger a new onDraw() call. In the onTouch() or onClick() function you also trigger a new onDraw call by calling invalidate()

class SnakView extends View {
  @Override
  protected void onDraw(Canvas canvas) {
    // draw on canvas
    invalidate();
  }

  @Override
  public void onClick(View v) {
    // handle the event
    invalidate();
  }
}
Plenish answered 22/7, 2015 at 7:53 Comment(1)
And how do i "wait" in onDraw if i cannot use Handler?Cammie
P
1

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.

Pomology answered 1/8, 2015 at 19:28 Comment(0)
N
0

You can try and Add android:hardwareAccelerated="true" to your manifest, to the or the .

This will work for devices having 3.0+.

Also your target api level should be 11.

Then it will work more smoothly.

Nahuatlan answered 29/7, 2015 at 11:21 Comment(1)
hardware acceleration is turned on by default starting from the Android 4, so I doubt that this will fix OP's issue.Brescia

© 2022 - 2024 — McMap. All rights reserved.