How to add animated emoticon in TextView or EditText in Android - android

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

Related

ImageView not showing bitmap after reappearing on screen API < 23

I came across a weird issue of the android framework again:
I have an activity which displays detailed information on an object. It is designed to look like a "floating" activity, meaning it overlays the MainActivity and can be dismissed by a simple swipe down from the user.
Screenshot
How it's done (wrong?)
Because setting the window background to #android:color/transparent lead to ugly side effects, I'm using a custom ImageView as the background (I modified this one by Chris Banes https://github.com/chrisbanes/philm/blob/master/app/src/main/java/app/philm/in/view/BackdropImageView.java):
public class BackdropImageView extends ImageView {
private static final int MIN_SCRIM_ALPHA = 0x00;
private static final int MAX_SCRIM_ALPHA = 0xFF;
private static final int SCRIM_ALPHA_DIFF = MAX_SCRIM_ALPHA - MIN_SCRIM_ALPHA;
private float mScrimDarkness;
private float factor;
private int mScrimColor = Color.BLACK;
private int mScrollOffset;
private int mImageOffset;
private final Paint mScrimPaint;
public BackdropImageView(Context context) {
super(context);
mScrimPaint = new Paint();
factor = 2;
}
public BackdropImageView(Context context, AttributeSet attrs) {
super(context, attrs);
mScrimPaint = new Paint();
factor = 2;
}
private void setScrollOffset(int offset) {
if (offset != mScrollOffset) {
mScrollOffset = offset;
mImageOffset = (int) (-offset / factor);
offsetTopAndBottom(offset - getTop());
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void setFactor(float factor) {
this.factor = factor;
}
#Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
if (mScrollOffset != 0) {
offsetTopAndBottom(mScrollOffset - getTop());
}
}
public void setScrimColor(int scrimColor) {
if (mScrimColor != scrimColor) {
mScrimColor = scrimColor;
ViewCompat.postInvalidateOnAnimation(this);
}
}
public void setProgress(int offset, float scrim) {
mScrimDarkness = ScrollUtils.getFloat(scrim, 0, 1);
setScrollOffset(offset);
}
#Override
protected void onDraw(#NonNull Canvas canvas) {
// Update the scrim paint
mScrimPaint.setColor(ColorUtils.setAlphaComponent(mScrimColor,
MIN_SCRIM_ALPHA + (int) (SCRIM_ALPHA_DIFF * mScrimDarkness)));
if (mImageOffset != 0) {
canvas.save();
canvas.translate(0f, mImageOffset);
canvas.clipRect(0f, 0f, canvas.getWidth(), canvas.getHeight() + mImageOffset + 1);
super.onDraw(canvas);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mScrimPaint);
canvas.restore();
} else {
super.onDraw(canvas);
canvas.drawRect(0, 0, canvas.getWidth(), canvas.getHeight(), mScrimPaint);
}
}
}
When I start the activity, I create a snapshot of the current activity and then save it to cache, passing it's path through Intent.putExtra(String key, String value); :
public static Intent createOverlayActivity(Activity activity) {
Intent startIntent = new Intent(activity, OverlayActivity.class);
View root = activity.findViewById(android.R.id.content);
Rect clipRect = new Rect();
activity.getWindow().getDecorView()
.getWindowVisibleDisplayFrame(clipRect);
Bitmap bitmap = Bitmap.createBitmap(
root.getWidth(),
root.getHeight(),
Bitmap.Config.ARGB_8888
);
Canvas canvas = new Canvas(bitmap);
canvas.drawRGB(0xEE, 0xEE, 0xEE);
// Quick fix for status bar appearing in Lollipop and above
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
canvas.translate(0, -clipRect.top / 2);
canvas.clipRect(clipRect);
}
root.draw(canvas);
try {
File file = new File(activity.getCacheDir(), "background.jpg");
FileOutputStream stream = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.JPEG, 50, stream);
stream.flush();
stream.close();
bitmap.recycle();
startIntent.putExtra("bgBitmap", file.getPath());
Log.d(TAG, "Rendered background image.");
} catch (IOException e) {
e.printStackTrace();
}
return startIntent;
}
And in the OverlayActivity's onCreate() I receive the path to the cached file and load the Bitmap into the ImageView:
Bitmap screenshot = BitmapFactory.decodeFile(getIntent().getStringExtra("bgBitmap"));
if (screenshot == null) {
throw new IllegalArgumentException("You have to provide a valid bitmap!");
}
/* BackdropImageView is an ImageView subclass allowing
* me to darken the image with a scrim using the slide offset value */
backdropImageView = new BackdropImageView(this);
backdropImageView.setId(android.R.id.background);
backdropImageView.setFactor(1.125f);
backdropImageView.setScrimColor(Color.BLACK);
backdropImageView.setImageBitmap(screenshot);
backdropImageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
The issue
As you can see in the screenshot above, it works pretty decent on devices running API 23, but not on devices below.
Here the image is shown when the activity slides in, until completely covered, but when I the slide down again, the image is gone and the ImageView just shows a solid grey:
Update
I've figured out that the issue has to hide somewhere in the BackdropImageView class, since a simple ImageView works.
Any ideas on what could cause this weird issue?
Thanks in advance!
Ok, seems like I focused too much on the actual image loading than on the view itself.
I found out that the problem was related to the use of offsetTopAndBottom(offset - getTop()); in the subclass. Somehow this makes the ImageView disappear sometimes (still not sure when exactly) on Android Versions previous to Marshmallow, although it was still in the right position in the view hierarchy.
So here's my work-around:
When I removed those lines of code, it worked. Because I needed to offset the view to create a parallax scrolling effect, I moved this functionality out of the view subclass. What I'm doing now is simply calling View.setTranslationY(float) everytime before BackdropImageView.setProgress(int, float) in any scrolling related callback I'm using.
Somehow this works perfectly fine.

