Android Animation Playback: Inline Animation with TextView

TextView allows us to attach and detach markup objects to a range of text using Spannable. We can use this to implement inline animation within TextView.

0. AnimatedImageSpan

We implement a AnimatedImageSpan, which extends DynamicDrawableSpan. The object will accept a set of image frames and keep track of the frame to display. A handler and a runnable are implemented to update the frame to display.

Note that AnimatedImageSpan doesn’t update the screen by itself. It calls AnimatedImageUpdateHandler.updateFrame to refresh the entire TextView, which subsequently redraw the AnimatedImageSpan with new image.

public class AnimatedImageSpan extends DynamicDrawableSpan {

    private AnimationAssetsSet mGifAssets;

    private int mCurrentFrameIdx;

    private Context mContext;

    

    private SimpleImageMemCache mImageCache;

    private AnimatedImageUpdateHandler mImageUpdater;

    

    private final Handler handler = new Handler();

    

    public AnimatedImageSpan(Context context) {

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

    

    public void setAnimationAssets(AnimationAssetsSet pGifAssets) {

        mGifAssets = pGifAssets;

    }

    

    private Runnable mRunnable;

    private int mPlaybackTimes;

    private boolean mPlaying;

    public void playGif(final AnimationSettings pGifSettings, AnimatedImageUpdateHandler pListener) {

        mPlaying = true;

        mImageUpdater = pListener;

        mPlaybackTimes = 0;

        mRunnable = new Runnable() {

            public void run() {

                mCurrentFrameIdx = (mCurrentFrameIdx + 1)%mGifAssets.getNumOfFrames();

//                Logger.d(this, "current frame " + mCurrentFrameIdx);

                handler.postDelayed(this, pGifSettings.mDelay);

                if (null != mImageUpdater) {

//                    Logger.d(this, "update frame using listener " + mImageUpdater.getId());

                    mImageUpdater.updateFrame();

                }

                if (mCurrentFrameIdx == mGifAssets.getNumOfFrames() - 1) {

                    if (pGifSettings.mPlaybackTimes == 0) {

                        //repeat forever

                    } else {

                        mPlaybackTimes++;

                        if (mPlaybackTimes == pGifSettings.mPlaybackTimes) {

                            stopRendering();

                        }

                    }

                }

            }

        };

        handler.post(mRunnable);

    }

    

    public boolean isPlaying() {

        return mPlaying;

    }

    

    public void stopRendering() {

        handler.removeCallbacks(mRunnable);

        mPlaying = false;

    }

    

    @Override

    public Drawable getDrawable() {

        Bitmap bitmap = mImageCache.loadBitmap(mContext, mGifAssets.getGifFramePath(mCurrentFrameIdx));

        BitmapDrawable drawable = new BitmapDrawable(mContext.getResources(), bitmap);

        int width = drawable.getIntrinsicWidth();

        int height = drawable.getIntrinsicHeight();

        drawable.setBounds(0, 0, width > 0 ? width : 0, height > 0 ? height : 0);

        return drawable;

    }

 

    @Override

    public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {

//        Logger.d(this, "draw " + mCurrentFrameIdx);

        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();

    }

}

1. Detect Click Events

We want the animation to start playing when the area is clicked, therefore detecting clicking is necessary. We’ll extend ClickableSpan as below.

private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

When the click event is detected, we start the animation playback, if it’s clicked while the animation is playing, we stop the playback.

2. MyTextView

We glue everything together in the MyTextView as below.

public class MyTextView extends TextView {

    private SpannableStringBuilder mSb = new SpannableStringBuilder();

    private String dummyText = "dummy " + System.currentTimeMillis();

    private Context mContext;

    private SimpleImageMemCache mImageCache;

    private ArrayList<AnimatedImageSpan> mAnimatedImages = new ArrayList<AnimatedImageSpan>();

 

    public MyTextView(Context context) {

        super(context);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs) {

        super(context, attrs);

        mContext = context;

    }

    

    public MyTextView(Context context, AttributeSet attrs, int defStyle) {

        super(context, attrs, defStyle);

        mContext = context;

    }

    

    public void setImageCache(SimpleImageMemCache pImageCache) {

        mImageCache = pImageCache;

    }

 

    @Override

    protected void onDetachedFromWindow() {

        super.onDetachedFromWindow();

        Log.d(this.getClass().getName(), "onDetachedFromWindow ");

        for (AnimatedImageSpan ais : mAnimatedImages) {

            Log.d(this.getClass().getName(), "animation playing " + ais.isPlaying());

            if (ais.isPlaying()) {

                ais.stopRendering();

            }

        }

        mAnimatedImages.clear();

        mSb.clearSpans();

        mSb.clear();

    }

    

    public void appendText(String pStr) {

        mSb.append(pStr);

    }

    

    public void appendAnimation(AnimationAssetsSet pAsset, AnimationSettings pSettings) {

        mSb.append(dummyText);

        AnimatedImageSpan ais = new AnimatedImageSpan(mContext);

        ais.setImageCache(mImageCache);

        ais.setAnimationAssets(pAsset);

        mSb.setSpan(ais, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        AnimationClickableSpan clickSpan = new AnimationClickableSpan(this, ais, pSettings);

        mSb.setSpan(clickSpan, mSb.length() - dummyText.length(), mSb.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);

        mAnimatedImages.add(ais);

    }

    

    public void finishAddingContent() {

        this.setText(mSb);

        this.setMovementMethod(LinkMovementMethod.getInstance());

    }

    

    private static class AnimationClickableSpan extends ClickableSpan {

        AnimatedImageSpan mAnimatedImage;

        AnimationSettings mSettings;

        AnimatedImageUpdateHandler mHandler;

        AnimationClickableSpan(MyTextView pView, AnimatedImageSpan pSpan, AnimationSettings pSettings) {

            mAnimatedImage = pSpan;

            mSettings = pSettings;

            mHandler = new AnimatedImageUpdateHandler(pView);

        }

        

        @Override

        public void onClick(View widget) {

            MyTextView view = (MyTextView) widget;

            if (mAnimatedImage.isPlaying()) {

                mAnimatedImage.stopRendering();

            } else {

                mAnimatedImage.playGif(mSettings, mHandler);

            }

        }

    }

}

3. The Complete Source Code

The complete source code can found at github repo.

Leave a Reply

Your email address will not be published. Required fields are marked *