Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add animated emoticon in TextView or EditText in Android

Tags:

android

gif

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();
    }
}
like image 757
Stay Avatar asked Dec 26 '11 03:12

Stay


1 Answers

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. :)

like image 137
MH. Avatar answered Sep 28 '22 12:09

MH.