I would to create an animated WatchFace for Android Wear.
I've 20 images to add (or to change completely) every X ms to the background.
Now: i've followed this tutorial but the animation doesn't start. I see only one of the twenty bitmap over my background:
if (isInAmbientMode()) {
canvas.drawBitmap(mBackgroundAmbient, SRC, DEST, null);
} else {
canvas.drawBitmap(mBackground, SRC, DEST, null);
for (int i = 0; i < LoopBMP.length; i++) {
canvas.save();
Bitmap cloud = LoopBMP[i];
canvas.drawBitmap(cloud,centerX, centerY,null);
canvas.restore();
}
}
Any suggestion?
You're misunderstanding how the CanvasWatchFaceService.Engine does its drawing. I'm guessing that the code snippet you posted is in your onDraw method; this method is called once for each frame of your animation.
Meaning that you need to move your animation "loop" outside of the onDraw method. There are several ways to accomplish this, but I've dummied up one below based on your code.
private int i;
#Override
public void onDraw(Canvas canvas, Rect bounds) {
super.onDraw(canvas, bounds);
// probably other code here
if (isInAmbientMode()) {
canvas.drawBitmap(mBackgroundAmbient, SRC, DEST, null);
} else {
canvas.drawBitmap(mBackground, SRC, DEST, null);
if (i < LoopBMP.length) {
canvas.save();
Bitmap cloud = LoopBMP[i];
canvas.drawBitmap(cloud,centerX, centerY,null);
canvas.restore();
i++;
// probably want an X-ms delay here to time the animation
invalidate();
} else {
i = 0;
}
}
// probably other code here
}
Note that this is a snippet I just threw together to demonstrate what I'm talking about; it's by no means ready-to-run. In particular, you will want a delay between frames of your animation; you can implement that with a Handler like the one used for the second hand in this sample: http://developer.android.com/samples/WatchFace/Wearable/src/com.example.android.wearable.watchface/AnalogWatchFaceService.html#l117
Related
Ok, I am developing a sidescrolling game and my problem is on how to properly draw and update the screen. I am drawing on a SurfaceView and I use Path to make the contourns, currently the algorithm only draws this:
And I am sidescrolling by using Path.offSet() and then canvas.drawPath(), later on I update the last X position on the path by using Path.addRect() (and thats basically how I am drawing everything: using Path.addRect())
So here is the thread that updates the screen:
#Override
public void run() {
int x = LibraryLoader.getTerrainSizeX();
int y = LibraryLoader.getTerrainSizeY();
int count = 0;
Paint paint = new Paint();
paint.setStyle(Paint.Style.FILL);
Path path = new Path();
makePath(path, x, y, 0, LibraryLoader.getTerrainThickness());
Path path2 = new Path();
makePath(path2, x, y, LibraryLoader.getTerrainThickness(), y);
while (run) {
Canvas c = null;
try {
c = surfaceHolder.lockCanvas(null);
synchronized (surfaceHolder) {
fps = fps();
drawMyData(c, path, path2, paint, fps);
LibraryLoader.updateOffSet();
updatePaths(path, path2, x, y);
if ((count++) == (x / 2) - 1) {
LibraryLoader.updateOffSetArray();
count = 0;
}
}
} finally {
if (c != null) {surfaceHolder.unlockCanvasAndPost(c);}
}
}
and its respective methods:
public void updatePaths(Path path, Path path2, int x, int y) {
path.offset(-1f, 0);
path.addRect(x-3, topValue, x-2, bottomValue, Path.Direction.CW);
path2.offset(-1f, 0);
path2.addRect(x-3, topValue, x-2, y, Path.Direction.CW);
}
So, in my phone it works perfectly at 60fps, the problem is I tested in a lower end device and it begins at 40fps then drops every update until it gets below 10fps...(and keeps dropping). I guess I need to clean the state of the path, or I shouldn't even be using the Path class to begin with. So my question is how should I update the screen with the best performance? Obs: The canvas is not hardware accelerated.
Well folks I figured out that I was wrong about everything I did. The answer is simple: If your android application updates the whole screen every frame, use Opengl. Canvas is for app design for what I've seen, hope I am not mistaken. For example, if you want to make a custom animation for a LOGO or a button, so you use canvas, I guess. If anyone stumbles in this post do watch the videos Morrison Chang mentioned, they are very helpful to put you on the right track. Cheers.
I have been experimenting with squeezing as much performance out of SurfaceView as possible. Currently, I'm subclassing it and implementing a runnable interface on it instead of a callback. I understand there is no hardware acceleration on it.
Still, if I either draw a canvas primitive vertical line scrolling across the screen or a bitmap vertical line, both run slower and slower after each pass. This felt to me like a memory leak, or is it just Android itself? Is OpenGL or another library really my last resort?
I've drawn plenty of scrolling backgrounds before at decent speeds (I think around 5 pixels per tick, this I'm aiming around 20-50 pixels a tick which if anything would be less stops along the way to render).
EDIT: Here is the SurfaceView extended, the thread it makes, the drawing method, and the initialization of it. Basically, this is in a slightly bigger class that just holds this screen's data. The drawXYZ() methods simply use the canvas primitives or a bitmap to paint mainly as the background, which is a solid background color with some vertical and horizontal lines on it like a music staff, little calculating is involved.
The drawCursor is what makes the scrolling vertical line and when I just let it loop the scrolling from left to right, it eventually lags much slower than the first scroll.
public class MySurfaceView extends SurfaceView implements Runnable
{
Thread renderThread = null;
SurfaceHolder holder;
volatile boolean running = false;
public MySurfaceView() {
super(mainActivity);
this.holder = getHolder();
holder.setFixedSize(screenW, screenH);
}
public void resume() {
running = true;
renderThread = new Thread(this);
renderThread.start();
}
#Override
public void run() {
while (running) {
if (!holder.getSurface().isValid()) {
continue;
}
Canvas canvas = holder.lockCanvas();
if(canvas != null) {
doDraw(canvas);
holder.unlockCanvasAndPost(canvas);
}
}
}
public void pause() {
running = false;
while (true) {
try {
renderThread.join();
break;
} catch (InterruptedException e) {
// retry
}
}
}
protected void doDraw(Canvas canvas)
{
canvas.drawColor(Color.rgb(56, 56, 62));
lastNotePlayed = OptionsContainer.getNotePlaying();
//Draw contours (rows).
paint.setColor(Color.rgb(0, 255, 255));
paint.setStrokeWidth(3);
paint.setTextSize(35);
drawContours(canvas, paint);
//Beats per measure (BPM).
paint.setColor(Color.rgb(233, 232, 232));
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.STROKE);
paint.setPathEffect(bpmLines);
drawBPM(canvas, paint);
paint.setPathEffect(null);
//Draw measures.
paint.setStrokeWidth(5);
drawMeasures(canvas, paint);
//Draw note node inputs.
paint.setColor(Color.rgb(76, 255, 0));
for (int i = 0; i < OptionsContainer.noteList.length; i++) {
if (OptionsContainer.noteList[i].getContour() != 0) {
if (OptionsContainer.noteList[i].getContour() > (OptionsContainer.contour / 2)) {
//Staff on left side, below note.
canvas.drawBitmap(lowerStaffBmp, OptionsContainer.noteList[i].getX(), OptionsContainer.noteList[i].getY(), null);
} else {
canvas.drawBitmap(higherStaffBmp, OptionsContainer.noteList[i].getX(), OptionsContainer.noteList[i].getY() - 40, null);
}
}
}
//Draw cursor.
paint.setStrokeWidth(2);
paint.setColor(Color.WHITE);
drawCursor(canvas, paint);
if (OptionsContainer.isRest)
canvas.drawBitmap(restBmp, (OptionsContainer.screenWidth / 2), (screenHeight - 100) / 2, null);
}
}
#Override
public void init() {
surfaceView = new MySurfaceView();
surfaceView.setLayoutParams(layoutParams);
surfaceView.setOnTouchListener(new View.OnTouchListener() {
#Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_UP) {
// Normalize x,y between 0 and 1
float x = event.getX();
float y = event.getY();
if (x < (OptionsContainer.screenWidth) && y < screenH) {
NoteNode note = new NoteNode(x, y, MainActivity.options);
if (note.getContour() == OptionsContainer.noteList[note.getBeat() - 1].getContour()) {
OptionsContainer.noteList[note.getBeat() - 1] = new NoteNode(x, screenHeight + 200, MainActivity.options);
} else {
OptionsContainer.noteList[note.getBeat() - 1] = note;
}
}
}
return true;
}
});
mainActivity.addContentView(surfaceView, layoutParams);
surfaceView.resume();
}
EDIT #2: Final Answer
Add Path.reset() after the path is drawn in drawBPM(). I'd imagine that stops a memory leak of that path which is trying to keep track of ALL the paths it has been writing and overwriting, little to our knowledge just looking at the lines on the screen. There was a similar Stack Overflow question but fadden's debugging tips below were very helpful for initially trying to figure out what and where it was going wrong.
"Squeezing performance" and Canvas-rendering don't really go together on a SurfaceView, but you can do okay on many devices.
Grafika's "multi-surface test" Activity features a bouncing circle, rendered in software. I haven't noticed it get slower over time, so I suspect something is wrong in your code. Note Grafika does not subclass SurfaceView, and I generally recommend against doing so -- it's too easy to do the wrong thing. The only valid reason to subclass SurfaceView is if you want to draw on both the Surface and the View, e.g. for some sort of mask effect.
You didn't show any code, so there's not much more we can tell you.
I don't see anything blatantly wrong in the code; seems pretty straightforward. I'd check to make sure OptionsContainer.noteList.length isn't growing without bound. Next step would be to use traceview to figure out which part of the rendering is slow, or just spread System.nanoTime() calls around to identify which part is getting progressively slower. If everything in the method shown is executing at a consistent speed except drawCursor(), move the time-check calls into there, narrowing it down until you find what's draining your performance.
If something is consuming memory quickly enough to cause heap issues, you should see a great deal of GC activity in the logcat output. The DDMS allocation tracker tool can help with that.
I was trying to make moving bitmap with accelerometer smoother and accidentally noticed that when I call invalidate(); at the end of onDraw() method instead of calling it at the end of onSensorChanged() I get much smoother movement, even if I don't have any kind of low-pass filters. Then I tried to do the same with my LiveWallpaper, but as you know there is no onDraw() method in Engine of WallpaperService, but you have to create one yourself and call it for example with Handler. But doing it that way doesn't give any smoother result even if the rest of the code is same as in other programs.
This is the code that I use in my non-Wallpaper programs and it works fine:
public void onDraw(Canvas c) {
xPosition += xAcceleration;
yPosition += yAcceleration;
drawable = BitmapFactory.decodeResource(getResources(),R.drawable.ball);
c.drawBitmap(drawable, xPosition,yPosition, paint);
invalidate();
}
So I went and tried to create my own invalidate-like solution for WallpaperService and came up with this:
void drawFrame() {
final SurfaceHolder holder = getSurfaceHolder();
Canvas c = null;
try {
c = holder.lockCanvas();
if (c != null) {
xPosition += xAcceleration;
yPosition += yAcceleration;
background = BitmapFactory.decodeResource(getResources(),R.drawable.bg);
drawable = BitmapFactory.decodeResource(getResources(),R.drawable.ball);
c.drawBitmap(background, 0,0, null);
c.drawBitmap(drawable, xPosition,yPosition, null);
}
} catch (Exception ex){
}
holder.unlockCanvasAndPost(c);
drawFrame();
}
So what I am doing is:
Get Canvas.
Draw on Canvas.
Unlock Canvas and start over.
As I have understood this should give me invalidate();-like behaviour, but instead it tries to show wallpaper and after while it gives me StackOverflowError.
Ok I got this solved already. All I had to to was move bitmap initializations into onCreate() method.
as the question, I use ImageSpan to add a image into TextView. but it can't animate.Do you have any advise?
I try to extend AnimationDrawable to add drawable into ImageSpan. but it doesn't work
public class EmoticonDrawalbe extends AnimationDrawable {
private Bitmap bitmap;
private GifDecode decode;
private int gifCount;
public EmoticonDrawalbe(Context context, String source) {
decode = new GifDecode();
decode.read(context, source);
gifCount = decode.getFrameCount();
if (gifCount <= 0) {
return;
}
for (int i = 0; i < gifCount; i++) {
bitmap = decode.getFrame(i);
addFrame(new BitmapDrawable(bitmap), decode.getDelay(i));
}
setOneShot(false);
}
#Override
public void draw(Canvas canvas) {
super.draw(canvas);
start();
}
}
I would try to either:
Split the animated image (presumably a .gif file?) into separate frames and combine those into an AnimationDrawable that you then pass to the ImageSpan's constructor.
Subclass ImageSpan and override the onDraw() method to add your own logic to draw the different frames based on some sort of timer. There's an api demo that illustrates how to use the Movie class to load up an animated gif that might be worth looking into.
Big Edit:
Alright, sorry for not getting back earlier, but I had to set aside some time to investigate this myself. I've had a play with it since I'll probably be needing a solution for this myself for one of my future projects. Unfortunately, I ran into similar problems with using an AnimationDrawable, which seems to be caused by the caching mechanism that DynamicDrawableSpan (an indirect superclass of ImageSpan) uses.
Another issue for me is that there does not appear to be a straightforward wat to invalidate a Drawable, or ImageSpan. Drawable actually has invalidateDrawable(Drawable) and invalidateSelf() methods, but the first did not have any effect in my case, whereas the latter only works if some magical Drawable.Callback is attached. I couldn't find any decent documentation on how to use this...
So, I went a step further up the logic tree to solve the problem. I have to add a warning in advance that this is most likely not an optimal solution, but for now it's the only one I was able to get to work. You probably won't run into problems if you use my solution sporadically, but I'd avoid filling the whole screen with emoticons by all means. I'm not sure what would happen, but then again, I probably don't even want to know.
Without further ado, here's the code. I added some comments to make it self-explanatory. It's quite likely a used a different Gif decoding class/libary, but it should work with about any out there.
AnimatedGifDrawable.java
public class AnimatedGifDrawable extends AnimationDrawable {
private int mCurrentIndex = 0;
private UpdateListener mListener;
public AnimatedGifDrawable(InputStream source, UpdateListener listener) {
mListener = listener;
GifDecoder decoder = new GifDecoder();
decoder.read(source);
// Iterate through the gif frames, add each as animation frame
for (int i = 0; i < decoder.getFrameCount(); i++) {
Bitmap bitmap = decoder.getFrame(i);
BitmapDrawable drawable = new BitmapDrawable(bitmap);
// Explicitly set the bounds in order for the frames to display
drawable.setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
addFrame(drawable, decoder.getDelay(i));
if (i == 0) {
// Also set the bounds for this container drawable
setBounds(0, 0, bitmap.getWidth(), bitmap.getHeight());
}
}
}
/**
* Naive method to proceed to next frame. Also notifies listener.
*/
public void nextFrame() {
mCurrentIndex = (mCurrentIndex + 1) % getNumberOfFrames();
if (mListener != null) mListener.update();
}
/**
* Return display duration for current frame
*/
public int getFrameDuration() {
return getDuration(mCurrentIndex);
}
/**
* Return drawable for current frame
*/
public Drawable getDrawable() {
return getFrame(mCurrentIndex);
}
/**
* Interface to notify listener to update/redraw
* Can't figure out how to invalidate the drawable (or span in which it sits) itself to force redraw
*/
public interface UpdateListener {
void update();
}
}
AnimatedImageSpan.java
public class AnimatedImageSpan extends DynamicDrawableSpan {
private Drawable mDrawable;
public AnimatedImageSpan(Drawable d) {
super();
mDrawable = d;
// Use handler for 'ticks' to proceed to next frame
final Handler mHandler = new Handler();
mHandler.post(new Runnable() {
public void run() {
((AnimatedGifDrawable)mDrawable).nextFrame();
// Set next with a delay depending on the duration for this frame
mHandler.postDelayed(this, ((AnimatedGifDrawable)mDrawable).getFrameDuration());
}
});
}
/*
* Return current frame from animated drawable. Also acts as replacement for super.getCachedDrawable(),
* since we can't cache the 'image' of an animated image.
*/
#Override
public Drawable getDrawable() {
return ((AnimatedGifDrawable)mDrawable).getDrawable();
}
/*
* Copy-paste of super.getSize(...) but use getDrawable() to get the image/frame to calculate the size,
* in stead of the cached drawable.
*/
#Override
public int getSize(Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
Drawable d = getDrawable();
Rect rect = d.getBounds();
if (fm != null) {
fm.ascent = -rect.bottom;
fm.descent = 0;
fm.top = fm.ascent;
fm.bottom = 0;
}
return rect.right;
}
/*
* Copy-paste of super.draw(...) but use getDrawable() to get the image/frame to draw, in stead of
* the cached drawable.
*/
#Override
public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
Drawable b = getDrawable();
canvas.save();
int transY = bottom - b.getBounds().bottom;
if (mVerticalAlignment == ALIGN_BASELINE) {
transY -= paint.getFontMetricsInt().descent;
}
canvas.translate(x, transY);
b.draw(canvas);
canvas.restore();
}
}
Usage:
final TextView gifTextView = (TextView) findViewById(R.id.gif_textview);
SpannableStringBuilder sb = new SpannableStringBuilder();
sb.append("Text followed by animated gif: ");
String dummyText = "dummy";
sb.append(dummyText);
sb.setSpan(new AnimatedImageSpan(new AnimatedGifDrawable(getAssets().open("agif.gif"), new AnimatedGifDrawable.UpdateListener() {
#Override
public void update() {
gifTextView.postInvalidate();
}
})), sb.length() - dummyText.length(), sb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
gifTextView.setText(sb);
As you can see I used a Handler to provide the 'ticks' to advance to the next frame. The advantage of this is that it will only fire off an update whenever a new frame should be rendered. The actual redrawing is done by invalidating the TextView which contains the AnimatedImageSpan. At the same time the drawback is that whenever you have a bunch of animated gifs in the same TextView (or multiple for that matter), the views might be updated like crazy... Use it wisely. :)
You can use ObjectAnimator to animate the drawable in the ImageSpan
http://developer.android.com/reference/android/animation/ObjectAnimator.html
I just found out something and I was wondering about how and why.
I'm developing a small arcade game for Android. I decided to ignore OpenGL and use the standard SurfaceView and Drawables to do it, since it's suppose to be light (10 sprites or so).
I have drawables that I load, and I use the method Draw and passing them my canvas. This how every sprite is drawn to the screen.
Well it turns out that drawing 4-5 big sprites (200X400 or so) takes a long time on less-than-brand-new phone models. Long enough to make my game unplayable. We're talking about 50-60 milliseconds to draw a single frame using this method. And I really don't do anything there apart from drawing, nowhere I can cut costs. So I decided to try and use Bitmaps instead. Here, however, I need to pre-set the size, since there's no 'setBounds' method in a bitmap. No prob, I resize them to fit my current screen on load, problem solved.
OK. So I got bitmaps. I use Canvas.DrawBitmap now to draw. I bench the new draw method.. and I get a whooping 400% performance boost! Instead of 50-60ms, the entire draw loop now takes 8-12ms. What the hell??
To rule it out, I timed the setBounds too, it takes <1ms so it's not to blame. It's the actual Drawable.Draw that slows things down.
For me this is great news, since I really didn't want to learn OpenGL to make my game playable, but I can't stop wondering about it - Is it fine? are there problems with my method? Why isn't it mentioned anywhere?
The SurfaceView of your Canvas is meant to be used when you should iterate constantly and Drawable is not for that purpose.
Canvas.drawBitmap is doing a lot less work than Drawable.draw so it is faster.
Drawable.draw
Since Drawable is an abstract class, let's look at BitmapDrawable:
BitmapDrawable.draw(canvas)
public void draw(Canvas canvas) {
final Bitmap bitmap = mBitmapState.mBitmap;
if (bitmap == null) {
return;
}
final BitmapState state = mBitmapState;
final Paint paint = state.mPaint;
if (state.mRebuildShader) {
final Shader.TileMode tmx = state.mTileModeX;
final Shader.TileMode tmy = state.mTileModeY;
if (tmx == null && tmy == null) {
paint.setShader(null);
} else {
paint.setShader(new BitmapShader(bitmap,
tmx == null ? Shader.TileMode.CLAMP : tmx,
tmy == null ? Shader.TileMode.CLAMP : tmy));
}
state.mRebuildShader = false;
}
final int restoreAlpha;
if (state.mBaseAlpha != 1.0f) {
final Paint p = getPaint();
restoreAlpha = p.getAlpha();
p.setAlpha((int) (restoreAlpha * state.mBaseAlpha + 0.5f));
} else {
restoreAlpha = -1;
}
final boolean clearColorFilter;
if (mTintFilter != null && paint.getColorFilter() == null) {
paint.setColorFilter(mTintFilter);
clearColorFilter = true;
} else {
clearColorFilter = false;
}
updateDstRectAndInsetsIfDirty();
final Shader shader = paint.getShader();
final boolean needMirroring = needMirroring();
if (shader == null) {
if (needMirroring) {
canvas.save();
// Mirror the bitmap
canvas.translate(mDstRect.right - mDstRect.left, 0);
canvas.scale(-1.0f, 1.0f);
}
canvas.drawBitmap(bitmap, null, mDstRect, paint);
if (needMirroring) {
canvas.restore();
}
} else {
updateShaderMatrix(bitmap, paint, shader, needMirroring);
canvas.drawRect(mDstRect, paint);
}
if (clearColorFilter) {
paint.setColorFilter(null);
}
if (restoreAlpha >= 0) {
paint.setAlpha(restoreAlpha);
}
}
You can see that it even calls canvas.drawBitmap internally.
Canvas.drawBitmap
Compare that to Canvas.drawBitmap. It is much shorter.
Canvas.drawBitmap
public void drawBitmap(#NonNull Bitmap bitmap, float left, float top, #Nullable Paint paint) {
throwIfCannotDraw(bitmap);
native_drawBitmap(mNativeCanvasWrapper, bitmap, left, top,
paint != null ? paint.getNativeInstance() : 0, mDensity, mScreenDensity, bitmap.mDensity);
}
There are a few different drawBitmap methods but all of them are shorter than the Drawable.draw method. Watch out for traps like this to keep your bitmap drawing fast.