Android TransitionDrawable not fading

I have an ImageView and I am trying to fade from one image to the next using this code:
Drawable bgs[] = new Drawable[2];
public void redraw(int[][] grid) {
bgs[0] = bgs[1];
bgs[1] = new GameDrawable(grid, prefs.colors);
if (bgs[0] == null) {
gameField.setImageDrawable(bgs[1]);
} else {
TransitionDrawable crossfader = new TransitionDrawable(bgs);
crossfader.setCrossFadeEnabled(true);
gameField.setImageDrawable(crossfader);
crossfader.startTransition(500);
}
}
gameField is correctly referenced as an ImageView.
gameDrawable simply extends Drawable and draws the grid.
On each move and action the new GameDrawable is being rendered correctly but there is no fading whatsoever. The new image is simply displayed instantaneously. I have tried lengthening the transition time and swapping the order of the drawables with no effect.
Any help on is appreciated.
Update: I have now set my transition to something ridiculously long like 500000. The first drawable shows for a few seconds and then suddenly the second drawable appears. So still no transition.
Update 2:
I think my Drawable might be implemented incorrectly, so I have attached the code.
public class GameDrawable extends Drawable {
private Paint paint = new Paint();
private float blockWidth = 1;
private int[][] myGrid;
private int myColor;
private List<Point> myPoints;
public GameDrawable(int[][] grid) {
super();
this.myGrid = grid;
this.myColor = colors[yourColor];
paint.setStrokeWidth(1);
paint.setStyle(Paint.Style.FILL);
paint.setAlpha(0);
this.myPoints = yourPoints;
}
#Override
public void draw(Canvas canvas) {
float height = getBounds().height();
float width = getBounds().width();
blockWidth = width / myGrid.length;
if (height / myGrid.length < blockWidth) {
blockWidth = height / myGrid.length;
}
for (int x = 0; x < myGrid.length; x++) {
for (int y = 0; y < myGrid[x].length; y++) {
paint.setColor(colors[myGrid[x][y]]);
canvas.drawRect(x * blockWidth, y * blockWidth, (x+1)*blockWidth, (y+1)*blockWidth, paint);
}
}
}
#Override
public void setAlpha(int alpha) {
paint.setAlpha(alpha);
invalidateSelf();
}
#Override
public void setColorFilter(ColorFilter cf) {
paint.setColorFilter(cf);
invalidateSelf();
}
#Override
public int getOpacity() {
return PixelFormat.TRANSLUCENT;
}
}
Looking at your code, I see a problem at the line
bgs[0] = bgs[1];
bgs[1] has not yet been defined before this line and so bgs[0] is null for the first method call. Because of this, (bgs[0] == null) is true, and so the later defined bgs[1] is directly set to the gameField ImageView.
Use corrected code below.
Drawable bgs[] = new Drawable[2];
Drawable firstDrawable = getResources().getDrawable(R.drawable.transparent);
public void redraw(int[][] grid) {
bgs[0] = firstDrawable;
bgs[1] = new GameDrawable(grid, prefs.colors);
firstDrawable = bgs[1];
TransitionDrawable crossfader = new TransitionDrawable(bgs);
crossfader.setCrossFadeEnabled(true);
gameField.setImageDrawable(crossfader);
crossfader.startTransition(500);
}
Note that TransitionDrawable does not work properly when the Drawable sizes are different. So you may need to resize firstDrawable beforehand.
EXTRA: I would avoid setCrossFadeEnabled(true) since the whole TransitionDrawable becomes translucent during the transition, revealing the background. Sometimes, this creates a "blinking" effect and destroys the smoothness of the transition.
EDIT: Looking at your custom Drawable implementation, I think the problem lies in the line
canvas.drawColor(Color.WHITE);
in the draw() method.
I looked at TransitionDrawable.java source and found that setAlpha is called on the drawables to get the cross fade effect. However, your canvas has a solid white color and setAlpha() only affects the paint. Hope this is your answer.
EDIT 2: The actual problem, as pointed out by Michael, was that TransitionDrawable's setAlpha() calls on the Drawables were rendered ineffective due to paint.setColor() in the GameDrawable's draw() method overriding the paint's alpha value set by the TransitionDrawable.

