NightWhistler / HtmlSpanner

Android HTML rendering library with CSS support
http://nightwhistler.github.io/HtmlSpanner/
875 stars 208 forks source link

Lazy Loading within TagNodeHandler #26

Closed JohnyGQD closed 10 years ago

JohnyGQD commented 10 years ago

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!

NightWhistler commented 10 years ago

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:

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

JohnyGQD commented 10 years ago

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

JohnyGQD commented 10 years ago

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.