I'm trying to animate a ShapeDrawable in a custom view. But I am not sure what the best method is to accomplish this task.
Should I try and draw a pawn on a path and call invalidate() until it has reached the destination square? Or is there some better method using maybe an AsyncTask or Handler?
Here is my code, I have omitted a lot of methods and variables in order to make it readable.
public class CheckerBoard extends View {
public enum State implements Parcelable {
EMPTY(0), WHITE(1), BLACK(2);
}
private final State[][] boardStates = new State[SIZE][SIZE];
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(bgColor);
for (int y = 0; y < SIZE; y++) {
for (int x = 0; x < SIZE; x++) {
if ((y % 2 == 0 && x % 2 != 0) || (y % 2 != 0 && x % 2 == 0)) {
drawRect(x, y, canvas);
drawPawn(x, y, canvas);
}
}
}
}
private void drawRect(int x, int y, Canvas c) {
}
private void drawPawn(int x, int y, Canvas c) {
}
private void init() {
setupBoard();
pawnLinePaint.setStyle(Paint.Style.STROKE);
wPawnDrawable.getPaint().setColor(wColor);
wPawnDrawable.getPaint().setShadowLayer(tileSize + 2, 4, 4, Color.GRAY);
bPawnDrawable.getPaint().setColor(bColor);
bPawnDrawable.getPaint().setShadowLayer(tileSize + 2, 4, 4, Color.GRAY);
playerState = startState;
}
public boolean onTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
int x = (int) (event.getX() / tileSize);
int y = (int) (event.getY() / tileSize);
if (selection[0] >= 0) { // A tile is already selected
if (isValidMove(selection[0], selection[1], x, y)) {
makeMove(x, y);
clearSelection();
switchPlayer();
invalidate();
}
} else { // New selection
if (isValidSelection(x, y)) {
selection[0] = x;
selection[1] = y;
invalidate();
}
}
return true;
default:
return super.onTouchEvent(event);
}
}
private void makeMove(int x, int y) {
// Move the pawn to the new square
boardStates[y][x] = boardStates[selection[1]][selection[0]];
// Old square is now empty
boardStates[selection[1]][selection[0]] = State.EMPTY;
}
private void switchPlayer() {
playerState = playerState == State.WHITE ? State.BLACK : State.WHITE;
}
public CheckerBoard(Context context) {
super(context);
init();
}
public CheckerBoard(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CheckerBoard(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private class Pawn extends ShapeDrawable {
public Pawn() {
super(new OvalShape());
}
public void drawWithCircles(Canvas canvas, float x, float y){
super.draw(canvas);
canvas.drawCircle(x * tileSize + pawnDiameter, y * tileSize + pawnDiameter, pawnDiameter - pawnPadding,
pawnLinePaint);
canvas.drawCircle(x * tileSize + pawnDiameter, y * tileSize + pawnDiameter, pawnDiameter - pawnPadding * 6,
pawnLinePaint);
canvas.drawCircle(x * tileSize + pawnDiameter, y * tileSize + pawnDiameter, pawnDiameter - pawnPadding * 8,
pawnLinePaint);
}
}
}
Thank you for your help.
Blight
You should create two threads for your application. One thread is the UI thread that only draws the board in its current state. The other thread is the Game engine or animation thread that moves the items on the board.
The first thread runs at whatever your desired frame rate is and the 2nd thread should run considerably faster. This way you don't actually have to handle the animation yourself as the UI thread just draws the board as it currently is. In your engine thread you update the state of the game,board,chess pieces, every cycle of the thread.
Doing things this way has a couple of benefits. First your game's framerate won't drop if the Engine thread gets bogged down in some sort of computation. Second it allows you to abstract the drawing away from the game in a way that will make debugging much easier.
Take a progress bar for example. Let say you tried to create a file uploader with a progress bar but only had one thread. So you start the progress bar then start uploading the file. If the upload process is blocking then you have to wait for the file to finish uploading before you can update the progress bar, essentially rendering the progress bar useless. But if you did this with two threads then you could set it up so one thread simply updates the progress bars graphics based upon some common variable. The other tread is responsible for performing an action and updating the progress variable.
Check out these links for more info:
http://obviam.net/index.php/the-android-game-loop/
http://www.rbgrn.net/content/54-getting-started-android-game-development
http://www.helloandroid.com/tutorials/using-threads-and-progressdialog
Related
I was trying to learn how to use SurfaceView, since I need to use it in a future project. I read that if I need to update the UI quickly, it is preferable to use SurfaceView with the help of a thread.
The purpose of my code is to be able to print on a bitmap, the pattern of a 'B',here is the code:
public void testTransition(Canvas canvas) {
if (canvas != null) {
canvas.drawColor(Color.BLACK);
Log.i("Thread", "Running...");
for (int y = 0; y < 8; y++) {
if (test[y] == 1) {
canvas.drawBitmap(ledBitMap, (x - diameterLeds) / 2, diameterLeds * y, paint);
}
}
canvas.drawColor(Color.BLACK);
delay(1000);
for (int y=0; y<8; y++){
if(test[y + 8] == 1)
canvas.drawBitmap(ledBitMap, (x - diameterLeds) / 2,diameterLeds * y, paint);
}
canvas.drawColor(Color.BLACK);
//ledThread.delay(1000);
for (int y=0; y<8; y++){
if(test[y + 16] == 1)
canvas.drawBitmap(ledBitMap, (x- diameterLeds) / 2,diameterLeds * y, paint);
}
canvas.drawColor(Color.BLACK);
//ledThread.delay(1000);
for (int y=0; y<8; y++){
if(test[y + 24] == 1)
canvas.drawBitmap(ledBitMap, (x - diameterLeds) / 2,diameterLeds * y, paint);
}
canvas.drawColor(Color.BLACK);
//ledThread.delay(1000);
for (int y=0; y<8; y++){
if(test[y + 32] == 1)
canvas.drawBitmap(ledBitMap, (x - diameterLeds) / 2,diameterLeds * y, paint);
}
//canvas.drawColor(Color.BLACK);
//ledThread.delay(600);
//delay(1);
}
}
#Override
public void run() {
long ticksPS = 1000 / 30;
long startTime;
long sleepTime;
while(isRunning){
Canvas canvas = null;
startTime = System.currentTimeMillis();
try {
canvas = ledView.getHolder().lockCanvas();
synchronized (ledView.getHolder()){
testTransition(canvas);
}
}finally {
if(canvas != null){
ledView.getHolder().unlockCanvasAndPost(canvas);
}
}
}
}
The variable test[] is a matrix of 40 elements ( 0 or 1)
private static int test[] = {1,1,1,1,1,1,1,1, 1,0,0,1,0,0,0,1, 1,0,0,1,0,0,0,1, 1,0,0,1,0,0,0,1, 0,1,1,0,1,1,1,0};
My goal is to read this arrangement and simulate that 'on or off' of the pattern of the letter B. But when I execute the code, the activity stays a few seconds with the blank screen and after that the circles appear painted on the bitmap but does not alternate the sequence, although the thread seems to be running. Only the last 8 elements of the test array are drawn
With a SurfaceView and lockCanvas(), the screen is not able to update until after you call unlockCanvasAndPost(). (I think you were expecting the screen to update midway through testTransition().)
The solution is to implement a state machine, that dirties the SurfaceView based on a timer (for example), and advances the LED display one row on each iteration.
I am trying to display a ExoPlayerView inside a circle, overlaying another ExoPlayer (picture in picture):
I have tried putting the 2nd player inside a frame with rounded corners (both this answer and this one) but the player will always escape the parent frame and draw the video's full rectangle.
I found this solution which uses a GLSurfaceView, however this solution uses the classic MediaPlayer and not ExoPlayer.
For the one that's supposed to have rounded corners, you can set in the layout XML file this about it:
app:surface_type="texture_view"
Found this solution here.
The drawback of using this is mainly performance and battery usage (written here) :
Should I use SurfaceView or TextureView? SurfaceView has a number of
benefits over TextureView for video playback:
Significantly lower power consumption on many devices. More accurate
frame timing, resulting in smoother video playback. Support for secure
output when playing DRM protected content. SurfaceView should
therefore be preferred over TextureView where possible. TextureView
should be used only if SurfaceView does not meet your needs. One
example is where smooth animations or scrolling of the video surface
is required prior to Android N (see How do I get smooth
animation/scrolling of video?). For this case, it’s preferable to use
TextureView only when SDK_INT is less than 24 (Android N) and
SurfaceView otherwise.
You need to create a custom container for it. try this and put you player view in it.
public class RoundFrameLayout extends FrameLayout {
private final Path clip = new Path();
private int posX;
private int posY;
private int radius;
public RoundFrameLayout(Context context) {
this(context,null);
}
public RoundFrameLayout(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public RoundFrameLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
// We can use outlines on 21 and up for anti-aliased clipping.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setClipToOutline(true);
}
}
#Override
protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
posX = Math.round((float) width / 2);
posY = Math.round((float) height / 2);
// noinspection NumericCastThatLosesPrecision
radius = (int) Math.floor((float) Math.min(width, height) / 2);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
setOutlineProvider(new OutlineProvider(posX, posY, radius));
} else {
clip.reset();
clip.addCircle(posX, posY, radius, Direction.CW);
}
}
#Override
protected void dispatchDraw(Canvas canvas) {
// Not needed on 21 and up since we're clipping to the outline instead.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
canvas.clipPath(clip);
}
super.dispatchDraw(canvas);
}
#Override
public boolean onInterceptTouchEvent(MotionEvent event) {
// Don't pass touch events that occur outside of our clip to the children.
float distanceX = Math.abs(event.getX() - posX);
float distanceY = Math.abs(event.getY() - posY);
double distance = Math.hypot(distanceX, distanceY);
return distance > radius;
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
static class OutlineProvider extends ViewOutlineProvider {
final int left;
final int top;
final int right;
final int bottom;
OutlineProvider(int posX, int posY, int radius) {
left = posX - radius;
top = posY - radius;
right = posX + radius;
bottom = posY + radius;
}
#Override
public void getOutline(View view, Outline outline) {
outline.setOval(left, top, right, bottom);
}
}
}
I'd like to be able to create an arbitrary input for time into a standard Android animation. Instead of an animation running for 1 second, I want for instance the input to be a coordinate from user touch input. This way I could for instance create a circle motion of object A when the position in the circular motion is defined by a linear input on slide A.
Crude illustration:
Now I'm thinking this could be achieved with defining the translation animation in XML just as with regular animations under /res/anim, but overriding the time input to come from a user input control instead. It minght also be done with a custom interpolator, I'm not sure. I don't what a set start and end time of the animation, in any case.
Anyone have any suggestions on how to achieve this?
edit: To further answer a couple of the comments: Think if it as the user slides/drags the blue dot. No interpolation between the input occurs. As soon as the user lifts the finger, the "animation" stops.
If I understand correctly you need some sort of 'rigging' - Defining a movement of one element as a function of another. In your case this function needs to transform the the linear position into a circular position.
There is no animation involved - When the user moves the blue circle, the red one is moved accordingly.
You should register for callbacks for the blue circle movement (i.e. onTouchEvent, or a seekBar's on change, depending on how you implement your 'bar'). Then you calculate the new position of the red circle and then you put it there.
Here's a simple working example of a custom view that draws two circles according to a given percentValue. I tested using s simple SeekBar and it works:
public class CanvasView extends View {
private int centerX = 0;
private int centerY = 0;
private int radius = 0;
private final int handleRadius = 25;
private final Paint circlePaint = new Paint();
private final Paint handlePaint = new Paint();
private float percentValue = 0f;
public CanvasView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
public CanvasView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CanvasView(Context context) {
super(context);
init();
}
private void init() {
circlePaint.setColor(Color.BLACK);
handlePaint.setColor(Color.RED);
}
// Call this whenever the value of that linear bar is changed - so when the user moves his finger etc.
public void setValue(float percentage) {
this.percentValue = percentage;
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// this is the main circle
canvas.drawCircle(centerX, centerY, radius, circlePaint);
// calculate the angle based on the percentage of the linear movement (substracting (pi/2) so the zero value is on top)
double angle = (percentValue / 100) * (2 * Math.PI) - Math.PI / 2;
// sin and cos to calculate the position of the smaller circle - the 'handle'
float handleX = centerX + (float) (radius * Math.cos(angle));
float handleY = centerY + (float) (radius * Math.sin(angle));
// drawing the circle
canvas.drawCircle(handleX, handleY, handleRadius, handlePaint);
}
#Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// choose whatever values you want here, based on the view's size:
centerX = w / 2;
centerY = h / 2;
radius = w / 3;
}
}
I'm trying to develop simple board game. The board is of size 9x9 fields. The balls are appearing on the fields and when the user clicks on the field with the ball, the ball starts to jumping. I implemented the animation in two ways. The first one is working, but it's not easy to add another one following animation (like little stretch or something). And the second one, which seems to be better (there is used the AnimatorSet) is not working. When user clicks on the field with the ball, the ball disappears. I have no idea why :-(.
The first class implements the board and it is the child of View:
public class BoardView extends View {
...
/**
* Initializes fields of the board.
*/
private void initializeFields() {
this.fields = new ArrayList<Field>();
for (int row = 0; row < BoardView.FIELDS_NUMBER; row++) {
for (int column = 0; column < BoardView.FIELDS_NUMBER; column++) {
this.fields.add(new Field(this, row, column));
}
}
}
#Override
protected void onDraw(Canvas canvas) {
canvas.drawColor(BoardView.COLOR_ACTIVITY);
if (this.fields == null) {
this.initializeFields();
}
for (int i = 0; i < this.fields.size(); i++) {
this.fields.get(i).draw(canvas);
}
}
...
}
The second one implements the field:
public class Field {
...
/**
* Draws itself on the screen.
*
* #param Canvas canvas
*/
public void draw(Canvas canvas) {
Rect field = this.getRect();
int round = (int)Math.floor(this.board.getFieldSize() / 4);
this.board.getPainter().setStyle(Paint.Style.FILL);
this.board.getPainter().setColor(Field.COLOR_DEFAULT);
// draw field
canvas.drawRoundRect(new RectF(field), round, round, this.board.getPainter());
// draw selected field
if (this.selected) {
this.board.getPainter().setColor(Field.COLOR_SELECTED);
canvas.drawRoundRect(new RectF(field), round, round, this.board.getPainter());
}
// draw ball
if (this.ball != null) {
Point fieldOrigin = new Point(field.left, field.top);
if (this.selected) {
this.ball.animate(canvas, fieldOrigin);
} else {
this.ball.draw(canvas, fieldOrigin);
}
}
}
...
}
And the last one implements the ball:
Here is the first method, which completely works, but it's not flexible enough:
public class Ball {
...
/**
* Draws itself on the screen.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void draw(Canvas canvas, Point fieldOrigin) {
// set painter
Paint painter = this.field.getBoard().getPainter();
painter.setStyle(Paint.Style.FILL);
painter.setColor(Ball.COLORS[this.color]);
// calculate parameters
float halfSize = this.field.getBoard().getFieldSize() / 2;
float cX = fieldOrigin.x + halfSize;
float cY = fieldOrigin.y + halfSize + this.dy;
float radius = 0.6f * halfSize;
// draw circle
canvas.drawCircle(cX, cY, radius, painter);
// the code continues, because of the shadow and light simulation (radial gradients)
}
/**
* Draws jumping animation.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void animate(Canvas canvas, Point fieldOrigin) {
float currentDy = (this.dy - 0.1f);
this.setDy((float)Math.abs(Math.sin(currentDy)) * (-0.15f * this.field.getBoard().getFieldSize()));
this.draw(canvas, fieldOrigin);
this.setDy(currentDy);
try {
Thread.sleep(Ball.ANIMATION_DELAY);
} catch (InterruptedException e) {}
this.field.invalidate();
}
...
}
As you can see, the animation is implemented by sleeping the current Thread and changing parameter dy.
The second method is showing the ball on the field, but the animation is not working as I said in the beginning of the post (after click, the ball disappears):
public class BallShape {
private Field field;
private LayerDrawable ball;
private int color;
private float diameter,
x, y; // top left corner - THE GETTERS AND SETTERS ARE IMPLEMENTED (because of Animator)
...
/**
* Initializes the ball.
*
* #param Field field
* #param int color
*/
public BallShape(Field field, int color) {
this.field = field;
this.color = ((color == Ball.COLOR_RANDOM) ? Ball.randomColor() : color);
// create ball
float halfSize = this.field.getBoard().getFieldSize() / 2;
this.diameter = 0.6f * field.getBoard().getFieldSize();
float radius = this.diameter / 2;
Rect fieldArea = field.getRect();
this.x = fieldArea.left + halfSize - radius;
this.y = fieldArea.top + halfSize - radius;
// color circle
OvalShape circleShape = new OvalShape();
circleShape.resize(this.diameter, this.diameter);
ShapeDrawable circle = new ShapeDrawable(circleShape);
this.initPainter(circle.getPaint());
// the code continues, because of the shadow and light simulation (radial gradients)
// compound shape - ball
ShapeDrawable[] compound = { circle };//, shadow, light };
this.ball = new LayerDrawable(compound);
}
/**
* Draws itself on the screen.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void draw(Canvas canvas, Point fieldOrigin) {
canvas.save();
canvas.translate(this.x, this.y);
this.ball.draw(canvas);
canvas.restore();
}
/**
* Draws jumping animation.
*
* #param Canvas canvas
* #param Point fieldOrigin
*/
public void animate(Canvas canvas, Point fieldOrigin) {
// common data
float halfSize = this.field.getBoard().getFieldSize() / 2;
float radius = this.diameter / 2;
float startY = fieldOrigin.y + halfSize - radius;
float endY = startY - halfSize + 2;
// bounce animation
ValueAnimator bounceAnimation = ObjectAnimator.ofFloat(this, "y", startY, endY);
bounceAnimation.setDuration(BallShape.ANIMATION_LENGTH);
bounceAnimation.setInterpolator(new AccelerateInterpolator());
bounceAnimation.setRepeatCount(ValueAnimator.INFINITE);
bounceAnimation.setRepeatMode(ValueAnimator.REVERSE);
//bounceAnimation.start();
// animation
AnimatorSet bouncer = new AnimatorSet();
bouncer.play(bounceAnimation);
// start the animation
bouncer.start();
}
...
}
Any idea why it's not working? What I've done wrong?
Thank you very, very much.
Two things I would fix.
First of all you start animation in draw() method. You should either start it in onClick() or at least set this.selected to false, to not start it on every draw(). Secondly, after your value animator changes a property, you need to redraw the BallShape. Otherwise nothing will change. For instance you can define setY(float Y) method, change Y there and call invalidate().
I face the following issue - come of the people that downloaded my new space simulation game (but not all) are complaining, that they do not see any controls on their devices. The problem seems to be present on S3 devices only. I am with S3 myself, and everything is appearing as designed.
The controls are implemented as custom radio buttons, I will post the code below, however my question is:
What could cause such different behavior?
public class CenteredRadioButton extends RadioButton {
private Drawable buttonDrawable;
public CenteredRadioButton(Context context, AttributeSet attrs) {
super(context, attrs);
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompoundButton, 0, 0);
buttonDrawable = a.getDrawable(1);
setButtonDrawable(R.drawable.radio_empty);
}
public void setCustomDrawable(Drawable drawable) {
buttonDrawable = drawable;
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (buttonDrawable != null) {
buttonDrawable.setState(getDrawableState());
final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
final int height = buttonDrawable.getIntrinsicHeight();
int y = 0;
switch (verticalGravity) {
case (Gravity.BOTTOM) : {
y = getHeight() - height;
break;
} case (Gravity.CENTER_VERTICAL) : {
y = (getHeight() - height) / 2;
break;
}
}
int buttonWidth = buttonDrawable.getIntrinsicWidth();
int buttonLeft = (getWidth() - buttonWidth) / 2;
buttonDrawable.setBounds(buttonLeft, y, buttonLeft + buttonWidth, y + height);
buttonDrawable.draw(canvas);
}
}
}
It turned out, that GIF image format is not entirely supported in some Android versions, for more information you could take a look at the thread below and a big THANK YOU to Rich for solving the issue!
Images not loading on Galaxy S3