Battery consumption with custom animated drawable

Loooong time viewer, finally getting round to signing up here at StackOverflow!
After a very long time searching for a way to do a scrolling background of a ViewGroup in Android, I've developed the following:
public class SlidingDrawable extends Drawable implements Drawable.Callback {
private static final String TAG = "SlidingDraw";
private static float STEP_SIZE = 1.0f;
private BitmapDrawable mBitmap;
private Context mContext;
private float mPosX;
private int mBitmapWidth;
private Runnable mInvalidater;
private Handler mHandler;
public SlidingDrawable(Context c){
mContext = c;
// use this as the callback as we're implementing the interface
setCallback(this);
mHandler = new Handler();
mInvalidater = new Runnable(){
#Override
public void run(){
// decrement the drawables step size
mPosX -= SlidingDrawable.STEP_SIZE;
/*
* Check to see if the current position is at point where it should
* loop. If so, reset back to 0 to restart
*/
if(Math.abs(mPosX) >= mBitmapWidth) mPosX = 0;
// redraw
invalidateDrawable(null);
}
};
}
public static void setStepSize(float newSize){
SlidingDrawable.STEP_SIZE = newSize;
}
public void createBitmap(String path, ViewGroup parent){
// height of the parent container
int height = parent.getHeight();
/* Initialize local variables
* bgBitmap - the resulting bitmap to send into SlidingDrawable instance
* imageStream - raw bitmap data to be decoded into bgBitmap
*/
WindowManager wMgr = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
int mScreenWidth = wMgr.getDefaultDisplay().getWidth();
InputStream imageStream;
Matrix imgMatrix = new Matrix();
Bitmap bitmap = null;
try {
imageStream = mContext.getAssets().open(path);
// create a temporary bitmap object for basic data
Bitmap temp = BitmapFactory.decodeStream(imageStream);
int width = temp.getWidth();
// find the width difference as a percentage to apply to the
// transformation matrix
float widthDifference = ((float)mScreenWidth) / (float)(width / 2);
imgMatrix.postScale(widthDifference, 0, 0f, 0f);
// create a copy of the bitmap, scaled correctly to maintain loop
bitmap = Bitmap.createScaledBitmap(temp, (int)(width * widthDifference), height, true);
// recycle the temp bitmap
temp.recycle();
} catch (IOException e) {
e.printStackTrace();
}
mBitmap = new BitmapDrawable(bitmap);
// required
mBitmapWidth = getIntrinsicWidth() / 2;
Rect bounds = new Rect(0, 0, getIntrinsicWidth(), getIntrinsicHeight());
setBounds(bounds);
}
#Override
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap.getBitmap(), mPosX, 0f, null);
scheduleDrawable(this, mInvalidater, SystemClock.uptimeMillis());
}
#Override
public int getOpacity() {
return PixelFormat.OPAQUE;
}
#Override
public void scheduleDrawable(Drawable who, Runnable what, long when) {
mHandler.postAtTime(what, who, when);
}
#Override
public void unscheduleDrawable(Drawable who, Runnable what) {
mHandler.removeCallbacks(what, who);
}
#Override
public void invalidateDrawable(Drawable who) {
invalidateSelf();
}
/*
* Methods not directly used or called omitted
*
*/
}
It is used in the Activity like so:
#Override
public void onWindowFocusChanged(boolean focus){
// set the background of the root view of main.xml
SlidingDrawable drawable = new SlidingDrawable(getApplicationContext());
drawable.createBitmap("bgimg/basebg.jpg", mRoot);
mRoot.setBackgroundDrawable(drawable);
}
Long story short, the basebg.jpg image is a tileable image roughly 1600x480. The constructor for SlidingDrawable scales and moves and yaddah yaddah. It works.
Now, the problem is, it seems really inefficient to do it like this. I can't seem to find much information on this sort of implementation, so I'm in the dark on where I can cut CPU cycles, or even if I'm using the method calls correctly.
My questions include:
Is it better to drawBitmap as opposed to using setTranslate() or postTranslate and draw the bitmap using a Matrix?
Is it better to use drawBitmap, or the canvas functions such as translate(), save(), and restore()?
What rate does the draw() method get called at, and is there a way to limit it to, say, 24 FPS o limit redraws?
What the heck is the "when" parameter of these sorts of things? Passing in SystemClock.uptimeMillis() is the only one that worked, and trying to delay it by adding a " + 100" or something to fire every 100ms just made it stutter.
I've researched this as much as I can... I'm leaving it to StackOverflow now :)
After some time with the drawing board, I simplified the functions down. Essentially, it was sending an invalidate() call on every SystemClock.uptimeMillis(), doing one redraw for each step change.
So, I removed the Drawable.Callback interface and passed the invalidateSelf() call directly from the Handler, removing the intermediary interface methods, which didn't seem to do anything special anyway.
There was a slight difference in the CPU usage using
drawBitmap(Bitmap source, int X, int Y, Paint p) vs.
drawBitmap(Bitmap source, Matrix matrix, Paint p), so I opted for the latter to save cycles.
New methods are as follows:
// Runnable
mInvalidater = new Runnable(){
#Override
public void run(){
// decrement the drawables step size
mPosX -= SlidingDrawable.STEP_SIZE;
if(Math.abs(mPosX) >= mBitmapWidth){
mPosX = 0;
mImageMatrix.setTranslate(0, 0);
}
mImageMatrix.postTranslate(-STEP_SIZE, 0);
SlidingDrawable.this.invalidateSelf();
}
};
// Draw method
#Override
public void draw(Canvas canvas) {
canvas.drawBitmap(mBitmap.getBitmap(), mImageMatrix, null);
mHandler.postAtTime(mInvalidater, SystemClock.uptimeMillis() + 64);
}
I tested the battery results by unplugging the phone, running the application for around a minute, then opening the battery usage on my Moto Droid. Before, the battery usage surpassed the Display, now it sits comfortably below.
Angry Birds was also a benchmark, by running the opening screen (where the bg scrolls and the birds fly everywhere) for the same amount of time, in some cases my app sat below Angry Birds, but not always.
CPU usage was checked using the ADB shell command dumpsys cpuinfo as there seems to be a problem viewing CPU info on through the DDMS on devices running 2.2.
I'd still be up to hear other thoughts on this, but for now, it's solved.

