NightWhistler/HtmlSpanner

Lazy Loading within TagNodeHandler

Closed this issue · 3 comments

I have trouble getting images loaded from the Internet into TextView. Local images are just fine, so there is always the option to delay TextView rendering until images are preloaded, but it does not seem like a good idea since there might be many of them. I have tried this (using koush's Ion):

public class AsyncImageHandler extends TagNodeHandler {
    private Context context;

    public AsyncImageHandler(MyApplication context) {
        this.context = context;
    }

    @Override
    public void handleTagNode(TagNode node, final SpannableStringBuilder builder, final int start, final int end, final SpanStack stack) {
        String src = node.getAttributeByName("src");

        builder.append("\uFFFC");

        Ion.with(context, src).asBitmap().setCallback(new FutureCallback<Bitmap>() {
            @Override
            public void onCompleted(Exception e, Bitmap bitmap) {
                if (bitmap != null) {
                    Drawable drawable = new BitmapDrawable(bitmap);
                    drawable.setBounds(0, 0, bitmap.getWidth() - 1, bitmap.getHeight() - 1);

                    stack.pushSpan(new ImageSpan(drawable), start, end);
                }
            }
        });
    }
}

But obviously, the TextView is already rendered when the image is loaded, so nothing happens (except for the \uFFFC, of course). Is there any way around that, without having to wait for pre-load? Thank you!

Hmmm... this is a tough one.

You are right that the strategy you describe won't work, since the ImageSpan won't even be applied to the stack. The SpanStack is simply a temporary holding place for all the spans, which are then applied to the text in one go.

There are 2 strategies I could think of:

  • Create your own Drawable which allows the underlying image to be updated. Create the ImageSpan while rendering and pass a reference to your custom Drawable to the background task. When the image data comes in, update the Drawable
  • Implement some kind of smart image caching, and then simply re-render the text every time an image is loaded.

Neither strategy is great, but I think they'd be usable.

Thank you, you have been very helpful again :-). What seemed to work is a combination of those two. It still needs some work on my side, but I will paste the current code for other visitors to start with:

public class AsyncImageHandler extends TagNodeHandler {
    private Context context;
    private TextView textView;

    public AsyncImageHandler(MyApplication context, TextView textView) {
        this.context = context;
        this.textView = textView;
    }

    @Override
    public void handleTagNode(TagNode node, final SpannableStringBuilder builder, final int start, final int end, final SpanStack stack) {
        final String src = node.getAttributeByName("src");

        builder.append("\uFFFC");

        Drawable drawable = context.getResources().getDrawable(R.drawable.ic_placeholder);
        drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());

        final DynamicImageSpan imageSpan = new DynamicImageSpan(drawable);
        stack.pushSpan(imageSpan, start, builder.length());

        Ion.with(context, src).asBitmap().setCallback(new FutureCallback<Bitmap>() {
            @Override
            public void onCompleted(Exception e, Bitmap bitmap) {
                Drawable drawable = new BitmapDrawable(context.getResources(), bitmap);
                drawable.setBounds(0, 0, bitmap.getWidth() - 1, bitmap.getHeight() - 1);
                imageSpan.setDrawable(drawable);

                if (textView != null) {
                    textView.setText(textView.getText());
                }
            }
        });
    }
}

To create DynamicImageSpan I had to combine DynamicDrawableSpan and ImageSpan from AOSP to create drawable setter and reset the weak reference to the cached drawable:

public void setDrawable(Drawable drawable) {
    this.mDrawable = drawable;
    this.mDrawableRef = null;
}

Full code here, but it is very messy, as I was gluing those two together, and I am not sure about compatibility with pre-KitKat yet. I will get to those later.
http://pastebin.com/DshxzUHm

Just one last note: The obvious downside is, that you have to create new handler for each TextView. It works for my app, since there is only one per fragment/activity, but might not be great for other devs. However, it should be extremely easy to implement custom callbacks and update the existing instance as you go.