How can I use the animation framework inside the canvas?
The Android animation class applies to objects such as views and layouts. The canvas is just a surface for drawing which is either part of a View or linked to a bitmap. In onDraw in a custom view only one frame is drawn at the time until next invalidate is called, which means that you have to draw your animation frame by frame. Here is an example of bouncing ball which rotates, which you may find useful.
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.os.Bundle;
import android.text.format.Time;
import android.view.View;
public class StartActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(new BallBounce(this));
}
}
class BallBounce extends View {
int screenW;
int screenH;
int X;
int Y;
int initialY ;
int ballW;
int ballH;
int angle;
float dY;
float acc;
Bitmap ball, bgr;
public BallBounce(Context context) {
super(context);
ball = BitmapFactory.decodeResource(getResources(),R.drawable.football); //load a ball image
bgr = BitmapFactory.decodeResource(getResources(),R.drawable.sky_bgr); //load a background
ballW = ball.getWidth();
ballH = ball.getHeight();
acc = 0.2f; //acceleration
dY = 0; //vertical speed
initialY = 100; //Initial vertical position.
angle = 0; //Start value for rotation angle.
}
@Override
public void onSizeChanged (int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
screenW = w;
screenH = h;
bgr = Bitmap.createScaledBitmap(bgr, w, h, true); //Resize background to fit the screen.
X = (int) (screenW /2) - (ballW / 2) ; //Centre ball into the centre of the screen.
Y = initialY;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
//Draw background.
canvas.drawBitmap(bgr, 0, 0, null);
//Compute roughly ball speed and location.
Y+= (int) dY; //Increase or decrease vertical position.
if (Y > (screenH - ballH)) {
dY=(-1)*dY; //Reverse speed when bottom hit.
}
dY+= acc; //Increase or decrease speed.
//Increase rotating angle.
if (angle++ >360)
angle =0;
//Draw ball
canvas.save(); //Save the position of the canvas.
canvas.rotate(angle, X + (ballW / 2), Y + (ballH / 2)); //Rotate the canvas.
canvas.drawBitmap(ball, X, Y, null); //Draw the ball on the rotated canvas.
canvas.restore(); //Rotate the canvas back so that it looks like ball has rotated.
//Call the next frame.
invalidate();
}
}
This is just a simple illustration but I would use surfaceView and drive frames from another thread, which is a bit more complicated but a proper way to do when making interactive animations like games etc. Here is an example with a scrolling backround and the user can move the ball with his finger:
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.Bundle;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
public class SurfaceViewActivity extends Activity {
BallBounces ball;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ball = new BallBounces(this);
setContentView(ball);
}
}
class BallBounces extends SurfaceView implements SurfaceHolder.Callback {
GameThread thread;
int screenW; //Device's screen width.
int screenH; //Devices's screen height.
int ballX; //Ball x position.
int ballY; //Ball y position.
int initialY ;
float dY; //Ball vertical speed.
int ballW;
int ballH;
int bgrW;
int bgrH;
int angle;
int bgrScroll;
int dBgrY; //Background scroll speed.
float acc;
Bitmap ball, bgr, bgrReverse;
boolean reverseBackroundFirst;
boolean ballFingerMove;
//Measure frames per second.
long now;
int framesCount=0;
int framesCountAvg=0;
long framesTimer=0;
Paint fpsPaint=new Paint();
//Frame speed
long timeNow;
long timePrev = 0;
long timePrevFrame = 0;
long timeDelta;
public BallBounces(Context context) {
super(context);
ball = BitmapFactory.decodeResource(getResources(),R.drawable.football); //Load a ball image.
bgr = BitmapFactory.decodeResource(getResources(),R.drawable.sky_bgr); //Load a background.
ballW = ball.getWidth();
ballH = ball.getHeight();
//Create a flag for the onDraw method to alternate background with its mirror image.
reverseBackroundFirst = false;
//Initialise animation variables.
acc = 0.2f; //Acceleration
dY = 0; //vertical speed
initialY = 100; //Initial vertical position
angle = 0; //Start value for the rotation angle
bgrScroll = 0; //Background scroll position
dBgrY = 1; //Scrolling background speed
fpsPaint.setTextSize(30);
//Set thread
getHolder().addCallback(this);
setFocusable(true);
}
@Override
public void onSizeChanged (int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
//This event-method provides the real dimensions of this custom view.
screenW = w;
screenH = h;
bgr = Bitmap.createScaledBitmap(bgr, w, h, true); //Scale background to fit the screen.
bgrW = bgr.getWidth();
bgrH = bgr.getHeight();
//Create a mirror image of the background (horizontal flip) - for a more circular background.
Matrix matrix = new Matrix(); //Like a frame or mould for an image.
matrix.setScale(-1, 1); //Horizontal mirror effect.
bgrReverse = Bitmap.createBitmap(bgr, 0, 0, bgrW, bgrH, matrix, true); //Create a new mirrored bitmap by applying the matrix.
ballX = (int) (screenW /2) - (ballW / 2) ; //Centre ball X into the centre of the screen.
ballY = -50; //Centre ball height above the screen.
}
//***************************************
//************* TOUCH *****************
//***************************************
@Override
public synchronized boolean onTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN: {
ballX = (int) ev.getX() - ballW/2;
ballY = (int) ev.getY() - ballH/2;
ballFingerMove = true;
break;
}
case MotionEvent.ACTION_MOVE: {
ballX = (int) ev.getX() - ballW/2;
ballY = (int) ev.getY() - ballH/2;
break;
}
case MotionEvent.ACTION_UP:
ballFingerMove = false;
dY = 0;
break;
}
return true;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
//Draw scrolling background.
Rect fromRect1 = new Rect(0, 0, bgrW - bgrScroll, bgrH);
Rect toRect1 = new Rect(bgrScroll, 0, bgrW, bgrH);
Rect fromRect2 = new Rect(bgrW - bgrScroll, 0, bgrW, bgrH);
Rect toRect2 = new Rect(0, 0, bgrScroll, bgrH);
if (!reverseBackroundFirst) {
canvas.drawBitmap(bgr, fromRect1, toRect1, null);
canvas.drawBitmap(bgrReverse, fromRect2, toRect2, null);
}
else{
canvas.drawBitmap(bgr, fromRect2, toRect2, null);
canvas.drawBitmap(bgrReverse, fromRect1, toRect1, null);
}
//Next value for the background's position.
if ( (bgrScroll += dBgrY) >= bgrW) {
bgrScroll = 0;
reverseBackroundFirst = !reverseBackroundFirst;
}
//Compute roughly the ball's speed and location.
if (!ballFingerMove) {
ballY += (int) dY; //Increase or decrease vertical position.
if (ballY > (screenH - ballH)) {
dY=(-1)*dY; //Reverse speed when bottom hit.
}
dY+= acc; //Increase or decrease speed.
}
//Increase rotating angle
if (angle++ >360)
angle =0;
//DRAW BALL
//Rotate method one
/*
Matrix matrix = new Matrix();
matrix.postRotate(angle, (ballW / 2), (ballH / 2)); //Rotate it.
matrix.postTranslate(ballX, ballY); //Move it into x, y position.
canvas.drawBitmap(ball, matrix, null); //Draw the ball with applied matrix.
*/// Rotate method two
canvas.save(); //Save the position of the canvas matrix.
canvas.rotate(angle, ballX + (ballW / 2), ballY + (ballH / 2)); //Rotate the canvas matrix.
canvas.drawBitmap(ball, ballX, ballY, null); //Draw the ball by applying the canvas rotated matrix.
canvas.restore(); //Rotate the canvas matrix back to its saved position - only the ball bitmap was rotated not all canvas.
//*/
//Measure frame rate (unit: frames per second).
now=System.currentTimeMillis();
canvas.drawText(framesCountAvg+" fps", 40, 70, fpsPaint);
framesCount++;
if(now-framesTimer>1000) {
framesTimer=now;
framesCountAvg=framesCount;
framesCount=0;
}
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
thread = new GameThread(getHolder(), this);
thread.setRunning(true);
thread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
boolean retry = true;
thread.setRunning(false);
while (retry) {
try {
thread.join();
retry = false;
} catch (InterruptedException e) {
}
}
}
class GameThread extends Thread {
private SurfaceHolder surfaceHolder;
private BallBounces gameView;
private boolean run = false;
public GameThread(SurfaceHolder surfaceHolder, BallBounces gameView) {
this.surfaceHolder = surfaceHolder;
this.gameView = gameView;
}
public void setRunning(boolean run) {
this.run = run;
}
public SurfaceHolder getSurfaceHolder() {
return surfaceHolder;
}
@Override
public void run() {
Canvas c;
while (run) {
c = null;
//limit frame rate to max 60fps
timeNow = System.currentTimeMillis();
timeDelta = timeNow - timePrevFrame;
if ( timeDelta < 16) {
try {
Thread.sleep(16 - timeDelta);
}
catch(InterruptedException e) {
}
}
timePrevFrame = System.currentTimeMillis();
try {
c = surfaceHolder.lockCanvas(null);
synchronized (surfaceHolder) {
//call methods to draw and process next fame
gameView.onDraw(c);
}
} finally {
if (c != null) {
surfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
}
}
Here are the images:
Make canvas animate in android
You will need to do (at least) two things:
- Modify your code so that the position of the ball is variable. This should not be too hard.
- Have a way of updating the variable(s) that define the ball position and triggering redraws of your custom view so that over time the ball appears to follow the path you want.
The second step requires a little care. You might be tempted to write a loop that invokes Thread.sleep(frameRate)
(where frameRate
is the number of milliseconds between frames), updates the ball position, and then calls invalidate()
for your custom view to trigger a repaint. The problem with this is that you cannot pause the event thread. There are (again) two ways of dealing with this:
- Create a worker thread that has the animation loop. It cannot call
invalidate()
directly, but it can callpostInvalidate()
to the same effect. - Declare a
Runnable
that in itsrun()
method updates the ball position, callsinvalidate()
, and then asks the view to run theRunnable
itself again after a delay offrameRate
(by callingpostDelayed()
).
Both methods are reasonable approaches. You will also need logic to know when the animation should end, and you might want to give the user control over when it starts or to allow replay.
Canvas animation as div background - EaselJS
You can include the canvas in your DIV, and then set it's position as absolute. Other content in the DIV will sit on top.
#canvas {
position: absolute;
display: inline;
}
Here is a quick sample:
http://jsfiddle.net/obcv1rex/1/
You can add text to the first DIV to see it scroll. I set the size to 1000x1000, but to make it more dynamic you would want to size the canvas with JavaScript (using CSS will scale the contents).
When to initialize resources to be used inside Compose Canvas?
There are two ways to keep some objects between recompositions in Compose - using remember
or representation models. For this particular case remember
is a better fit.
If you have a static size given by Modifier.size(widthDp, widthDp)
, it is easy to calculate everything in advance:
val density = LocalDensity.current
val paint = remember(widthDp) {
// in case you need to use width in your calculations
val widthPx = with(density) {
widthDp.toPx()
}
val blurMask = BlurMaskFilter(
15f,
BlurMaskFilter.Blur.NORMAL
)
val radialGradient = android.graphics.RadialGradient(
100f, 100f, 50f,
intArrayOf(android.graphics.Color.WHITE, android.graphics.Color.BLACK),
floatArrayOf(0f, 0.9f), android.graphics.Shader.TileMode.CLAMP
)
Paint().asFrameworkPaint().apply {
shader = radialGradient
maskFilter = blurMask
color = android.graphics.Color.WHITE
}
}
If you don't have a static size, for example you want to use Modifier.fillMaxSize
, you can use Modifier.onSizeChanged
to get the real size and update your Paint
- that's why I pass size
as key
in the remember
call - it will recalculate the value when the key
changes.
val (size, updateSize) = remember { mutableStateOf(null) }
val paint = remember(size) {
if (size == null) {
Paint()
} else {
Paint().apply {
// your code
}
}
}
Box(
modifier = Modifier
.fillMaxSize()
.onSizeChanged(updateSize)
.drawBehind {
// ...
}
)
Related Topics
How to Listen For a Webview Finishing Loading a Url
Application Skeleton to Support Multiple Screens
Adding External Library in Android Studio
Firebase Data Desc Sorting in Android
How to Implement My Very Own Uri Scheme on Android
Ideal Way to Cancel an Executing Asynctask
How to Create a Circular Imageview in Android
Android Respond to Url in Intent
Android Studio 3.5 Refactor Issue
How to Load Script from Assets Index.Android.Bundle on Windows
How to Correctly Save Instance State of Fragments in Back Stack
How to Detect When an Android App Goes to the Background and Come Back to the Foreground
Creating a System Overlay Window (Always on Top)