Animation at a specified rate using canvas / Ondraw

In my Android app, I am trying to show letters one by one with a short delay between each, while also playing a sound for each letter. I have everything working, and the sounds play with the correct delay, but the text always prints to the screen far too fast. The canvas seems to be updated even when i am not specifically invalidating the view.
Here is what I have so far - I also tried a variant of this based on the "snake" example and had the same results... any help would be appreciated!
public class SpellingView extends View {
private static final String WORD = "TRUCK";
int width;
int height;
String textToPrint;
float textspace;
int j=0;
private final Path arc;
private final Paint tPaint;
//constructor for SpellingView
public SpellingView(Context context) {
super(context);
arc = new Path();
tPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
displayLetterLoop();
}
public void displayLetterLoop(){
for (int i = 0; i < WORD.length(); i++){
final Runnable mUpdateUITimerTask = new Runnable() {
public void run() {
Spelling.mp.start();
}
};
final Handler mHandler = new Handler();
mHandler.postDelayed(mUpdateUITimerTask, i*1500);
}
}
#Override
protected void onDraw(Canvas canvas) {
int k;
// Drawing commands go here
width = canvas.getWidth();
height = canvas.getHeight();
arc.addArc(new RectF((width*.15f), (height*.15f), (width*.85f), (height*.4f)), 180,180);
tPaint.setStyle(Paint.Style.FILL_AND_STROKE);
tPaint.setColor(Color.RED);
tPaint.setTextSize(height * 0.1f);
tPaint.setTextAlign(Paint.Align.LEFT);
setBackgroundColor(Color.BLACK);
for (k = 0; k < j; k++){
char c = WORD.charAt(k);
String cs = Character.toString(c);
textToPrint+= cs;
textspace =(float) (k*(width/WORD.length())*.9);
canvas.drawTextOnPath(cs, arc, textspace , 0, tPaint);
}
if(j<WORD.length()){
j++;
}
}
}
Custom view will invalidate itself when is a part of a layout which for some reason redraw itself. Therefore you could envelop your code in onDraw() with a condition and a flag so that it draws your stuff only when the timer sets the flag and calls invalidate. After one letter is drawn then the flag shoud be set on false like:
if (drawLetter){
drawLetter = false;
/code...
}
However this also may need to be a sychronized block.
OnDraw should happen 60 times a second and not only when you are invalidating.
So maybe you need to update some class variables (when you are invalidating) and use those for your draw logic # OnDraw.

