Reputation: 2260
I just made an Activity which uses at "setContentView" a view from a class which extends SurfaceView. The problem is: It works fine, but when I exit it (BACK key) it crashes. Code:
package ro.etrandafir.mate.appCreator;
import android.app.Activity;
import android.os.Bundle;
import android.content.Context;
import android.view.View;
import android.view.SurfaceView;
import android.graphics.Canvas;
import android.view.MotionEvent;
import android.graphics.Color;
import android.graphics.Paint;
public class Sample2 extends Activity implements View.OnTouchListener {
float x = 0, y = 0;
SampleTwoView theView;
public boolean onTouch(View v, MotionEvent event) {
// TODO: Implement this method
x = event.getX();
y = event.getY();
return true;
}
@Override
protected void onPause() {
super.onPause();
finish();
}
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
theView = new SampleTwoView(this);
theView.setOnTouchListener(this);
setContentView(theView);
}
public class SampleTwoView extends SurfaceView implements Runnable {
Paint p = new Paint();
public SampleTwoView(Context context) {
super(context);
p.setColor(Color.RED);
Thread theThread = new Thread(this);
theThread.start();
}
public void run() {
while (true) {
if (!getHolder().getSurface().isValid()) continue;
Canvas canvas;
canvas = getHolder().lockCanvas();
canvas.drawColor(Color.BLUE);
if ((x != 0) && (y != 0)) canvas.drawCircle(x, y, 40, p);
getHolder().unlockCanvasAndPost(canvas);
}
}
}
}
What can I do? Should I add onDestroy or what?
Thanks in advance, Matei
Upvotes: 2
Views: 2144
Reputation: 201
As someone mentioned it in the above, when your activity ends, your thread is still running, but your custom SurfaceView is no longer available, so you will get a Null Point Exception. Your existing code can easily be patched by adding a boolean that gets set to false as soon as the onPause fn gets called:I had the same problem. To solve it I added the following onPause() to your SampleTwoView class:
// pause method will destroy the Thread
public void pause() {
isRunning = false;
while (true) {
try {
myThread.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
break;
}
myThread = null;
}
Then call this onPause() method in your onPause() method of your Sample2 class as follows:
@Override
protected void onPause() {
super.onPause();
SampleTwoView.onPause();
finish();
}
So everytime the onPause() method of your main Activity class is called the Thread will be destroyed. I hope this will help you. Cheers!
Upvotes: 2
Reputation: 8579
The issue you are getting is related to this code:
Canvas canvas;
canvas = getHolder().lockCanvas();
canvas.drawColor(Color.BLUE);
When your activity ends, your thread is still running, but your custom SurfaceView
is no longer available, so you will get a null ptr exception. Your existing code can easily be patched by adding a boolean that gets set to false as soon as the onPause
fn gets called:
public void run() {
while (booleanThatGetsSetToFalseWhenActivityPauses) {
if (!getHolder().getSurface().isValid()) continue;
Canvas canvas;
canvas = getHolder().lockCanvas();
canvas.drawColor(Color.BLUE);
if ((x != 0) && (y != 0)) canvas.drawCircle(x, y, 40, p);
getHolder().unlockCanvasAndPost(canvas);
}
}
However, I would suggest altering the structure of your application as a whole. This may just be for practice, but I think a more efficient and bug free way of accomplishing your goal would be to simply use a standard SurfaceView
and to completely decouple your drawing logic from any custom view.
My redesigned activity is below, but it utilizes a Ball
class that is used to maintain the ball's logic, which, in your current code is separately coupled with both the actvity (the coordinates) and the view (the Paint
). In this new ball class, a ball has a location (specified by a PointF
), a Paint
, and a diameter. It also has methods to get most of these variables in addition to setting some.
public class Ball {
private Paint mPaint;
private PointF mCoordinates;
private int mDiameter;
public Ball (int color, int diameter) {
mPaint = new Paint();
mPaint.setColor(color);
mCoordinates = new PointF();
mCoordinates.x = 0;
mCoordinates.y = 0;
mDiameter = diameter;
}
public void setCoordinates (float x, float y) {
mCoordinates.x = x;
mCoordinates.y = y;
}
public PointF getCoordinates() {
return mCoordinates;
}
public Paint getPaint() {
return mPaint;
}
public int getDiameter() {
return mDiameter;
}
/* You did not want to draw the uninitialized ball, so this method checks that */
public boolean hasNonZeroLocation () {
return (mCoordinates.x != 0 && mCoordinates.y != 0);
}
}
I use the Ball
class in the activity as shown below. Notice that the redrawing to the canvas now only occurs when a user touches the canvas as opposed to an infinite while loop. This is due to the utilization of the Handler
class which posts actions to run to the UI thread. Additionally, now we do not need a custom view, and our ball's logic has been decoupled from the activity and the view.
public class RedBallActivity extends Activity {
Handler mDrawingHandler;
SurfaceView mDrawingSurfaceView;
Ball mBall;
private final Runnable drawRedBallOnBlueSurface = new Runnable() {
@Override
public void run() {
if (!mDrawingSurfaceView.getHolder().getSurface().isValid()) return;
Canvas canvas = mDrawingSurfaceView.getHolder().lockCanvas();
canvas.drawColor(Color.BLUE);
if (mBall.hasNonZeroLocation())
canvas.drawCircle(mBall.getCoordinates().x, mBall.getCoordinates().y, mBall.getDiameter(), mBall.getPaint());
mDrawingSurfaceView.getHolder().unlockCanvasAndPost(canvas);
}
};
private final OnTouchListener mCanvasTouchListener = new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
mBall.setCoordinates(event.getX(), event.getY());
mDrawingHandler.post(drawRedBallOnBlueSurface);
return true;
}
};
@Override
protected void onCreate(Bundle b) {
super.onCreate(b);
mDrawingSurfaceView = new SurfaceView(this);
mDrawingSurfaceView.setOnTouchListener(mCanvasTouchListener);
setContentView(mDrawingSurfaceView);
mBall = new Ball(Color.RED, 40);
mDrawingHandler = new Handler();
}
}
Now, if you actually run this code you will notice that initially the screen is not drawn with a blue background. You might be tempted to simply call mDrawingHandler.post(drawRedBallOnBlueSurface);
at the end of the onCreate
method, but it is not guaranteed that the SurfaceView will be ready to be drawn upon (see the documentation on this lockCanvas method). If you want the surface to initially be blue, you need to implement a [SurfaceHolder.Callback][2]
, which needs to be connected to the SurfaceView's SurfaceHolder, and on the surfaceCreated
method being called, we know the surface is ready, so we can then call mDrawingHandler.post(drawRedBallOnBlueSurface);
Now, with this added, I change the Activity to implement [SurfaceHolder.Callback][2]
as follows:
public class FriendManagerActivity extends Activity implements SurfaceHolder.Callback {
and add this line to the constructor:
mDrawingSurfaceView.getHolder().addCallback(this);
and implement the interface:
@Override
public void surfaceCreated(SurfaceHolder holder) {
mDrawingHandler.post(drawRedBallOnBlueSurface);
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
Feel free to ask any questions on my little redesign! While your problem could be easily patched, I felt like the way you were coupling logic with Views was a little bit flawed, and thought a little more info on SurfaceView coding would be helpful.
Upvotes: 2