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();
}
}
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.
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();
}
}
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.
Related
I'm trying to use SurfaceView with Canvas to draw a waveform. I'm using one SurfaceView with Canvas to draw, and it works.
But when I want to make my first SurfaceView overlay by the second SurfaceView(using FrameLayout). It's doesn't work.
And these two questions appears to me:
1. If I use Canvas in second SurfaceView, then the second canvas becomes null;
2. If I don't use Canvas in second SurfaceView, but just call overlay, then the SurfaceView size will be overlay but the graph is same.
Referenced code is as below:
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Log.e("onCreate", "here");
l1 = (LinearLayout) findViewById(R.id.l1);
l2 = (LinearLayout) findViewById(R.id.l2);
l2.setVisibility(View.INVISIBLE);
sfv = (SurfaceView) findViewById(R.id.sfv);
sfh = sfv.getHolder();
sfv.getHolder().setFormat(PixelFormat.TRANSLUCENT);
sfv2 = (SurfaceView) findViewById(R.id.sfv2);
sfh2 = sfv2.getHolder();
sfv2.getHolder().setFormat(PixelFormat.TRANSLUCENT);
paint.setColor(Color.BLUE);
paint.setStrokeWidth(3);
paint2.setColor(Color.RED);
paint2.setStrokeWidth(3);
Log.i("flow", "now at before init()");
tv = (TextView) findViewById(R.id.tv);
sfh.addCallback(this);
sfh2.addCallback(this);
//init();
}
public void init() {
Log.e("init", "here");
if (pic) {
canvas = sfh.lockCanvas(new Rect(xtime, 0, xtime + 2, getWindowManager().getDefaultDisplay().getHeight()));
canvas.drawARGB(255, 0, 0, 0);
for (int i = 0; i < 600; i++) {
a = 600 - i;
canvas.drawLine(xtime, oldy, xtime + 2, a, paint);
xtime += 2;
oldy = a;
if (xtime > 1000) {
xtime = 0;
oldy = 0;
}
}
sfh.unlockCanvasAndPost(canvas);
// tv.setText("in the init2");
}
else{
Log.e("sfh2", "here");
canvas = sfh2.lockCanvas(new Rect(xtime2, 0, xtime2 + 2, getWindowManager().getDefaultDisplay().getHeight()));
if(canvas == null){
Log.e("canvas", "null here");
}
else {
canvas.drawARGB(255, 255, 255, 255);
for (int i = 0; i < 300; i++) {
a = 300 - i;
canvas.drawLine(xtime2, oldy, xtime2 + 2, a, paint2);
xtime2 += 2;
oldy = a;
if (xtime2 > 1000) {
xtime2 = 0;
oldy2 = 0;
}
}
sfh2.unlockCanvasAndPost(canvas);
}
}
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
Log.e("surfaceCreated", "here");
init();
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
Log.e("surfaceChanged", "here");
init();
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.e("surfaceDestroyed", "here");
}
public void onchange(View view) {
if (pic) {
pic = false;
l1.setVisibility(View.VISIBLE);
l2.setVisibility(View.INVISIBLE);
tv.setText("change sf2");
Log.e("pic", "pic " + pic);
} else {
pic = true;
l1.setVisibility(View.INVISIBLE);
l2.setVisibility(View.VISIBLE);
tv.setText("change sf1");
Log.e("pic", "pic " + pic);
}
}
I have been in confusion for two weeks, hope somebody can help me to resolve this issue.
Thanks everyone.
First and foremost, bear in mind that a SurfaceView has two parts, the Surface and the View. The View part acts like any other View. The Surface is created asynchronously, and is a completely separate layer.
Your current code is creating two overlapping Surfaces with the same Z-ordering, which means they're trying to occupy the same space at the same time. The results are undefined, but generally speaking you will see one Surface but not the other. Use e.g. setZOrderMediaOverlay() on the one that should be in front of the other. See Grafika's "multi-surface test" Activity for an example with three overlapping Surfaces.
The Surfaces are created asynchronously, and not at the same time, so you should expect your surfaceCreated() callback to fire twice. It looks like you're calling init() each time. If it first fires for Surface A, and init() tries to draw on Surface B, the method will fail, because it's trying to draw on a Surface that hasn't been created yet. Check the value of SurfaceHolder in surfaceCreated(), and and only draw on the Surface that was just created. (Alternatively, wait until they've both been created, and then draw on both.)
I have this code. It isn't complex at all, I'm learning and I was practising and messing around with the surface view. I only want 2 rectangles to be there and an image going down. When we touch in the second rectangle, the image starts going up. We touch the one in the left and the image restarts going down. When it arrives the line 89, it stops and gives the null pointer exception. I guess the error happens when I create the canvas.
public class LearningThreads extends Activity {
ActivitySurface activitySurface;
boolean crossGoesUp = false;//Sets if the cross goes up or down
int leftRectangle1, topRectangle1, rightRectangle1, bottomRectangle1;
int leftRectangle2, topRectangle2, rightRectangle2, bottomRectangle2;
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
activitySurface = new ActivitySurface(this);
activitySurface.setOnTouchListener(new canvasClicked());
setContentView(activitySurface);//Sets the content to be the class we've created
}
protected void onPause() {//When the app is paused, it calls the method which pauses the thread that is constantly running
super.onPause();
activitySurface.pause();
}
protected void onResume() {//When the app starts or restarts, it calls the method which starts the thread
super.onResume();
activitySurface.resume();
}
public class canvasClicked implements OnTouchListener {
public boolean onTouch(View v, MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {//Only if the user starts touching something because I'm not interested in when he releases
if (e.getX() <= leftRectangle1 && e.getX() >= rightRectangle1 && e.getY() <= topRectangle1 && e.getY() >= bottomRectangle1) {//Tests if the user touched one of the rectangles
crossGoesUp = false;
}
if (e.getX() <= leftRectangle2 && e.getX() >= rightRectangle2 && e.getY() <= topRectangle2 && e.getY() >= bottomRectangle2) {//Tests if the user touched the other rectangle
crossGoesUp = true;
}
}
return false;//It doesn't repeat
}
}
public class ActivitySurface extends SurfaceView {
Thread mainThread;
boolean isRunning = false;//Sets when the app is running or not
SurfaceHolder holder;//Gives us useful methods to use in the canvas
int crossY = 0;//Sets the y coordinate of the cross
public ActivitySurface(Context context) {
super(context);
holder = getHolder();
}
public void resume() {
isRunning = true;
mainThread = new Thread(new mainThread());
mainThread.start();
}
public void pause() {
isRunning = false;
try {
mainThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public class mainThread implements Runnable {//Takes care of the thread
public void run() {
while(isRunning) {
if (holder.getSurface().isValid())//Tests if the surface is valid, if it is not it won't do anything until it is
continue;
Canvas canvas = holder.lockCanvas();//Creating the canvas: it has a mistake, everytime I use the canvas it gives a NullPointerException
canvas.drawRGB(50, 50, 50);//Setting the color of the canvas
leftRectangle1 = canvas.getWidth()/4 - 40;//Setting the variables so they can be used outside this Thread
topRectangle1 = canvas.getHeight()/2 - 25;
rightRectangle1 = canvas.getWidth()/4 + 40;
bottomRectangle1 = canvas.getHeight()/2 + 25;
leftRectangle2 = canvas.getWidth()/4 + (canvas.getWidth()/4) * 2 - 40;
topRectangle2 = canvas.getHeight()/2 - 25;
rightRectangle2 = canvas.getWidth()/4 + (canvas.getWidth()/4) * 2 + 40;
bottomRectangle2 = canvas.getHeight()/2 + 25;
Paint paint = new Paint();//Setting the paint which will define the colors of the rectangles
paint.setARGB(0, 100, 100, 100);
Rect rectangle1 = new Rect();//Setting the position of the rectangle 1
rectangle1.set(leftRectangle1, topRectangle1, rightRectangle1, bottomRectangle1);
Rect rectangle2 = new Rect();//Setting the position of the rectangle 2
rectangle2.set(leftRectangle2, topRectangle2, rightRectangle2, bottomRectangle2);
canvas.drawRect(rectangle1, paint);//Drawing the rectangles
canvas.drawRect(rectangle2, paint);
Bitmap cross = BitmapFactory.decodeResource(getResources(), R.drawable.animation);//Creating the image which is going to go up and down
canvas.drawBitmap(cross, canvas.getWidth()/2 - cross.getWidth()/2, crossY, paint);
if (crossGoesUp) {//If the crossGoesUp is true, that means the user last touch was in the rectangle 2, so the image goes up
if (crossY < -cross.getHeight())//Tests if the image isn't out of bounds
crossY = canvas.getHeight() + cross.getHeight();
crossY -= 5;
} else {
if (crossY > canvas.getHeight() + cross.getHeight())//Same as above
crossY = -cross.getHeight();
crossY += 5;
}
holder.unlockCanvasAndPost(canvas);
}
}
}
}
}
This is my logcat:
05-02 07:13:41.897: E/AndroidRuntime(1634): FATAL EXCEPTION: Thread-103
05-02 07:13:41.897: E/AndroidRuntime(1634): Process: garden.apps.my_apps, PID: 1634
05-02 07:13:41.897: E/AndroidRuntime(1634): java.lang.NullPointerException
05-02 07:13:41.897: E/AndroidRuntime(1634): at com.apps.my_apps.LearningThreads$ActivitySurface$mainThread.run(LearningThreads.java:90)
05-02 07:13:41.897: E/AndroidRuntime(1634): at java.lang.Thread.run(Thread.java:841)
you should use SurfaceHolder.Callback, your paint is invisible
paint.setARGB(alpha,Red,Green,Blue) - alpha 0..255 0-invisible 255-visible
public class LearningThreads extends Activity {
ActivitySurface activitySurface;
boolean crossGoesUp = false;//Sets if the cross goes up or down
int leftRectangle1, topRectangle1, rightRectangle1, bottomRectangle1;
int leftRectangle2, topRectangle2, rightRectangle2, bottomRectangle2;
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
activitySurface = new ActivitySurface(this);
activitySurface.setOnTouchListener(new canvasClicked());
setContentView(activitySurface);//Sets the content to be the class we've created
}
protected void onPause() {//When the app is paused, it calls the method which pauses the thread that is constantly running
super.onPause();
}
protected void onResume() {//When the app starts or restarts, it calls the method which starts the thread
super.onResume();
}
public class canvasClicked implements View.OnTouchListener {
public boolean onTouch(View v, MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {//Only if the user starts touching something because I'm not interested in when he releases
if (e.getX() <= leftRectangle1 && e.getX() >= rightRectangle1 && e.getY() <= topRectangle1 && e.getY() >= bottomRectangle1) {//Tests if the user touched one of the rectangles
crossGoesUp = false;
}
if (e.getX() <= leftRectangle2 && e.getX() >= rightRectangle2 && e.getY() <= topRectangle2 && e.getY() >= bottomRectangle2) {//Tests if the user touched the other rectangle
crossGoesUp = true;
}
}
return false;//It doesn't repeat
}
}
public class ActivitySurface extends SurfaceView implements SurfaceHolder.Callback {
Thread mainThread;
boolean isRunning = false;//Sets when the app is running or not
SurfaceHolder holder;//Gives us useful methods to use in the canvas
int crossY = 0;//Sets the y coordinate of the cross
public ActivitySurface(Context context) {
super(context);
holder = getHolder();
holder.addCallback(this);
}
public void resume() {
isRunning = true;
mainThread = new Thread(new mainThread());
mainThread.start();
}
public void pause() {
isRunning = false;
try {
mainThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public class mainThread implements Runnable {//Takes care of the thread
public void run() {
while (isRunning) {
if (!holder.getSurface().isValid())//Tests if the surface is valid, if it is not it won't do anything until it is
continue;
Canvas canvas = holder.lockCanvas();//Creating the canvas: it has a mistake, everytime I use the canvas it gives a NullPointerException
canvas.drawRGB(50, 50, 50);//Setting the color of the canvas
leftRectangle1 = canvas.getWidth() / 4 - 40;//Setting the variables so they can be used outside this Thread
topRectangle1 = canvas.getHeight() / 2 - 25;
rightRectangle1 = canvas.getWidth() / 4 + 40;
bottomRectangle1 = canvas.getHeight() / 2 + 25;
leftRectangle2 = canvas.getWidth() / 4 + (canvas.getWidth() / 4) * 2 - 40;
topRectangle2 = canvas.getHeight() / 2 - 25;
rightRectangle2 = canvas.getWidth() / 4 + (canvas.getWidth() / 4) * 2 + 40;
bottomRectangle2 = canvas.getHeight() / 2 + 25;
Paint paint = new Paint();//Setting the paint which will define the colors of the rectangles
paint.setARGB(255, 100, 100, 100);
Rect rectangle1 = new Rect();//Setting the position of the rectangle 1
rectangle1.set(leftRectangle1, topRectangle1, rightRectangle1, bottomRectangle1);
Rect rectangle2 = new Rect();//Setting the position of the rectangle 2
rectangle2.set(leftRectangle2, topRectangle2, rightRectangle2, bottomRectangle2);
canvas.drawRect(rectangle1, paint);//Drawing the rectangles
canvas.drawRect(rectangle2, paint);
Bitmap cross = BitmapFactory.decodeResource(getResources(), R.drawable.animation);//Creating the image which is going to go up and down
canvas.drawBitmap(cross, canvas.getWidth() / 2 - cross.getWidth() / 2, crossY, paint);
if (crossGoesUp) {//If the crossGoesUp is true, that means the user last touch was in the rectangle 2, so the image goes up
if (crossY < -cross.getHeight())//Tests if the image isn't out of bounds
crossY = canvas.getHeight() + cross.getHeight();
crossY -= 5;
} else {
if (crossY > canvas.getHeight() + cross.getHeight())//Same as above
crossY = -cross.getHeight();
crossY += 5;
}
holder.unlockCanvasAndPost(canvas);
}
}
}
#Override
public void surfaceCreated(SurfaceHolder holder) {
resume();
}
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
}
#Override
public void surfaceDestroyed(SurfaceHolder holder) {
pause();
}
}
someActivity.runOnUiThread(new Runnable() {
#Override
public void run() {
//Your code to run in GUI thread here
}//public void run() {
});
I hope this will help you.
I managed to figger it out by myself, the problem was in this block of code:
if (holder.getSurface().isValid())//Tests if the surface is valid, if it is not it won't do anything until it
continue;
I forgot to put the exclamation mark, so the app only did what was below when the surface was not valid and it gave me a NullPointerException.
Thank you anyway.
Is it possible to get these values programmatically:
frames per second
how often the method onDraw() is called if the whole View is invalidated immediately.
1) Here is how I'm counting fps:
public class MyView extends View {
private int mFPS = 0; // the value to show
private int mFPSCounter = 0; // the value to count
private long mFPSTime = 0; // last update time
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (SystemClock.uptimeMillis() - mFPSTime > 1000) {
mFPSTime = SystemClock.uptimeMillis();
mFPS = mFPSCounter;
mFPSCounter = 0;
} else {
mFPSCounter++;
}
String s = "FPS: " + mFPS;
canvas.drawText(s, x, y, paint);
invalidate();
}
}
or just write your own object that would calculate this for you :)...
2) Try using
Log.d(tag, "onDraw() is called");
in your onDraw() method.
I am developing a simple app that produced bubbles on screen on touch. Bubble move around on the screen and get popped when it reaches the border of screen or if a user touches it. I have successfully coded a bubble to pop when reaches borders of the screen but can't figure out a way to detect if the user touched it.
I want to detect if the user touched any bubble on the screen.
Note:- The bubbles are created using custom view. Also I have included some important functions only but can include whole code if you want. Here's the code
public class BubbleActivity extends Activity {
// These variables are for testing purposes, do not modify
private final static int RANDOM = 0;
private final static int SINGLE = 1;
private final static int STILL = 2;
private static int speedMode = RANDOM;
private static final int MENU_STILL = Menu.FIRST;
private static final int MENU_SINGLE_SPEED = Menu.FIRST + 1;
private static final int MENU_RANDOM_SPEED = Menu.FIRST + 2;
private static final String TAG = "Lab-Graphics";
// Main view
private RelativeLayout mFrame;
// Bubble image
private Bitmap mBitmap;
// Display dimensions
private int mDisplayWidth, mDisplayHeight;
// Sound variables
// AudioManager
private AudioManager mAudioManager;
// SoundPool
private SoundPool mSoundPool;
// ID for the bubble popping sound
private int mSoundID;
// Audio volume
private float mStreamVolume;
// Gesture Detector
private GestureDetector mGestureDetector;
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Set up user interface
mFrame = (RelativeLayout) findViewById(R.id.frame);
// Load basic bubble Bitmap
mBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.b64);
}
#Override
protected void onResume() {
super.onResume();
// Manage bubble popping sound
}
#Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
// Get the size of the display so this view knows where borders are
mDisplayWidth = mFrame.getWidth();
mDisplayHeight = mFrame.getHeight();
}
}
// Set up GestureDetector
private void setupGestureDetector() {
mGestureDetector = new GestureDetector(this,
new GestureDetector.SimpleOnGestureListener() {
// Detecting if user touched bubble here
#Override
public boolean onSingleTapConfirmed(MotionEvent event) {
// Trying to get bubble position but can't just get x=0, y=0 tried
// many things
Log.d(TAG,""+((ViewGroup)mFrame).getChildCount());
for(int i=0; i<((ViewGroup)mFrame).getChildCount(); ++i) {
View nextChild = ((ViewGroup)mFrame).getChildAt(i);
Rect rect = new Rect();
nextChild.getLocalVisibleRect(rect);
int[] location = new int[2];
nextChild.getLocationOnScreen(location);
Log.d(TAG, "X = " + location[0] + " Y = " + location[1]);
}
if(event.getAction() == MotionEvent.ACTION_DOWN){
BubbleView bubbleView = new BubbleView(getApplicationContext(), event.getX(),event.getY());
bubbleView.start();
mFrame.addView(bubbleView);
}
return true;
}
});
}
#Override
public boolean onTouchEvent(MotionEvent event) {
// TODO - delegate the touch to the gestureDetector
return mGestureDetector.onTouchEvent(event);
}
#Override
protected void onPause() {
// TODO - Release all SoundPool resources
super.onPause();
}
// BubbleView is a View that displays a bubble.
// This class handles animating, drawing, popping amongst other actions.
// A new BubbleView is created for each bubble on the display
private class BubbleView extends View {
private static final int BITMAP_SIZE = 64;
private static final int REFRESH_RATE = 40;
private final Paint mPainter = new Paint();
private ScheduledFuture<?> mMoverFuture;
private int mScaledBitmapWidth;
private Bitmap mScaledBitmap;
// location, speed and direction of the bubble
private float mXPos, mYPos, mDx, mDy;
private long mRotate, mDRotate;
public BubbleView(Context context, float x, float y) {
super(context);
log("Creating Bubble at: x:" + x + " y:" + y);
// Create a new random number generator to
// randomize size, rotation, speed and direction
Random r = new Random();
// Creates the bubble bitmap for this BubbleView
createScaledBitmap(r);
// Adjust position to center the bubble under user's finger
mXPos = x - mScaledBitmapWidth / 2;
mYPos = y - mScaledBitmapWidth / 2;
// Set the BubbleView's speed and direction
setSpeedAndDirection(r);
// Set the BubbleView's rotation
setRotation(r);
mPainter.setAntiAlias(true);
}
// Start moving the BubbleView & updating the display
private void start() {
// Creates a WorkerThread
ScheduledExecutorService executor = Executors
.newScheduledThreadPool(1);
// Execute the run() in Worker Thread every REFRESH_RATE
// milliseconds
// Save reference to this job in mMoverFuture
mMoverFuture = executor.scheduleWithFixedDelay(new Runnable() {
#Override
public void run() {
// TODO - implement movement logic.
// Each time this method is run the BubbleView should
// move one step. If the BubbleView exits the display,
// stop the BubbleView's Worker Thread.
// Otherwise, request that the BubbleView be redrawn.
if(!isOutOfView()){
moveWhileOnScreen();
}
else{
stop(true);
}
}
}, 0, REFRESH_RATE, TimeUnit.MILLISECONDS);
}
private synchronized boolean intersects(float x, float y) {
// TODO - Return true if the BubbleView intersects position (x,y)
return false;
}
// Cancel the Bubble's movement
// Remove Bubble from mFrame
// Play pop sound if the BubbleView was popped
private void stop(final boolean popped) {
if (null != mMoverFuture && mMoverFuture.cancel(true)) {
// This work will be performed on the UI Thread
mFrame.post(new Runnable() {
#Override
public void run() {
// TODO - Remove the BubbleView from mFrame
if (popped) {
log("Pop!");
// TODO - If the bubble was popped by user,
// play the popping sound
mFrame.removeView(BubbleView.this);
//mMoverFuture.cancel(true);
mSoundPool.play(mSoundID, 1, 1, 1, 0, 1);
}
log("Bubble removed from view!");
}
});
}
}
// Change the Bubble's speed and direction
private synchronized void deflect(float velocityX, float velocityY) {
log("velocity X:" + velocityX + " velocity Y:" + velocityY);
//TODO - set mDx and mDy to be the new velocities divided by the REFRESH_RATE
mDx = velocityX/REFRESH_RATE;
mDy = velocityY/REFRESH_RATE;
}
// Draw the Bubble at its current location
#Override
protected synchronized void onDraw(Canvas canvas) {
// TODO - save the canvas
canvas.save();
// TODO - increase the rotation of the original image by mDRotate
mRotate = mRotate + mDRotate;
// TODO Rotate the canvas by current rotation
canvas.rotate(mRotate, mXPos + mScaledBitmapWidth/2, mYPos + mScaledBitmapWidth/2);
// TODO - draw the bitmap at it's new location
canvas.drawBitmap(mScaledBitmap, mXPos, mYPos,mPainter);
// TODO - restore the canvas
canvas.restore();
}
private synchronized boolean moveWhileOnScreen() {
// TODO - Move the BubbleView
// Returns true if the BubbleView has exited the screen
mXPos = mDx+mXPos;
mYPos = mDy+mYPos;
postInvalidate();
return false;
}
private boolean isOutOfView() {
// TODO - Return true if the BubbleView has exited the screen
if(mXPos + mScaledBitmapWidth/2 >= mDisplayWidth - mScaledBitmapWidth/2 || mXPos <0
||mYPos + mScaledBitmapWidth/2 >= mDisplayHeight - mScaledBitmapWidth/2 || mYPos <0){
return true;
}
return false;
}
}
Update :-
To clarify a bit, I want to get the location of all the bubbles on the screen and then compare them to event.getX() and event.getY() to detect if i tapped on any bubble. II have to check bubble tap in onSingleTapConfirmed(). I am correctly able to get the total number of bubbles but can't detect their location on the screen.
for(int i=0; i<((ViewGroup)mFrame).getChildCount(); ++i) {
View nextChild = ((ViewGroup)mFrame).getChildAt(i);
Rect rect = new Rect();
nextChild.getLocalVisibleRect(rect);
int[] location = new int[2];
nextChild.getLocationOnScreen(location);
Log.d(TAG, "X = " + location[0] + " Y = " + location[1]);
}
Above code gives the correct number of bubbles but return their coordinates as 0,0.
In your onSingleTapConfirmed function, try the following to iterate through your BubbleViews and pass the Event X and Y coordinates on.
for(int i=0;i<mFrame.getChildCount();i++){
BubbleView bubbleThis = (BubbleView) mFrame.getChildAt(i);
if (bubbleThis.intersects(event.getX(),event.getY())){
bubbleThis.stop(true);
return true;
}
}
The function in BubbleView should then return true if the X and Y fall inside its boundaries. I will add the function inside intersects function in BubbleView as clarification:
private synchronized boolean intersects(float x, float y) {
if ( (x>mXPos && x<(mXPos+mScaledBitmapWidth)) && (y>mYPos && y<(mYPos+mScaledBitmapWidth)) ) {
return true;
}
return false;
}
If you want to know if a user tapped a bubble, set its onClickListener. If you want to know if the user just touched it, override its onTouchEvent and look for ACTION_DOWN.
How are you implementing the onDown() method of your SimpleOnGestureListener?
Please take a look at these answers:
Gesture Detector not working
Android GestureDetector with SimpleOnGestureListener within SurfaceView
Detect which View was tapped in the onSingleTapConfirmed method
Bubble is circle in shape, so you just need to compare its radius with the distance between bubble center and the position.
mRadius = radius of the bubble
mDistance = distance between (event.getX(), event.getY()) and bubble center (mXPos + mRadius, mYPos + mRadius)
I am trying to make a cannon game that is printed in an Android Developer's book and having trouble with the drawing of the actual game. I can hear my sounds, the alert dialog at the end of the game is being shown after my timer runs out even though I cant see the timer, and the white background is shown, just no game elements
Here is the main activity:
//CannonGame.java
//Main Activity for the Cannon game app.
package com.deitel.cannongame;
import android.os.Bundle;
import android.app.Activity;
import android.view.Menu;
import android.media.AudioManager;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.GestureDetector.SimpleOnGestureListener;
public class CannonGame extends Activity {
private GestureDetector gestureDetector; //listens for double taps
private CannonView cannonView; //custom view to display the game
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); //call super's onCreate method
setContentView(R.layout.main); //inflate the layout
//get the cannonview
cannonView = (CannonView) findViewById(R.id.cannonView);
//initialize the GuestureDetector
gestureDetector = new GestureDetector(this, gestureListener);
//allow for volume keys to set game volume
setVolumeControlStream(AudioManager.STREAM_MUSIC);
} //end method onCreate
//when the app is pushed to the background, pause it
#Override
public void onPause(){
super.onPause(); //call the super method
cannonView.stopGame(); //terminates the game
} //end method onPause
//release resources
#Override
protected void onDestroy(){
super.onDestroy();
cannonView.releaseResources();
} //end method onDestroy
//called when the user touches the screen in this Activity
#Override
public boolean onTouchEvent(MotionEvent event){
//get int representing the type of action which caused this event
int action = event.getAction();
//the user touched the screen or dragged aong the screen
if (action == (MotionEvent.ACTION_DOWN) || action == MotionEvent.ACTION_MOVE)
cannonView.alignCannon(event); //align the cannon
//call the GuestureDetector's onTouchEvent method
return gestureDetector.onTouchEvent(event);
} //end onTouchEvent
//listens for touch events sent to the GuestureDetector
SimpleOnGestureListener gestureListener = new SimpleOnGestureListener(){
//called when the use doubletaps the screen
#Override
public boolean onDoubleTap(MotionEvent e){
cannonView.fireCannonball(e); //fire the cannonball
return true; //the event was handled
} //end method onDoubleTap
}; //end gestureListener
#Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.cannon_game, menu);
return true;
}
}
Here is the custom view the main activity calls and all it's properties:
//CannonView.java
//Displays the Cannon Game
package com.deitel.cannongame;
import java.util.HashMap;
import java.util.Map;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.Paint;
import android.media.SoundPool;
import android.media.AudioManager;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class CannonView extends SurfaceView implements SurfaceHolder.Callback{
private CannonThread cannonThread; //controls the game loop
private Activity activity; //to display Game Over dialog in GUI thread
private boolean dialogIsDisplayed = false;
//constants for game play
public static final int TARGET_PIECES = 7; //sections in the target
public static final int MISS_PENALTY = 2; //seconds deducted on a miss
public static final int HIT_REWARD = 3; //seconds added on a hit
//variables for the game loop and tracking statistics
private boolean gameOver; //is the game over?
private double timeLeft; //the amount of time left in seconds
private int shotsFired; //the number of shots the user has fired
private double totalTimeElapsed; //the number of seconds elapsed
//variables for the blocker and target
private Line blocker; //start and end points of the blocker
private int blockerDistance; //blocker distance from the left
private int blockerBeginning; //blocker distance from the top
private int blockerEnd; //blocker bottom edge distance from the top
private int initialBlockerVelocity; //initial blocker speed multiplier
private float blockerVelocity; //blocker speed multiplier during game
private Line target; //start and end points of the target
private int targetDistance; //target distance from left
private int targetBeginning; //target distance from the top
private double pieceLength; //length of target piece
private int targetEnd; //target bottom distance from the top
private int initialTargetVelocity; //initial target speed multiplier
private float targetVelocity; //target speed multiplier during game
private int lineWidth; //width of the target and blocker
private boolean[] hitStates; //is each target piece hit?
private int targetPiecesHit; //number of target pieces hit (out of 7)
//variables for the cannon and cannonball
private Point cannonball; //cannonball image's upper-left corner
private int cannonballVelocityX; //cannonball's x velocity
private int cannonballVelocityY; //cannonball's y velocity
private boolean cannonballOnScreen; //is the cannonball on the screen
private int cannonballRadius; //cannonball radius
private int cannonballSpeed; //cannonball speed
private int cannonBaseRadius; //cannon base radius
private int cannonLength; //cannon barrel length
private Point barrelEnd; //the endpoint of the cannon's barrel
private int screenWidth; //width of the screen
private int screenHeight; //height ofthe screen
//constants and variables for managing sounds
private static final int TARGET_SOUND_ID = 0;
private static final int CANNON_SOUND_ID = 1;
private static final int BLOCKER_SOUND_ID = 2;
private SoundPool soundPool; //plays sound effects
private Map<Integer, Integer> soundMap; //maps IDs to SoundPool
//Paint vairables used when drawing each item on the screen
private Paint textPaint; //paint used to draw text
private Paint cannonballPaint; //paint used to draw the cannonball
private Paint cannonPaint; //paint used to draw the cannon
private Paint blockerPaint; //paint used to draw the blocker
private Paint targetPaint; //paint used to draw the target
private Paint backgroundPaint; //paint used to clear the drawing area
//public constructor
public CannonView(Context context, AttributeSet attrs){
super(context, attrs); //call super's constructor
activity = (Activity) context;
//register SurfaceHolder.Calback listener
getHolder().addCallback(this);
//initiate Lines and points representing game items
blocker = new Line(); //create the blocker as a Line
target = new Line(); //create the target as a Line
cannonball = new Point(); //create the cannonball as a point
//initialize hitStates as a boolean array
hitStates = new boolean[TARGET_PIECES];
//initialize SoundPool to play the app's three sound effects
soundPool = new SoundPool(1, AudioManager.STREAM_MUSIC, 0);
//create Map of sounds and pre-load sounds
soundMap = new HashMap<Integer, Integer>(); //create new HashMap
soundMap.put(TARGET_SOUND_ID, soundPool.load(context, R.raw.target_hit, 1));
soundMap.put(CANNON_SOUND_ID, soundPool.load(context, R.raw.cannon_fire, 1));
soundMap.put(BLOCKER_SOUND_ID, soundPool.load(context, R.raw.blocker_hit, 1));
//construct Paints for drawing text, cannonball, cannon, blocker, and target; these are configured
//in method onSizeChanged
textPaint = new Paint(); //paint for drawing text
cannonballPaint = new Paint(); //paint for drawing the cannonball
cannonPaint = new Paint(); //paint for drawing the cannon
blockerPaint = new Paint(); //Paint for drawing the blocker
targetPaint = new Paint(); //Paint for drawing the target
backgroundPaint = new Paint(); //paint for drawing the background
} //end CannonView constructor
//called when the size of this view changes --including when this view is first added to the view hierarchy
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh){
super.onSizeChanged(w, h, oldw, oldh);
screenWidth = w; //store the width
screenHeight = h; //store the height
cannonBaseRadius = h / 18; //cannon base radius 1/18 screen height
cannonLength = w / 8; //cannon length 1/8 screen width
cannonballRadius = w / 36; //cannonball radious 1/36 screen width
cannonballSpeed = w * 3 / 2; //cannonball speed multiplier
lineWidth = w / 24; //target and blocker 1/24 screen width
//configure instance variables related to the blocker
blockerDistance = w * 5 / 8; //blocker 5/8 screen width from left
blockerBeginning = h / 8; //distance from top 1/8 screen height
blockerEnd = h * 3 / 8; //distance from top 3/8 screen height
initialBlockerVelocity = h / 2; //initial blocker speed multiplier
blocker.start = new Point(blockerDistance, blockerBeginning);
blocker.end = new Point(blockerDistance, blockerEnd);
//configure instance variables related to the target
targetDistance = w * 7 / 8; //target 7/8 screen width from left
targetBeginning = h / 8; //distance from top 1/8 screen height
targetEnd = h * 7 / 8; //distance from top 7/8 screen height
pieceLength = (targetEnd - targetBeginning) / TARGET_PIECES;
initialTargetVelocity = -h / 4; //initial target speed multiplier
target.start = new Point(targetDistance, targetBeginning);
target.end = new Point(targetDistance, targetEnd);
//endpoint of the cannon's barrel initially points horizontally
barrelEnd = new Point(cannonLength, h / 2);
//configure Paint objects for drawing game elements
textPaint.setTextSize(w / 20); //text size 1/20 of screen width
textPaint.setAntiAlias(true); //smoothes the text
cannonPaint.setStrokeWidth(lineWidth * 1.5f); //set line thickness
blockerPaint.setStrokeWidth(lineWidth); //set line thickness
targetPaint.setStrokeWidth(lineWidth); //set line thickness
backgroundPaint.setColor(Color.WHITE); //set background color
newGame(); //set up and start a new game
} //end method onSizeChange
public void newGame(){
//set every element of hitStates to false--restores target pieces
for (int i = 0; i < TARGET_PIECES; ++i)
hitStates[i] = false;
targetPiecesHit = 0; //no target pieces have been hit
blockerVelocity = initialBlockerVelocity; //set initial blocker velocity
targetVelocity = initialTargetVelocity; //set initial target velocity
timeLeft = 10; //start the countdown at 10 seconds
cannonballOnScreen = false; //the cannonball is not on the screen
shotsFired = 0; //set the initial number of shots fired
totalTimeElapsed = 0.0; //set the time elapsed to zero
blocker.start.set(blockerDistance, blockerEnd);
blocker.end.set(blockerDistance, blockerEnd);
target.start.set(targetDistance, targetBeginning);
target.end.set(targetDistance, targetEnd);
if (gameOver){
gameOver = false; //the game is not over
cannonThread = new CannonThread(getHolder());
cannonThread.start();
} //end if
} //end method newGame
//called repleatedly by the CannonThread to update the game elements
private void updatePositions(double elapsedtimeMS){
double interval = elapsedtimeMS / 1000.0; //convert to seconds
if(cannonballOnScreen){ //if there is currently a shot fired
//update cannonball position
cannonball.x += interval * cannonballVelocityX;
cannonball.y += interval * cannonballVelocityY;
//check for collision with blocker
if(cannonball.x + cannonballRadius > blockerDistance && cannonball.x - cannonballRadius
< blockerDistance && cannonball.y + cannonballRadius > blocker.start.y && cannonball.y - cannonballRadius < blocker.end.y){
cannonballVelocityX *= -1; //reverse cannonball's direction
timeLeft -= MISS_PENALTY; //penalize the user
//play blocker sound
soundPool.play(soundMap.get(BLOCKER_SOUND_ID), 1, 1, 1, 0, 1f);
} //end if
//check for collisions with left and right walls
else if(cannonball.x + cannonballRadius > screenWidth || cannonball.x - cannonballRadius < 0)
cannonballOnScreen = false; //remove cannonball from screen
else if(cannonball.y + cannonballRadius > screenHeight || cannonball.y - cannonballRadius < 0)
cannonballOnScreen = false; //remove cannonball from screen
else if(cannonball.x + cannonballRadius > targetDistance && cannonball.x - cannonballRadius <
targetDistance && cannonball.y + cannonballRadius > target.start.y && cannonball.y - cannonballRadius < target.end.y){
//determine target section number (0 is the top)
int section = (int) ((cannonball.y - target.start.y) / pieceLength);
//check if the piece hasn't been hit yet
if((section >= 0 && section < TARGET_PIECES) && !hitStates[section]){
hitStates[section] = true; //section was hit
cannonballOnScreen = false; //remove cannonball
timeLeft += HIT_REWARD; //add reward to remaining time
//play target hit sound
soundPool.play(soundMap.get(TARGET_SOUND_ID), 1, 1, 1, 0, 1F);
//if all pieces have been hit
if (++targetPiecesHit == TARGET_PIECES){
cannonThread.setRunning(false); //show winning dialog
gameOver = true; //the game is over
} //end if
} //end if
} //end else if
} //end if
//update the blockers position
double blockerUpdate = interval * blockerVelocity;
blocker.start.y += blockerUpdate;
blocker.end.y += blockerUpdate;
//update the target's position
double targetUpdate = interval * targetVelocity;
target.start.y += targetUpdate;
target.end.y += targetUpdate;
//if the blocker hit the top or bottom, reverse direction
if(blocker.start.y < 0 || blocker.end.y > screenHeight)
blockerVelocity *= -1;
//if the target hit the top or bottom, reverse direction
if(target.start.y < 0 || target.end.y > screenHeight)
targetVelocity *= -1;
timeLeft -= interval; //subtract from time left
//if the trimer reached zero
if(timeLeft <= 0){
timeLeft = 0.0;
gameOver = true; //the game is over
cannonThread.setRunning(false);
showGameOverDialog(R.string.lose); //show the losing dialog
} //end if
} //end method updatePositions
//fires a cannonball
public void fireCannonball(MotionEvent event){
if(cannonballOnScreen) //if a cannonball is already on the screen
return; //do nothing
double angle = alignCannon(event); //get the cannon barrel's angle
//move the cannonball to be inside the cannon
cannonball.x = cannonballRadius; //align x-coordinate with cannon
cannonball.y = screenHeight / 2; //centers ball vertically
//get the x component of the total velocity
cannonballVelocityX = (int) (cannonballSpeed * Math.sin(angle));
//get the y component of the total velocity
cannonballVelocityY = (int)(-cannonballSpeed * Math.cos(angle));
cannonballOnScreen = true; //the cannonball is on the screen
++shotsFired; //increment shots fired
//play cannon fired sound
soundPool.play(soundMap.get(CANNON_SOUND_ID), 1, 1, 1, 0, 1f);
} //end method fireCannonball
//aligns the cannon in response to a user touch
public double alignCannon(MotionEvent event){
//get the location of the touch in this view
Point touchPoint = new Point((int) event.getX(), (int) event.getY());
//compute the touch's distance from the center of the screen on the Y axis
double centerMinusY = (screenHeight / 2 - touchPoint.y);
double angle = 0; //initialize angle to 0
//calculate the angle the barrel makes with the horizontal
if(centerMinusY != 0) //prevent division by 0
angle = Math.atan((double) touchPoint.x / centerMinusY);
//if the touch is on the lower half of the screen
if(touchPoint.y > screenHeight / 2)
angle += Math.PI; //adjust the angle
//calculate the endpoint of the cannon barrel
barrelEnd.x = (int) (cannonLength * Math.sin(angle));
barrelEnd.y = (int) (-cannonLength * Math.cos(angle) + screenHeight / 2);
return angle; //return the computed angle
} //end method alignCannon
//draws the game to the given Canvas
public void drawGameElements(Canvas canvas){
//clear the background
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), backgroundPaint);
//display the time remaining
canvas.drawText(getResources().getString(R.string.time_remaining_format, timeLeft), 30, 50, textPaint);
//if a cannonball is currently on the screen, draw it
if(cannonballOnScreen)
canvas.drawCircle(cannonball.x, cannonball.y, cannonballRadius, cannonballPaint);
//draw the cannon barrel
canvas.drawLine(0, screenHeight / 2, barrelEnd.x, barrelEnd.y, cannonPaint);
//draw the cannon base
canvas.drawCircle(0, (int) screenHeight / 2, (int) cannonBaseRadius, cannonPaint);
//draw the blocker
canvas.drawLine(blocker.start.x, blocker.start.y, blocker.end.x, blocker.end.y, blockerPaint);
Point currentPoint = new Point(); //start of current target section
//initialize curPoint to the starting point of the target
currentPoint.x = target.start.x;
currentPoint.y = target.start.y;
//draw the target
for (int i = 1; i <= TARGET_PIECES; ++i){
//if this target piece is not hit, draw it
if(!hitStates[i - 1]){
//alternate coloring the pieces yellow and blue
if(i % 2 == 0)
targetPaint.setColor(Color.YELLOW);
else
targetPaint.setColor(Color.BLUE);
canvas.drawLine(currentPoint.x, currentPoint.y, target.end.x,
(int) (currentPoint.y + pieceLength), targetPaint);
} //end if
//move curPoint to the start of the next piece
currentPoint.y += pieceLength;
} //end for
} //end method drawGameElements
//display an AlertDialog when the game ends
private void showGameOverDialog(int messageId){
//create a dialog dislaying the given String
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getContext());
dialogBuilder.setTitle(getResources().getString(messageId));
dialogBuilder.setCancelable(false);
//display number of shots fired an total time elapsed
dialogBuilder.setMessage(getResources().getString(R.string.results_format, shotsFired, totalTimeElapsed));
dialogBuilder.setPositiveButton(R.string.reset_game,
new DialogInterface.OnClickListener() {
//called when "Reset Game" Button is pressed
#Override
public void onClick(DialogInterface dialog, int which) {
dialogIsDisplayed = false;
newGame(); //set up and start a new game
} //end method onClick
} //end anonymous inner class
); //end call to setPositiveButton
activity.runOnUiThread(
new Runnable() {
public void run(){
dialogIsDisplayed = true;
dialogBuilder.show(); //display the dialog
} //end run
}//end runnable
); //end call to runOnUiThread
} //end method showGameOverDialog
//pauses the game
public void stopGame(){
if(cannonThread != null)
cannonThread.setRunning(false);
} //end method stopGame
//releases reources; called by CannonGame's onDestroy method
public void releaseResources(){
soundPool.release(); //release all resource used by the SoundPool
soundPool = null;
} //end method releaseResources
//called when surface changes size
#Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height){
} //end method surfaceChanged
//called when surface is first created
#Override
public void surfaceCreated(SurfaceHolder holder){
cannonThread = new CannonThread(holder);
cannonThread.setRunning(true);
cannonThread.start(); //start the game loop thread
} //end method surfaceCreated
//called when surface is destroyed
#Override
public void surfaceDestroyed(SurfaceHolder holder){
//ensure that thread terminates properly
boolean retry = true;
cannonThread.setRunning(false);
while(retry){
try{
cannonThread.join();
retry = false;
} //end try
catch(InterruptedException e){
} //end catch
} //end while
} //end method surfaceDestroyed
//Thread subclass to control the game loop
private class CannonThread extends Thread{
private SurfaceHolder surfaceHolder; //for manipulating canvas
private boolean threadIsRunning = true; //running by default
//initialize the surface holder
public CannonThread(SurfaceHolder holder){
surfaceHolder = holder;
setName("CannonThread");
} //end constructor
//changes the running state
public void setRunning(boolean running){
threadIsRunning = running;
} //end method setRunning
//controls the game loop
#Override
public void run(){
Canvas canvas = null; //used for drawing
long previousFrameTime = System.currentTimeMillis();
while(threadIsRunning){
try{
canvas = surfaceHolder.lockCanvas(null);
//lock the surafceHolder for drawing
synchronized(surfaceHolder){
long currentTime = System.currentTimeMillis();
double elapsedTimeMS = currentTime - previousFrameTime;
totalTimeElapsed += elapsedTimeMS / 1000.0;
updatePositions(elapsedTimeMS); //update game stats
drawGameElements(canvas); //draw
previousFrameTime = currentTime; //updateprevious time
} //end synchronized block
} //end try
finally{
if(canvas != null)
surfaceHolder.unlockCanvasAndPost(canvas);
} //end finally
} //end while
} //end method run
} //end nested class CannonThread
} //end class CannonView
Finally, here is the xml constraint that my custom view uses:
<?xml version="1.0" encoding="utf-8"?>
<com.deitel.cannongame.CannonView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="#+id/cannonView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#android:color/white"/>
I should mention that I tried the book's example files off their website, and I get the exact same problem.
Change the xml
android:background="#android:color/transparent"
It is likely that you are being burned by overzealous scaling by your fancy android. I would recommend that you use this:
in your manifest. Just put it after your Application information, like this:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.yourstuff.app"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="10"
android:targetSdkVersion="15" />
<application android:label="#string/app_name" android:icon="#drawable/r">
<activity android:name=".LandingActivity"
android:theme="#android:style/Theme.NoTitleBar.Fullscreen"
android:launchMode="singleTask"
android:label="#string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".yourOtherActivity"
android:theme="#android:style/Theme.NoTitleBar.Fullscreen"
/>
</application>
<supports-screens android:anyDensity="true" />
</manifest>