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.
Related
So this bit of code seems to be the problem:
public void surfaceCreated(final SurfaceHolder sh) {
loop = new Loop();
loop.setMethod(new LoopMethod() {
public void callMethod() {
Canvas canvas = sh.lockCanvas();
synchronized(sh) {
draw(canvas);
}
sh.unlockCanvasAndPost(canvas);
}
});
loop.setRunning(true);
loop.start();
}
This works fine up until the user backs out of the application, or opens up the Recent Items tray. Upon which I get the following error:
java.lang.IllegalStateException: Surface has already been released.
which points to this line:
sh.unlockCanvasAndPost(canvas);
Why is this occurring and how do I fix it?
EDIT: ENTIRE PROBLEMATIC CLASS
public class SplashScreen extends SurfaceView implements SurfaceHolder.Callback {
private SplashActivity splashActivity;
private Loop loop;
//Loading gif info
private Movie loadingGif;
private long gifStart;
private int gifW, gifH;
private boolean gifDone;
public SplashScreen(Context context, SplashActivity splashActivity) {
super(context);
getHolder().addCallback(this);
this.splashActivity = splashActivity;
//Load gif in
InputStream gifStream = context.getResources().openRawResource(+ R.drawable.load);
loadingGif = Movie.decodeStream(gifStream);
gifW = loadingGif.width();
gifH = loadingGif.height();
gifDone = false;
}
private void beginLoading() {
}
public void surfaceCreated(final SurfaceHolder sh) {
loop = new Loop();
loop.setMethod(new LoopMethod() {
public void callMethod() {
Canvas canvas = sh.lockCanvas();
synchronized(sh) {
draw(canvas);
}
sh.unlockCanvasAndPost(canvas);
}
});
loop.setRunning(true);
loop.start();
}
public void draw(Canvas canvas) {
if(canvas != null) {
super.draw(canvas);
canvas.save();
canvas.drawColor(Color.rgb(69, 69, 69));
Paint p = new Paint();
p.setAntiAlias(true);
long now = android.os.SystemClock.uptimeMillis();
if(gifStart == 0)
gifStart = now;
if(loadingGif != null) {
int dur = loadingGif.duration();
if(!gifDone) {
int relTime = (int) ((now - gifStart) % dur);
loadingGif.setTime(relTime);
}
else
loadingGif.setTime(dur);
if(now - gifStart > dur && !gifDone) {
gifDone = true;
loadingGif.setTime(dur);
beginLoading();
}
float scaleX = getWidth()/gifW*1f;
float scaleY = getHeight()/gifH*1f;
float centerX = getWidth()/2/scaleX - gifW/2;
float centerY = getHeight()/2/scaleY - gifH/2;
canvas.scale(scaleX, scaleY);
loadingGif.draw(canvas, centerX, centerY, p);
canvas.restore();
p.setColor(Color.WHITE);
p.setTypeface(Typeface.create("Segoe UI", Typeface.BOLD));
p.setTextSize(UsefulMethods.spToPx(12));
String s = "Version " + BuildConfig.VERSION_NAME;
canvas.drawText(s, 10, getHeight() - 10, p);
}
}
else
System.out.println("not drawing :(");
}
public void surfaceDestroyed(SurfaceHolder sh) {
loop.setRunning(false);
}
public void surfaceChanged(SurfaceHolder sh, int format, int width, int height) {
System.out.println("call");
}
}
Check if visibility has changed, if it has pause/resume loop:
public void onVisibilityChanged(View view, int visibility) {
super.onVisibilityChanged(view, visibility);
if(loop != null)
if(visibility == VISIBLE)
if(!loop.isRunning())
loop.setRunning(true);
else;
else if(visibility == INVISIBLE)
if(loop.isRunning())
loop.setRunning(false);
}
In my case I covered sh.unlockCanvasAndPost with
if(sh.getSurface().isValid()){
sh.unlockCanvasAndPost(canvas);
}
If we will check isValid() source will see:
Returns true if this object holds a valid surface.
#return True if it holds a physical surface, so lockCanvas() will succeed.Otherwise returns false.
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.
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've made a custom pie chart view that I want to animate starting when the pie chart is visible. Currently what I have is the pie chart animating but by the time you can actually see it on the screen the animation is half over. This is what I have:
public class SinglePieChart extends SurfaceView implements SurfaceHolder.Callback {
// Chart setting variables
private int emptyCircleCol, strokeColor, number, total;
// Paint for drawing custom view
private Paint circlePaint;
private RectF rect;
private Context context;
private AnimThread animThread;
private SurfaceHolder holder;
// animation variables
private float speed;
private float current = 0.0f;
private boolean percentsCalculated = false;
private float degree;
private int viewWidth, viewHeight;
public SinglePieChart(Context ctx, AttributeSet attrs) {
super(ctx, attrs);
context = ctx;
// Paint object for drawing in doDraw
circlePaint = new Paint();
circlePaint.setStyle(Style.STROKE);
circlePaint.setStrokeWidth(3);
circlePaint.setAntiAlias(true);
circlePaint.setDither(true);
rect = new RectF();
//get the attributes specified in attrs.xml using the name we included
TypedArray a = context.getTheme().obtainStyledAttributes(attrs,
R.styleable.DashboardChartSmall, 0, 0);
try {
//get the colors specified using the names in attrs.xml
emptyCircleCol = a.getColor(R.styleable.DashboardChartSmall_smCircleColor, 0xFF65676E); // light gray is default
strokeColor = a.getColor(R.styleable.DashboardChartSmall_smColor, 0xFF39B54A); // green is default
// Default number values
total = a.getInteger(R.styleable.DashboardChartSmall_smTotal, 1);
number = a.getInteger(R.styleable.DashboardChartSmall_smNumber, 0);
} finally {
a.recycle();
}
this.setZOrderOnTop(true);
holder = getHolder();
holder.setFormat(PixelFormat.TRANSPARENT);
holder.addCallback(this);
}
protected void calculateValues() {
degree = 360 * number / total;
percentsCalculated = true;
speed = 10 * number / total;
viewWidth = this.getMeasuredWidth();
viewHeight = this.getMeasuredHeight();
float top, left, bottom, right;
if (viewWidth < viewHeight) {
left = 4;
right = viewWidth - 4;
top = ((viewHeight - viewWidth) / 2) + 4;
bottom = viewHeight - top;
} else {
top = 4;
bottom = viewHeight - 4;
left = ((viewWidth - viewHeight) / 2) + 4;
right = viewWidth - left;
}
rect.set(left, top, right, bottom);
}
protected void doDraw(Canvas canvas) {
if (total == 0) {
// Number values are not ready
animThread.setRunning(false);
return;
}
if (!percentsCalculated) {
calculateValues();
}
// set the paint color using the circle color specified
float last = current;
float start = -90;
circlePaint.setColor(strokeColor);
canvas.drawArc(rect, start, (last > degree) ? degree : last, false, circlePaint);
start += (last > number) ? number : last;
last = (last < number) ? 0 : last - number;
circlePaint.setColor(emptyCircleCol);
if (current > 360) {
current = 360;
}
canvas.drawArc(rect, start, 360 - current, false, circlePaint);
current += speed;
if (last > 0 || number == 0) {
// we're done
animThread.setRunning(false);
}
}
public void setNumbers(int num, int tot) {
number = num;
total = tot;
invalidate();
requestLayout();
}
public void setColor(int col) {
strokeColor = col;
}
public void redraw() {
calculateValues();
animThread.setRunning(true);
invalidate();
requestLayout();
}
#Override
public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {
}
#Override
public void surfaceCreated(SurfaceHolder arg0) {
animThread = new AnimThread(holder, context, this);
animThread.setRunning(true);
animThread.start();
}
#Override
public void surfaceDestroyed(SurfaceHolder arg0) {
animThread.setRunning(false);
boolean retry = true;
while(retry) {
try {
animThread.join();
retry = false;
} catch(Exception e) {
Log.v("Exception Occured", e.getMessage());
}
}
}
public class AnimThread extends Thread {
boolean mRun;
Canvas mcanvas;
SurfaceHolder surfaceHolder;
Context context;
SinglePieChart msurfacePanel;
public AnimThread(SurfaceHolder sholder, Context ctx, SinglePieChart spanel) {
surfaceHolder = sholder;
context = ctx;
mRun = false;
msurfacePanel = spanel;
}
void setRunning(boolean bRun) {
mRun = bRun;
}
#Override
public void run() {
super.run();
while (mRun) {
mcanvas = surfaceHolder.lockCanvas();
if (mcanvas != null) {
msurfacePanel.doDraw(mcanvas);
surfaceHolder.unlockCanvasAndPost(mcanvas);
}
}
}
}
}
Also if you see any programming errors, memory leaks, poor performing code, please let me know. I'm new to Android.
Here is the layout that uses the SinglePieChart class:
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.davidscoville.vokab.views.elements.SinglePieChart
android:id="#+id/smallPieChart"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<TextView
android:id="#+id/dashSmNumber"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:textSize="25sp"
android:textColor="#FFFFFF" />
</RelativeLayout>
<TextView
android:id="#+id/dashSmLabel"
android:layout_width="match_parent"
android:layout_height="20dp"
android:textSize="14sp"
android:gravity="center"
android:textColor="#FFFFFF" />
</merge>
Alright I'm going with the my pie chart won't automatically animate and it will have a new function that the Activity will trigger to start animating once it's ready. I wish there was an easier way...
Alternatively you can use the animation framework(or nine old androids if you want to support older apis). This will allow you to animate properties on your view, in your case the start and current variables.
I'd set this to happen during onAttachedToWindow.
Note if you aren't doing a lot of other things in this pie chart a surfaceview might be overkill for your needs.
I have a surface view, and whenever I touch the screen, an image comes up in the spot I touch. It's great, but I cannot figure out how to put a background on the SurfaceView. I have tried using the OnDraw to draw a background right away (Without having to touch it), and that only works some of the time. It force closes most of the time.
Would anyone want to look at my code and see if it's possible to get a background image on the Surface view? Thanks in advance.
class MyView extends SurfaceView implements SurfaceHolder.Callback {
private Thready _thread;
private ArrayList _graphicsz = new ArrayList();
private GraphicObject _currentGraphic = null;
public MyView(Context context) {
super(context);
getHolder().addCallback(this);
_thread = new Thready(getHolder(), this);
setFocusable(true);
}
#Override
public boolean onTouchEvent(MotionEvent event) {
synchronized (_thread.getSurfaceHolder()) {
GraphicObject graphic = null;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
graphic = new GraphicObject(BitmapFactory.decodeResource(getResources(), R.drawable.cat1small));
graphic.getCoordinates().setX((int) event.getX() - graphic.getGraphic().getWidth() / 2);
graphic.getCoordinates().setY((int) event.getY() - graphic.getGraphic().getHeight() / 2);
_currentGraphic = graphic;
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
_currentGraphic.getCoordinates().setX((int) event.getX() - _currentGraphic.getGraphic().getWidth() / 2);
_currentGraphic.getCoordinates().setY((int) event.getY() - _currentGraphic.getGraphic().getHeight() / 2);
} else if (event.getAction() == MotionEvent.ACTION_UP) {
_graphicsz.add(_currentGraphic);
_currentGraphic = null;
}
return true;
}
}
#Override
public void onDraw(Canvas canvas) {
canvas.drawColor(Color.BLACK);
Bitmap bitmap1;
GraphicObject.Coordinates coords1;
for (GraphicObject graphic : _graphicsz) {
bitmap1 = graphic.getGraphic();
coords1 = graphic.getCoordinates();
canvas.drawBitmap(bitmap1, coords1.getX(), coords1.getY(), null);
}
// draw current graphic at last...
if (_currentGraphic != null) {
bitmap1 = _currentGraphic.getGraphic();
coords1 = _currentGraphic.getCoordinates();
canvas.drawBitmap(bitmap1, coords1.getX(), coords1.getY(), null);
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// TODO Auto-generated method stub
}
public void surfaceCreated(SurfaceHolder holder) {
_thread.setRunning(true);
_thread.start();
}
public void surfaceDestroyed(SurfaceHolder holder) {
// simply copied from sample application LunarLander:
// we have to tell thread to shut down & wait for it to finish, or else
// it might touch the Surface after we return and explode
boolean retry = true;
_thread.setRunning(false);
while (retry) {
try {
_thread.join();
retry = false;
} catch (InterruptedException e) {
// we will try it again and again...
}
}
}
}
class Thready extends Thread {
private SurfaceHolder _surfaceHolder;
private MyView _panel;
private boolean _run = false;
public Thready(SurfaceHolder surfaceHolder, MyView panel) {
_surfaceHolder = surfaceHolder;
_panel = panel;
}
public void setRunning(boolean run) {
_run = run;
}
public SurfaceHolder getSurfaceHolder() {
return _surfaceHolder;
}
#Override
public void run() {
Canvas c;
while (_run) {
c = null;
try {
c = _surfaceHolder.lockCanvas(null);
synchronized (_surfaceHolder) {
_panel.onDraw(c);
}
} 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 (c != null) {
_surfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
class GraphicObject {
/**
* Contains the coordinates of the graphic.
*/
public class Coordinates {
private int _x = 100;
private int _y = 0;
public int getX() {
return _x + _bitmap.getWidth() / 2;
}
public void setX(int value) {
_x = value - _bitmap.getWidth() / 2;
}
public int getY() {
return _y + _bitmap.getHeight() / 2;
}
public void setY(int value) {
_y = value - _bitmap.getHeight() / 2;
}
public String toString() {
return "Coordinates: (" + _x + "/" + _y + ")";
}
}
private Bitmap _bitmap;
private Coordinates _coordinates;
public GraphicObject(Bitmap bitmap) {
_bitmap = bitmap;
_coordinates = new Coordinates();
}
public Bitmap getGraphic() {
return _bitmap;
}
public Coordinates getCoordinates() {
return _coordinates;
}
}
Well, what strikes me first off all (and what could be causing a lot of problem) is this:
you're creating new bitmap way too often
Every time you get a touch event, you're loading in your bitmaps ... and you can get 80 to 100 touchevents every second!
So to start with, define global bitmaps (private Bitmap bmp1; etc), load them somewhere else and THEN use them in your touch event.
Also, remove the bmp loading/creation from onDraw and move it to somewhere else.
After you've done that, see what happens; you might have more problems (a REALY quick scan of your code seemed fine), but not creating and loading bitmaps 80 times a second will definitely help out :)