I´m trying to implement a fractal renderer on android. It uses a SurfaceView to display the results.
I use a seperate Thread for rendering to keep the UI working during processing, but it locks up until unlockCanvasAndPost is called. Is this the expected behavior? How can I keep the UI from locking up?
In this example I simulate the long processing by Thread.sleep.
My SurfaceView-Class looks like this:
public class MyView extends SurfaceView implements SurfaceHolder.Callback {
RenderThread rt = null;
public MyView(Context context, AttributeSet attrs) {
super(context, attrs);
getHolder().addCallback(this);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
// TODO Auto-generated method stub
rt = new RenderThread(holder);
rt.start();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
rt.setStop();
}
}
And this is the RenderThread-class:
public class RenderThread extends Thread {
SurfaceHolder holder;
boolean running = true;
public RenderThread(SurfaceHolder holder) {
this.holder = holder;
}
public void setStop() {
running = false;
}
#Override
public void run() {
while(running) {
Canvas canvas = holder.lockCanvas();
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(canvas == null) return;
Paint p = new Paint();
p.setARGB(255, 255, 0, 0);
canvas.drawLine(0, 0, 100, 100, p);
holder.unlockCanvasAndPost(canvas);
}
}
}
Several possibilities.
You don't show how you've incorporated your MyView class into the activity that contains it. Because you've subclassed SurfaceView, you have to add code to your activity to specifically deal with what you've added with RenderThread. The activity's onPause() method, for example, should be sending a signal to your MyView class to stop its RenderThread.
The surfaceDestroyed() method gets called just before the SurfaceHolder's surface is destroyed. You cannot return from that method until your RenderThread has stopped touching it. This is accomplished through synchronization. Official Android documentation for surfaceDestroyed().
There's a terrific example of how all of this is properly done in WindowSurface.java in the ApiDemos project (for Eclipse, installed with "Samples for SDK" via the Android SDK Manager). Once you've loaded the project into Eclipse (with File->New->Project->Android Sample Project), find WindowSurface.java in com.example.android.apis.graphics within the project.
Finally, if your actual code in the thread's run method (what you've replaced with Thread.sleep in the interest of brevity here) does anything with the canvas then your check for canvas==null should be immediately below the attempted locking of that canvas. Ideally, it's better form to do that anyway.
Related
I'm working with a SurfaceView so I can create a separate thread just for drawing.
My problem is how to obtain a bitmap of the current pixels on the screen for which I draw to the canvas.
Here's some code for a basic overview:
class SurfaceRenderView extends SurfaceView implements Runnable {
SurfaceHolder holder;
Thread thread;
Canvas canvas;
public SurfaceRenderView(Context context) {
super(context);
holder = getHolder(); //holder for Canvas so I can draw in my thread
thread = new Thread(this);
thread.start();
}
#Override
public void run() {
// thread has started
while(true) {
if(holder.getSurface().isValid()) {
canvas = holder.lockCanvas();
// do some drawing...
holder.unlockCanvasAndPost(canvas);
}
}
}
}
}
So I want to get a superior bitmap encapsulating all the bitmaps that I have drawn to the canvas of my SurfaceView. Because there is no method getBitmap() which returns a bitmap of my whole entire SurfaceView I need some other way to achieve such. Please help me out here.
Thank you!
To start with, your example fails, because lockCanvas works only in UI thread, which is a real pain, so I never use SurfaceView.
But since you insist on using SurfaceView, here is the hint: you create your own bitmap draw everything in it and supply it to SurfaceHolder just for drawing. This is how you have the bitmap available to your app. This may be less efficient than drawing on SurfaceHolder, because graphic acceleration doesn't work in this case.
The example assumes that the thread is initialized after the surface is ready, so you know width and size of the view. (You probably should use AsyncTask instead of Thread, but I am rather old-fashioned :) )
public class DrawingThread extends Thread {
Activity activity;
SurfaceView view;;
Bitmap backBuffer;
boolean bufferProcessed;
public DrawingThread(Activity activity, SurfaceView view) {
this.activity = activity;
this.view = view;
backBuffer = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Config.ARGB_8888);
}
#Override
public void run() {
try {
for (;;) {
Canvas canv = new Canvas(backBuffer);
// Draw on canvas
.....................................
bufferProcessed = false;
activity.runOnUiThread(new Runnable() {
public void run() {
SurfaceHolder holder = view.getHolder();
Canvas hCanv = holder.lockCanvas();
hCanv.drawBitmap(backBuffer, 0f, 0f, null);
holder.unlockCanvasAndPost(hCanv);
synchronized(DrawingThread.this) {
bufferProcessed = true;
DrawingThread.this.notify();
}
}
});
while(!bufferProcessed)
synchronized(this) {wait();}
}
}catch(InterruptedException ex) {}
}
}
public class BackgammonBoardView extends SurfaceView implements SurfaceHolder.Callback {
/***
* #author
* The threads class definition
*/
class BBVThread extends Thread
{
//mSurfaceHolder and mHandler are part of the thread.
//mContext is part of the container SurfaceView
private SurfaceHolder mSurfaceHolder;
private Handler mHandler;
//The threads constructor
public BBVThread(SurfaceHolder surfaceHolder, Context context,
Handler handler) {
Log.d(TAG,"BBVThread. Constructor.");
mSurfaceHolder = surfaceHolder;
mHandler = handler;
mContext = context;
mDiceCup = context.getResources().getDrawable(
R.drawable.dicecup);
}
#Override
public void run() {
Log.d(TAG,"BBVThread. run.");
Canvas canvas = null;
try {
canvas = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
Log.d(TAG,"Draw the dice cup...");
mDiceCup.setBounds(0, 0, 120,120);
mDiceCup.draw(canvas);
}
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (canvas != null) {
mSurfaceHolder.unlockCanvasAndPost(canvas);
}
}
}//end run
}
/****
* end of the BBVThread class
*/
/* ########################################
* # BackgammonBoardView Member Variables #
* ######################################## */
private Drawable mDiceCup;
/*
* End BackGammonBoardView Member Variables
*/
public BackgammonBoardView(Context context, AttributeSet attrs)
{
super(context, attrs);
Log.d(TAG,"SurfaceView Constructor.");
//register out interest in hearing about changes to our surface
SurfaceHolder holder = getHolder();
holder.addCallback(this);
thread = new BBVThread(holder, context, new Handler(){
#Override
public void handleMessage(Message m) {
}
});
setFocusable(true); // make sure we get key events
}
#Override
public void surfaceCreated(SurfaceHolder arg0)
{
//Start the thread. This will initiate run which will do the first draw
thread.start();
//Norify that the surface has been created
Log.d(TAG,"SurfaceView. surfaceCreated.");
}
}
I'm following the kind of code that I'm seeing in the LunarLander example. I created a res/drawable folder. Previously the images I had in res/drawable-hdpi however when I didn't see an image drawn to the canvas I created res/drawable and copied the .png's into that folder. My call to R.drawable.dicecup is resolved without any syntax errors. The image is 120 x 120 pixels as I sized it in my image editor. I was surprised when I opened the application and there is no dice cup image drawn anywhere. What could be wrong?
The thread is actually started in the surfaceCreated method. I'm getting Log.d's from run so I know run is executing. Theres no errors as far as I can tell and the program doesn't crash or anything just nothing is drawn.
I've tried moving the draw back into the UI thread but I still see nothing.
P.S. This turned out to be very much my fault. The Bitmap draws fine from the thread it was just being obliterated by the rectangle I am using for my background image and I had not discovered that yet. It should help now that I understand how to draw a bitmap to spot these types of problems. To make things as easy right now I created res/drawables and am loading the images from there.
It looks like you are trying to call draw on a thread other than the UI thread. If that's what you're doing, then that's why its not working. According to the docs, you cant do any UI work on non-UI threads.
I'm trying to create a simple game loop (its not really a game yet) that displays a circle, then after it ticks 100 times draws another circle. I also have a text field that should display how many times the loop has ran. Relevant code is as follows:
MainActivity
public class MainActivity extends Activity
{
#Override
public void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
DrawView v = new DrawView(this);
v.setBackgroundColor(Color.WHITE);
setContentView(v);
}
}
DrawView
public class DrawView extends SurfaceView implements SurfaceHolder.Callback
{
Paint p = new Paint();
MainThread thread;
private int y=0;
public DrawView(Context c)
{
super(c);
thread = new MainThread(this, getHolder());
thread.running = true;
getHolder().addCallback(this);
setFocusable(true);
}
public void draw(Canvas c)
{
if(c==null)
return;
super.onDraw(c);
p.setColor(Color.RED);
p.setTextSize(32);
p.setTypeface(Typeface.SANS_SERIF);
c.drawCircle(getWidth()/2-100,getHeight()/2, 50, p);
c.drawText("y = " + y, 50, 50, p);
if(y==100)
c.drawCircle(getWidth()/2+100,getHeight()/2, 50, p);
else
y++;
}
public void surfaceCreated(SurfaceHolder p1)
{
thread.start();
}
MainThread
public class MainThread extends Thread
{
private DrawView page;
private SurfaceHolder holder;
public boolean running;
public MainThread(DrawView p, SurfaceHolder h)
{
super();
page = p;
holder = h;
}
#Override
public void run()
{
while(running)
{
Canvas c = holder.lockCanvas();
page.draw(c);
holder.unlockCanvasAndPost(c);
}
}
}
It just displays the first circle and the text saying "y = 2." Nothing seems to update, or its doing it twice and then stopping. I am new to Android programming but not to Java. I'm sure I'm just missing something simple. Thanks for any help.
EDIT: Upon further observation, it seems the thread crashes randomly. Everytime I run the app, it dislays "y = " and then a different number each time. I'd reckon it makes it that many ticks before crashing. After I close the app, I get a message that says "Unfortunately, MyApp has stopped." I don't know enough about how Android works to know why its crashing.
EDIT 2: I've discovered its throwing an IllegalArgumentException on the line holder.unlockCanvasAndPost(c). Again, l'm not sure why. Can anyone explain what's happening and how to fix it?
EDIT 3: Logging the value of y each tick reveals that it couts up correctly and stops when it reaches 100 as intended. What happens onscreen does not reflect that for some reason.
I think what you're looking for is a combination of a Handler and a Runnable.
The handler calls the runnable, which runs once, then decides if it should call again.
something like:
private Handler renderHandler = new Handler();
private Runnable renderRunnable = new Runnable() {
#Override
public void run() {
//... do stuff here;
if(shouldRunAgain){
renderHandler.postDelayed(renderRunnable, 1000); // for 1 second
}
}
};
renderHandler.post(renderRunnable);
edit:
without seeing a log cat, I'm going to make an assumption that the UI updates are happening from a thread other than the main UI thread, causing the crash.
have a look at this, this, and this - I think these are more in line of what you're looking for.
There are multiple similar questions like mine, but these questions didn't help me.
I'm making a game. The game thread, SurfaceView and Activity is already finished and works so far. The problem is that the canvas is not redrawn. At startup, it draws the icon on the background, but at every tick, the icon doesn't move (It should move once a second). I want to mention, that I never needed to call postInvalidate. I have a working example where I never called it, but it doesn't work in my current example (I don't want to go into it deeper, since I actually don't need to call it). I copied the current code from my working example, the concept and the way of implementation is exactly the same, but my current code doesn't refresh the canvas. When I log the drawing positions in onDraw method, I see that it's coordinates are updated every second as expected, so I can be sure it's a canvas drawing problem. I have searched for hours but I didn't find what's different to my working example (except that I'm using another Android version and I don't extend thread but implement Runnable, because it's a bad style to extend thread. Nevertheless, I also extended thread to see if there is any difference, but it doesn't help). I already tried to clean the canvas by using canvas.drawColor(Color.BLACK), but that didn't help either. I already tried to use background colors instead of a background image which changes randomly every tick, but it didn't change but stays always the same.
I figured out that the canvas at the very first call has a density of (for example) 240. After the second tick, the canvas density is always 0. I know that the density will not help me here, but maybe it's an important information for someone.
Here are the important classes....
game layout, R.layout.game
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/gameContainer"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<com.mydomain.mygame.base.game.GameSurface
android:id="#+id/gameSurface"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1.0"
android:background="#drawable/background_game" >
</com.mydomain.mygame.base.game.GameSurface>
<!-- ...more definitions -->
</LinearLayout>
GameActivity (contains layout)
public class GameActivity extends Activity
{
#SuppressWarnings("unused")
private GameSurface gameSurface;
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.game);
gameSurface = (GameSurface)findViewById(R.id.gameSurface);
//TODO on button click -> execute methods
}
}
GameSurface (log in onDraw shows updated coordinates every tick)
public class GameSurface extends SurfaceView implements SurfaceHolder.Callback
{
private GameThread thread;
protected final static int TICK_FREQUENCY = 100;// ms, stays always the same. It's a technical constant which doesn't change
private static final String TAG = GameSurface.class.getSimpleName();
public GameSurface(Context context, AttributeSet attrs)
{
super(context, attrs);
ShapeManager.INSTANCE.init(context);
SurfaceHolder holder = getHolder();
holder.addCallback(this);
setFocusable(true); // make sure we get key events
thread = new GameThread(holder, this);
}
public void updateStatus()
{
GameProcessor.INSTANCE.updateShapes();
}
#Override
protected void onDraw(Canvas canvas)
{
for (Shape shape : GameProcessor.INSTANCE.getShapes())
{
Log.i(TAG, "getX()=" + shape.getX() + ", getY()=" + shape.getY());
canvas.drawBitmap(shape.getBitmap(), shape.getX(), shape.getY(), null);
}
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height)
{
//will never invoked since we only operate in landscape
}
#Override
public void surfaceCreated(SurfaceHolder holder)
{
// start the thread here so we don't busy-wait in run
thread.setRunning(true);
new Thread(thread).start();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder)
{
Log.i(TAG, "executing surfaceDestroyed()...");
thread.setRunning(false);
}
}
GameThread
public class GameThread implements Runnable
{
private SurfaceHolder surfaceHolder;
private boolean running = false;
private GameSurface gameSurface;
private long lastTick;
public GameThread(SurfaceHolder surfaceHolder, GameSurface gameSurface)
{
this.surfaceHolder = surfaceHolder;
this.gameSurface = gameSurface;
lastTick = System.currentTimeMillis();
}
#Override
public void run()
{
Canvas canvas;
while (running)
{
canvas = null;
if (System.currentTimeMillis() > lastTick + GameSurface.TICK_FREQUENCY)
{
long timeDifference = System.currentTimeMillis() - (lastTick + GameSurface.TICK_FREQUENCY);
try
{
canvas = surfaceHolder.lockCanvas(null);
synchronized (surfaceHolder)
{
gameSurface.updateStatus();
gameSurface.draw(canvas);
}
}
finally
{
if (canvas != null)
{
surfaceHolder.unlockCanvasAndPost(canvas);
}
}
lastTick = System.currentTimeMillis() - timeDifference;
}
}
}
public void setRunning(boolean running)
{
this.running = running;
}
}
Any ideas why this code doesn't update my canvas? I can't explain it. I do not post ShapeManager and GameProcessor since they don't have anything to do with the problem (and they only load and control the current states and speed of the game).
[UPDATE]
I figured out that onDraw() is invoked before the game thread has started. That means that canvas is passed to this method before thread is using it. The interesting thing is that, after the thread has started, it always uses the same canvas, but it's not the canvas reference which is passed the very first time. Although canvas = surfaceHolder.lockCanvas(null); is assigned every tick, it's always the same reference, but it's not the original reference.
In a working example of mine, the reference is always the same, since I create the bitmaps at constructor initialization time. I can't do that in my current implementation, since I have to do calculations with values I get from onMeasure() which is invoked much later than the constructor.
I tried to somehow pass the original canvas to the thread, but the reference still changes. Meanwhile I think this is the problem, but I don't know how to solve it yet.
As often happens, I found the solution on my own.
Obviously it's really a problem that it draws to different canvas instances. I'm still not sure why this happens. Didn't have the problem before. Nevertheless, I can avoid drawing to canvas by setting setWillNotDraw(true); in my GameSurface constructor and I must not invoke gameSurface.draw(canvas) in my thread, but gameSurface.postInvalidate() instead.
This is my first post here. Sorry if it is not well done and thanks in advance.
There is something that is driven me crazy.
I try to draw something and show it slowly on screen. For this I use Thread.sleep as shown in the code. But it is showing two different versions of the canvas in each iteration.
Can anybody explain what is wrong?
I only want to draw something and after a few seconds draw something else. And so on.
This is my code:
public class Vista extends SurfaceView implements Callback {
private Hilo hilo;
private SurfaceHolder sf;
class Hilo extends Thread {
public boolean running;
private Vista view;
Canvas c = null;
Bitmap btm = null;
public Hilo(SurfaceHolder holder, Vista view) {
sf = holder;
btm = Bitmap.createBitmap(480, 800, Config.ARGB_8888);
c = new Canvas(btm);
}
#Override
public void run() {
Paint paint = new Paint();
paint.setStyle(Style.FILL_AND_STROKE);
paint.setColor(Color.BLUE);
int k = 0;
while (running) {
c = sf.lockCanvas();
k+= 10;
c.drawText(String.valueOf(k/10), k, 20, paint);
sf.unlockCanvasAndPost(c);
try {
sleep(1000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
public Vista(Context context) {
super(context);
SurfaceHolder holder = getHolder();
holder.addCallback(this);
hilo = new Hilo(holder, this);
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// TODO Auto-generated method stub
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
hilo.running = true;
hilo.start();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
hilo.running = false;
while (retry) {
try {
hilo.join();
retry = false;
} catch (InterruptedException e) {
}
}
}
}
Your code is sleep()ing the Main thread, also known as the UI thread. This is something you absolutely mustn't do, as the main thread is responsible for handling all "stuff" in Android: UI rendering, UI event dispatch, etcetera. You can read the Processes and Threads guide on the Android Developer website for more information. Choice quote:
Thus, there are simply two rules to Android's single thread model:
Do not block the UI thread
Do not access the Android UI toolkit from outside the UI thread
The question then becomes, how do you animate a view without sleeping the main thread? The trick is to tell Android to go do something else and come back to you (redraw you) after a little while. Each time you draw, you calculate by how much to advance your animation.
One (crude) way to do this is with postInvalidateDelayed(long), which does exactly that: redraw your view after the given number of milliseconds. It's not a precise delay, it'll fluctuate a bit, so you can't assume that's exactly how much time passed. A way to do this is to store the System.nanoTime() in an instance field, so that the next time you're drawn, you can take the difference to a new reading of nanoTime(). This difference is how much time has passed and thus tells you by how much you should update your animation state. If your animation is supposed to last 1 second in total and 100 milliseconds went by since the last time you drew your scene, you must advance everything by 10%.
Android provides the a lot of utilities to make this easier.