2

Working on a consulting project. A last minute requirement is balls bouncing around on the screen (don't ask why...sigh)

Anyways...these balls are grouped with values. 10 balls are RED worth 100 points. 5 balls are BLUE worth 50 points. 5 balls are GREEN worth 25 points. 5 balls are YELLOW worth 10 points.

With that background the approach that I've taken is to extend a SurfaceView and define 5 threads each of which manages a particular groups of balls.

Each thread receives the same SurfaceHolder from the SurfaceView.

The reason I've chosen multiple threads instead of just one is because the performance of managing all of the balls onscreen is not the greatest.

OpenGL is not really an option right now.

Here's an example of one of the thread classes. When the thread is run, it creates a certain number of balls. Each ball is randomly created and added to a list.

public class hundred_balls_thread extends base_balls_thread {
    public hundred_balls_thread(SurfaceHolder holder, Context ctext, int radius) {
        super(holder, ctext, radius);
    }

    @Override
    public void run() {
        int x, y, radius;

        while (Calorie_balls.size() <= 21) {

            x = 100 + (int) (Math.random() * (mCanvasWidth - 200));
            y = 100 + (int) (Math.random() * (mCanvasHeight) - 200);
            radius = mRadius;

            if ((x - mRadius) < 0) {
                x = x + mRadius;
            }

            if ((x + mRadius) > mCanvasWidth) {
                x = x - mRadius;
            }

            if ((y + mRadius) > mCanvasHeight)
                y = y - mRadius;

            if ((y - mRadius) < 0)
                y = y + mRadius;

            calorie_ball ball = new calorie_ball(x, y, radius, context.getResources().getColor(R.color.red100ball), "100");

            boolean addit = true;

            Calorie_balls.add(ball);
        }

        super.run();
    }
}

Here's the base class that they all extend:

public class base_balls_thread extends Thread {
    protected int mCanvasWidth;
    protected int mCanvasHeight;
    protected int mRadius;
    protected Context context;

    public ArrayList<calorie_ball> Calorie_balls = new ArrayList<calorie_ball>(); // Dynamic array with dots

    private SurfaceHolder holder;
    private boolean running = false;
    private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private final Paint text_paint = new Paint(Paint.ANTI_ALIAS_FLAG);
    private final int refresh_rate = 100;      // How often we update the screen, in ms

    public base_balls_thread(SurfaceHolder holder, Context ctext, int radius) {
        this.holder = holder;
        context = ctext;
        mRadius = radius;
    }

    @Override
    public void run() {
        long previousTime, currentTime;
        previousTime = System.currentTimeMillis();
        Canvas canvas = null;

        while (running) {
            // Look if time has past
            currentTime = System.currentTimeMillis();
            while ((currentTime - previousTime) < refresh_rate) {
                currentTime = System.currentTimeMillis();
            }

            previousTime = currentTime;

            try {

                // PAINT
                try {
                    canvas = holder.lockCanvas();
                    synchronized (holder) {
                        draw(canvas);
                    }
                } finally {
                    if (canvas != null) {
                        holder.unlockCanvasAndPost(canvas);
                    }
                }
                // WAIT
                try {
                    Thread.sleep(refresh_rate); // Wait some time till I need to display again
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            } catch (Exception eal) {
                String msg = eal.getMessage();
                if (msg == null)
                    msg = "Blahbla";
            }
        }

    }

    // The actual drawing in the Canvas (not the update to the screen).
    private void draw(Canvas canvas) {

//        dot temp_dot;
        canvas.drawColor(Color.BLACK);
        paint.setStyle(Paint.Style.FILL_AND_STROKE);
        paint.setStrokeWidth(4);

        text_paint.setColor(Color.BLACK);
        text_paint.setTextSize(40);

        try {
            for (calorie_ball crcl : Calorie_balls) {
                paint.setColor(crcl.color);
                paint.setShader(new RadialGradient(crcl.x + 10, crcl.y, crcl.radius * 2, crcl.color, Color.BLACK, Shader.TileMode.CLAMP));
                if (crcl.x + crcl.radius < 0 && crcl.y + crcl.radius < 0) {
                    crcl.x = canvas.getWidth() / 2;
                    crcl.y = canvas.getHeight() / 2;
                } else {
                    crcl.x += crcl.xVelocity;
                    crcl.y += crcl.yVelocity;

                    if ((crcl.x > canvas.getWidth() - crcl.radius) || (crcl.x - crcl.radius < 0)) {
                        crcl.xVelocity = crcl.xVelocity * -1;
                    }
                    if ((crcl.y > canvas.getHeight() - crcl.radius) || (crcl.y - crcl.radius < 0)) {
                        crcl.yVelocity = crcl.yVelocity * -1;
                    }
                }

                String calval = crcl.get_calorie_value();
                int x = crcl.x + 5;
                int y = crcl.y + 5;

                canvas.drawCircle(crcl.x, crcl.y, crcl.radius, paint);
                canvas.drawText(calval, x, y, text_paint);
            }

        } catch (Exception ep) {
            String b = ep.getMessage();
            if (b == null)
                b = "blah";
        }
    }

    public void setRunning(boolean b) {
        running = b;
    }

    protected Canvas myCanvas;
    protected Bitmap cvsBmp;
    protected Matrix identityMatrix;

    public void setSurfaceSize(int width, int height) {
        synchronized (holder) {
            mCanvasWidth = width;
            mCanvasHeight = height;
        }
    }
}

What's happening is that if it's just ONE thread...it works fine. Once I introduce a second thread the mix...say a HUNDRED_BALLS_THREAD and a FIFTY_BALLS_THREAD that's when everything goes crazy.

The threading "works" if you want to call it that...but the screen flickers constantly.

I know the reasoning is probably obvious to some of you but unfortunately I don't understand why.

I would assume that because each thread is locking the canvas...it would wait.

Any way around this flickering? Is my design decision just completely wrong? I'm sure it's because each thread is accessing the same canvas...but I would think that would cause it flicker like that.

fadden
  • 51,356
  • 5
  • 116
  • 166
tronious
  • 1,547
  • 2
  • 28
  • 45
  • one problem is definitely the canvas.SETCOLOR call...that's causing the flicker...still not working the way I want though. Now I can't clear the previous ball location. – tronious Dec 17 '14 at 22:16

1 Answers1

5

The SurfaceView's Surface is double- or triple-buffered. Every call to unlockCanvasAndPost() submits a new buffer to the compositor. If you're only rendering 1/5th of the scene each time (let's call then A-B-C-D-E), then you'll get a frame with just the 'A' balls, then one with just the 'B' balls, and so on. That assumes that your threads are scheduled fairly round-robin, which they are generally not on Android/Linux. I suspect you're seeing flicker because you're essentially running at 50fps, showing only one set of objects at a time.

If you don't clear the Canvas each time, the glitches will be less obvious, because Android doesn't erase the Canvas for you. So you start with the contents of the previous front buffer, which will probably be a different set of balls.

The system provides exclusive access while the canvas is locked. You can try moving your (should-be-unnecessary) locking of the SurfaceHolder outside the canvas lock/unlock to see if it makes a difference.

For a full explanation, see the Android System-Level Graphics Architecture doc.

As far as your situation goes, while you can have multiple threads updating state like the position of the balls, it's difficult to have multiple threads sharing a single Canvas for rendering. If you really want to do it all in software, try this: create a bitmap, and render the circles yourself (with Bresenham or bitmaps) using as many threads as you like. Periodically have one thread freeze the bitmap, lock the canvas, and blit your bitmap to it.

If you'd like some examples of simple 2D GLES rendering, see Grafika or Android Breakout (the latter of which uses Bresenham to generate a circular ball texture).

fadden
  • 51,356
  • 5
  • 116
  • 166
  • thanks for that indepth explanation. I'm going to check out your links. If this ends up leading me to the solution I'll accept after following up on your suggestions. Thanks for taking the time to post that. – tronious Dec 19 '14 at 01:59
  • Hey @fadden So if there is only one surface (canvas points this surface buffer to draw shapes) for the SurfaceView and only one thread can access the surface to draw by doing lockCanvas() and unlockCanvas why do we need multiple thread? also wound't just normal View work here? I am missing some thing? – solti Apr 19 '16 at 19:31
  • (I not sure if the surface of SurfaceView and View's surface view are same thing) – solti Apr 19 '16 at 19:38
  • @solti Views can only be updated from the main UI thread, while the SurfaceView's Surface doesn't have that restriction. So you can tie up the renderer thread with expensive operations without stalling user input or other framework operations. You're also less susceptible to having your animation stalled by UI thread activity. Yes, a custom View might be a better choice than SurfaceView here. For info about why having multiple threads simultaneously access the same memory is tricky, see http://developer.android.com/training/articles/smp.html – fadden Apr 19 '16 at 20:49
  • @fadden Yeah makes sense about how in case of SurfaceView we can tie up renderer thread to do expensive drawing operation, while the UI thread handles the touch event, onMeasure, onLayout stuff. If this is the case why did you again said "custom View might be a better choice than SurfaceView here." again? Custom View is obviously not a good idea. I was thinking SurfaceView with 2-3 thread instead of 5 thread (not sure might have to collect data and do some research but I am not arguing here about the number of threads) – solti Apr 19 '16 at 21:11
  • Canvas rendering on SurfaceView Surfaces is not hardware-accelerated, but Canvas rendering on a custom View is (unless you turn it off, or you have a very old device). If your biggest expense is rendering then you should make that more efficient. A common way to split stuff across threads is to have one thread do the computation while the other latches the results and does the rendering... splitting the actual rendering across threads will likely end in tears. It's never this simple of course; cf. http://stackoverflow.com/questions/14077403/ – fadden Apr 19 '16 at 21:36
  • @fadden 1) That means costume view renders in GPU? for some reason I thought only TextureView supports rendering in GPU. Regarding splitting works among threads .. you are absolutely right on that and thank you for the link. – solti Apr 19 '16 at 21:42
  • TextureView *requires* hardware acceleration, because it's using a SurfaceTexture and GLES under the hood, but Canvas rendering onto a TextureView's Surface is not accelerated. Custom Views will use GPU rendering if hardware acceleration is enabled (http://developer.android.com/guide/topics/graphics/hardware-accel.html). More info about all of this: https://source.android.com/devices/graphics/architecture.html – fadden Apr 19 '16 at 22:09
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/109601/discussion-between-solti-and-fadden). – solti Apr 19 '16 at 22:12