Looking at implementing OpenGL in to app

I've written a game which uses a lot of lines, circles (some outlined, some filled, some with both) and text elements throughout to create a guitar fretboard which I get the user to interact with. Some of these elements are animated (coordinate, alpha, colour or a combination) and the app starts to skip lots of frames during most of the animations which I'd like to fix. I think OpenGL is the way to go, but I'm interested in some pointers before I jump in.
Currently my animation is achieved with lookup tables, async tasks and dynamic bitmap creation (for the text - I render it to bitmaps as I use custom fonts - so I never draw text directly to the canvas). I've got the async task running for n * 1000 ms and in the thread it waits for x ms (typically 50ms) and then pushes a progress message out - the helper classes then work out where in the time indexed lookup table the animation is and calculates the relative values based on that. I draw the static bits of the fretboard directly to the canvas with the included draw circle and draw line methods.
I'm not sure what is slowing my app down currently (mostly because I've not yet profiled it) but I'm pretty sure that even though I cache the bitmaps and have been fairly sensible about the way that I'm changing size & transparency, the use of bitmaps drawn directly to the Canvas is what is causing the slow down.
I've followed some tutorials and have written some OpenGL, but not enough to know much at all - I know it's fast though which is important. I don't know if I can use the same methods of drawing lines and circles directly to the canvas with OpenGL, and I think I'll still have to create some bitmaps for the text and I think I have to apply these as textures in order to show them.
So can anyone give me some pointers? Some sample code of drawing lines, circles and text would be amazing. Any pointers on animating within OpenGL - I think my current setup is pretty solid and can prob port it over but any advice would be great as this is my first look in to animating.
* EDIT *
Here's a basic overview of my code - there are many pieces, but I'm including them in the hope that someone else may be able to use some of it.:
I have a look up table
public enum LookUpTable {
COUNT_DOWN {
#Override
public float[][] getLookUpTable() {
/*
* 1 = total time left
* 2 = font alpha
* 3 = font size
*/
float[][] lookUpTable = {
{ 0f, 0f, 400f },
{ 400f, 255f, 200f },
{ 700f, 255f, 150f },
{ 1000f, 0f, 5f }
};
return lookUpTable;
}
#Override
public LookUpType getLookUpType() {
return LookUpType.REPEAT;
}
};
// does the timer loop around, or is it a one off run
private enum LookUpType {
REPEAT, SINGLE
}
abstract public LookUpType getLookUpType();
abstract public float[][] getLookUpTable();
}
I have extended the AsyncTask task into a builder function:
public class CountDownTimerBuilder {
// callbacks - instantiated in the view
protected CountDownEndEvent countDownEndEvent;
protected CountDownProgressEvent countDownProgressEvent;
protected CountDownInitEvent countDownInitEvent;
protected int updatePeriod;
protected float runTime;
public CountDownTimerBuilder withCountDownEndEvent(CountDownEndEvent countDownEndEvent) {
this.countDownEndEvent = countDownEndEvent;
return this;
}
public CountDownTimerBuilder withCountDownProgressEvent(CountDownProgressEvent countDownProgressEvent) {
this.countDownProgressEvent = countDownProgressEvent;
return this;
}
public CountDownTimerBuilder withCountDownInitEvent(CountDownInitEvent countDownInitEvent) {
this.countDownInitEvent = countDownInitEvent;
return this;
}
public CountDownTimerBuilder withUpdatePeriod(int updatePeriod) {
this.updatePeriod = updatePeriod;
return this;
}
public CountDownTimerBuilder withRunTime(float runTime) {
this.runTime = runTime;
return this;
}
public CountDownTimer build() {
return new CountDownTimer();
}
public static interface CountDownEndEvent {
public abstract void dispatch(Long... endResult);
}
public static interface CountDownInitEvent {
public abstract void dispatch();
}
public static interface CountDownProgressEvent {
public abstract void dispatch(Long... progress);
}
public class CountDownTimer {
AsyncTask<Void, Long, Long> genericTimerTask;
/**
* Starts the internal timer
*/
public void start() {
genericTimerTask = new GenericCountDownTimer().execute(new Void[] {});
}
public void cancel() {
if (genericTimerTask != null) {
genericTimerTask.cancel(true);
genericTimerTask = null;
}
}
private class GenericCountDownTimer extends AsyncTask<Void, Long, Long> {
#Override
protected Long doInBackground(Void... params) {
long startTime = System.currentTimeMillis();
long currentTime;
long countDown;
Log.i(ApplicationState.getLogTag(getClass()), "Timer running for " + runTime + " ms, updating every " + updatePeriod + " ms");
do {
try {
Thread.sleep(updatePeriod);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (this.isCancelled()) {
Log.i(ApplicationState.getLogTag(getClass()), "Timer Cancelled");
break;
}
currentTime = System.currentTimeMillis();
countDown = currentTime - startTime;
publishProgress((long)runTime - countDown);
} while (countDown <= runTime);
return 0l;
}
#Override
protected void onPreExecute() {
if (countDownInitEvent != null) {
countDownInitEvent.dispatch();
}
}
#Override
protected void onProgressUpdate(Long... progress) {
Log.v(ApplicationState.getLogTag(getClass()), "Timer progress " + progress[0] + " ms");
if (countDownProgressEvent != null) {
countDownProgressEvent.dispatch(progress);
}
}
#Override
protected void onPostExecute(Long endresult) {
if (countDownEndEvent != null) {
countDownEndEvent.dispatch(endresult);
}
}
}
}
}
I have a class where my animation values are calculated:
public class AnimationHelper {
private LookUpTable lookUpTable;
private float[][] lookUpTableData;
private float currentTime = -1;
private float multiplier;
private int sourceIndex;
public void setLookupTableData(LookUpTable lookUpTable) {
if (this.lookUpTable != lookUpTable) {
this.lookUpTableData = lookUpTable.getLookUpTable();
this.currentTime = -1;
this.multiplier = -1;
this.sourceIndex = -1;
}
}
private void setCurrentTime(float currentTime) {
this.currentTime = currentTime;
}
public float calculate(float currentTime, int index) {
if (this.currentTime == -1 || this.currentTime != currentTime) {
setCurrentTime(currentTime);
getCurrentLookupTableIndex();
getMultiplier();
}
return getCurrentValue(index);
}
private void getCurrentLookupTableIndex() {
sourceIndex = -1;
for (int scanTimeRange = 0; scanTimeRange < (lookUpTableData.length - 1); scanTimeRange++) {
if (currentTime < lookUpTableData[scanTimeRange + 1][0]) {
sourceIndex = scanTimeRange;
break;
}
}
}
private void getMultiplier() {
if ((lookUpTableData[sourceIndex][0] - lookUpTableData[sourceIndex + 1][0]) == 0.0f) {
multiplier = 0.0f;
} else {
multiplier = (currentTime - lookUpTableData[sourceIndex][0]) / (lookUpTableData[sourceIndex + 1][0] - lookUpTableData[sourceIndex][0]);
}
}
public float getCurrentValue(int index) {
float currentValue = lookUpTableData[sourceIndex][index] + ((lookUpTableData[sourceIndex + 1][index] - lookUpTableData[sourceIndex][index]) * multiplier);
return currentValue > 0 ? currentValue : 0;
}
}
In my game code I tie it all together by specifying the lookup table to use and creating callbacks for each of the different states, creating the timer with the builder class and starting it:
AnimationHelper animHelper = new AnimationHelper();
animHelper.setLookupTableData(LookUpTable.COUNT_DOWN);
CountDownInitEvent animationInitEvent = new CountDownInitEvent() {
public void dispatch() {
genericTimerState = TimerState.NOT_STARTED;
}
};
CountDownProgressEvent animationProgressEvent = new CountDownProgressEvent() {
public void dispatch(Long... progress) {
genericTimerState = TimerState.IN_PROGRESS;
// update the generic timer - we'll use this in all animations
genericTimerCountDown = progress[0];
invalidate();
}
};
CountDownEndEvent animationEndEvent = new CountDownEndEvent() {
public void dispatch(Long... endValue) {
genericTimerState = TimerState.FINISHED;
startGame();
}
};
CountDownTimer timer = new CountDownTimerBuilder()
.withRunTime(getCountDownPeriod(countDownTimePeriod)) // getCountDownPeriod() is used for handling screen rotation - esentially returns the run time for the timer in ms
.withUpdatePeriod(TIMER_UPDATE_PERIOD) // currently set at 50
.withCountDownInitEvent(animationInitEvent)
.withCountDownProgressEvent(animationProgressEvent)
.withCountDownEndEvent(animationEndEvent)
.build();
timer.start();
in my onDraw I get the specific values from the lookup table and act on them:
private int IFTL = 0; // total time left
private int IFY1 = 1; // initial instructions y offset
private int IFY2 = 2; // start message y offset
private int IFA1 = 3; // note to guess alpha
float yPosition1 = animHelper.calculate(genericTimerCountDown, IFY1);
float yPosition2 = animHelper.calculate(genericTimerCountDown, IFY2);
float alpha1 = animHelper.calculate(genericTimerCountDown, IFA1);
// getScreenDrawData() returns the coordinates and other positioning info for the bitmap
final ScreenDrawData guessNoteTitleDrawValues = FretBoardDimensionHelper.getScreenDrawData(AssetId.GUESS_NOTE_TITLE);
//change the y position of the bitmap being drawn to screen
guessNoteTitleDrawValues.withAlteredCoordinate(Constants.Y_COORDINATE, 0-yPosition1);
//
DrawBitmapBuilder.createInstance()
.withCanvas(getCanvas())
.withBitmap(bitmapCacheGet(AssetId.GUESS_NOTE_TITLE))
.withBitmapDrawValues(guessNoteTitleDrawValues)
.draw();
Paint paint = new Paint();
paint.setAlpha((int)alpha1);
final ScreenDrawData initialNoteDrawValues = FretBoardDimensionHelper.getScreenDrawData(AssetId.GUESS_NOTE_INITIAL_NOTE);
// draw to screen with specified alpha
DrawBitmapBuilder
.createInstance()
.withCanvas(getCanvas())
.withBitmap(bitmapCacheGet(AssetId.GUESS_NOTE_INITIAL_NOTE))
.withBitmapDrawValues(initialNoteDrawValues)
.withPaint(paint)
.draw();
You'll have to read between the lines a bit in that last bit of code as there are a load of helper functions in there.
Does this look like a reasonable approach? I'd love a code review if anyone can be bothered - more than happy to post up more code or explanations if required
I've been working a bit with OpenGL and well... It's not trivial.
For the text, you'll have to create a mutable Bitmap, write some text in it with canvas.drawText(...), and then convert this Bitmap (which should be in powerOfTwo dimension) to a gl texture, and apply it to a rectangle.
For circles... it's gonna be complicated. OpenGL draws lines, triangles, dots... not circles i'm afraid. One way to achieve a circle in OpenGL is to have a texture of a circle, and apply it to a rectangle.
Each time your application is put on pause, and resumed, you'll have to recreate each of your textures, and set up your gl surface once again...
If you need blending, you'll probably find that, with a lot of textures, your phone doesn't have enough memory to handle it all...
Now that I have scared you off : open gl is really one of the way to go ! It'll allow your app to delegate the drawing to the phone GPU, which will let you keep CPU for calculating animations and such.
You'll find some information about using OpenGL ES for android on this website :
http://blog.jayway.com/2009/12/03/opengl-es-tutorial-for-android-part-i/
There are 6 tutorials, and the 6th one is about textures. The 2nd tutorial tells you about drawing polygon, and lines.
Last of all, you should read this book : http://my.safaribooksonline.com/book/office-and-productivity-applications/9781430226475/copyright/ii
It's about android, openGL, ...
Good luck.
Edit :
Look like I don't have enough privilege to write a comment, therefor I'll edit this :
The way I'd do it would be maybe a lot more simpler :
I'd have a surface view, with a renderer dedicated to draw as often as possible. He would draw synchronized list of items (Synchronized, because of multi thread), with position, alpha, ... no async task here, just a regular rendering thread.
The surface view would also start a thread (regular thread once again, no async task), with a List (or something like that). This thread would, every 16 ms or something, run through the animation list, apply them. (synchronously of course if it needs to change some items used by the rendering thread).
When an animation is over (like, if it's been in the list for more than 2000 ms, easy to check with function such as System.currentTimeMillis()), I'd remove it from the list.
Voila !
Then you'll tell me : hey, but if i do a lot of animation, calculation might takes longer than 16 ms, and it appears to get slower !
My answer is : there is an easy enough solution for that : in the thread that deals with animation, before applying them you do something like that :
long lastTime = currentTime;
// Save current time for next frame;
currentTime = System.currentTimeMillis();
// Get elapsed time since last animation calculus
long ellapsedTime = currentTime - lastTime;
animate(ellapsedTime);
And in your animate function, you animate more if more time elapsed !
Hope this helps.

Categories

Resources