6

I have written a custom span for SpannableStrings "ImageURLSpan" that extends ImageSpan.

  • The ImageSpan is configured with a Drawable "RemoteImageDrawable" that renders a static placeholder, loads an image from the internet, then when the image has finished loading, it should render the loaded bitmap instead of the placeholder. That would happen if the draw() method of the drawable is called again (by the parent view?)

My problem is, at the drawable level, I don't know how to trigger an 'auto-refresh' at any point in the future (once the image is loaded). I have tried to call the parent view's invalidate with non-consistent results: Sometimes the draw method of the drawable is called again -so it's properly refreshed- and sometimes it's not. It's totally random!

What's the correct approach for a drawable that changes its content to "invalidate" itself? shouldn't the invalidation of the parent View cause all child drawables to invalidate? Do I need to use the Drawable callbacks or are they not related to my problem?

Please find attached relevant code:

public class ImageUrlSpan extends ImageSpan {

    public ImageUrlSpan(final View parentView, String imageUrl, int width, int height) {
            super(
                    new RemoteImageDrawable(
                            parentView, imageUrl, width, height, new ImageLoaders.OnDrawableDownloadListener() {

                                @Override
                                public void onDrawableLoaded(Drawable bitmap) {
                                    parentView.invalidate();
                                    // this works sometimes
                                    if (Conf.LOG_ON) Log.v(TAG, "Invalidating parent view... will that refresh myself? "+parentView);
                                }
                            }
                    ));
        }
    }

About the code of RemoteImageDrawable, you only have to know

  • it's a simple drawable that just paints a placeholder, schedules a image load, then when the image is ready, calls the listener above.

  • Whenever its draw method is called again (supposedly as a consequence of the invalidate in the listener above), the loaded bitmap would be rendered instead of the placeholder.

  • I omit the source code for simplicity, the problem is as simple as the draw method not being called most of the times. It puzzles me that sometimes it's called !

The following stack trace depicts the callstack of the first call to the Drawable's DRAW. You can see it comes from View.draw() -> StaticLayout.draw() -> RemoteImageDrawable.draw().

W/System.err(26142):    at com.regaliz.custom.RemoteImageDrawable.draw(RemoteImageDrawable.java:71)
W/System.err(26142):    at android.text.style.DynamicDrawableSpan.draw(DynamicDrawableSpan.java:107)
W/System.err(26142):    at android.text.TextLine.handleReplacement(TextLine.java:854)
W/System.err(26142):    at android.text.TextLine.handleRun(TextLine.java:937)
W/System.err(26142):    at android.text.TextLine.drawRun(TextLine.java:395)
W/System.err(26142):    at android.text.TextLine.draw(TextLine.java:193)
W/System.err(26142):    at android.text.Layout.drawText(Layout.java:348)
W/System.err(26142):    at android.text.Layout.draw(Layout.java:205)
W/System.err(26142):    at android.text.Layout.draw(Layout.java:183)
W/System.err(26142):    at com.regaliz.gui.views.SimpleTextView.drawTextLayout(SimpleTextView.java:347)
W/System.err(26142):    at com.regaliz.gui.views.SimpleTextView.onDraw(SimpleTextView.java:310)
W/System.err(26142):    at com.regaliz.gui.views.helper.SimpleTextViewMarquee.onDraw(SimpleTextViewMarquee.java:114)
W/System.err(26142):    at android.view.View.draw(View.java:13877)
W/System.err(26142):    at android.view.View.getDisplayList(View.java:12815)
W/System.err(26142):    at android.view.View.getDisplayList(View.java:12859)
W/System.err(26142):    at android.view.View.draw(View.java:13593)
W/System.err(26142):    at android.view.ViewGroup.drawChild(ViewGroup.java:2928)
W/System.err(26142):    at android.view.ViewGroup.dispatchDraw(ViewGroup.java:2797)

However, when I issue an invalidate, the drawable draw() is not called, so somewhere along this path, the view decides everything's already drawn :(

V/RemoteImageDrawable(26142): *** Remote bitmap loaded com.regaliz.util.AssetLoader$MeasuredBitmapDrawable@41d60798
V/ImageURLSpan(26142): Invalidating  parent view after loaded... will it work? com.regaliz.gui.views.helper.SimpleTextViewMarquee{41f6c898 GFED..CL ........ 0,0-719,663}
V/ImageURLSpan(26142): Invalidating parent view end.
rupps
  • 9,712
  • 4
  • 55
  • 95
  • 'What's the correct approach for a drawable that changes its content to "invalidate" itself?' its Drawable.invalidateSelf but i'm not sure if it makes any help in your case – pskink Sep 02 '14 at 15:16
  • Just tried it. I properly implemented Drawable.Callback at the View level, then when I issue invalidateSelf, the invalidateDrawable method gets called, so there's where I invalidate the View. But after doing that, I get the same behaviour: The drawable is not refreshed ! – rupps Sep 02 '14 at 15:29
  • i had similar problems as well, did you try to re-set the span with the same start, end and flags? – pskink Sep 02 '14 at 15:33
  • ok how do you "replace" the placeholder with the final image? – pskink Sep 02 '14 at 15:57
  • I found what is happening !! but yet to find how to solve. When I call StaticLayout.draw, its first line checks that the static layout is within the canvas cliprect, and somehow it decides it isn't, so just returns without painting ... gonna dig into the issue a little ! – rupps Sep 02 '14 at 16:18
  • hmm i used simple tv.invalidate() (not Drawable.callback stuff) and it works, see: http://pastebin.com/1b52rXLE – pskink Sep 02 '14 at 16:20
  • your code is exactly w/I had before the Callback thing. In my case worked but just sometimes. The callback looks like the same, it's just when I set the drawable's callback to the view, a call in the drawable to invalidateSelf automatically triggers a View.invalidate. unfortunately my problem seems that the StaticLayout I use to render text (I don't use a textview) when reaches the draw method, decides to exit instead of refreshing because of something related to the clipping canvas bounds, which I don't use by the way, .... thanks a lot for your code anyways, cool to see i'm doing the same! – rupps Sep 02 '14 at 16:30
  • hmmm I think I finally know the root cause: The text views are inside a ViewSwitcher with an animation -i forgot to mention this key point-, so when the images are loaded and the invalidate is triggered, it happens that that view is still hidden. Then the viewswithcer makes the transition and all, but the layout has not been updated because the view was hidden when the image loaded, and ViewSwitcher does not issue an extra invalidate. convoluted huh? – rupps Sep 02 '14 at 16:54
  • a bit convoluted, but does it work now? – pskink Sep 02 '14 at 16:57
  • I'm trying to figure out when exactly to call invalidate and it should work: The real problem is the view's visibility is GONE when the TextSwitcher makes the setText, so the spans are not refreshed. I have to extend TextSwitcher so, after calling setText, issues an extra invalidate when their internal TextView's have been made visible. I'm extending the TextSwitcher class for that, will report if I get it to work :S – rupps Sep 02 '14 at 17:03
  • imho its a bit brownian movement, since finally you set visibility from GONE to VISIBLE and StaticLayout will call draw() which will draw text and spans – pskink Sep 02 '14 at 17:09
  • You are very right! I suspect I have an extra-recreation of the layout when visibility chg from GONE to VIS that may well overwrite the drawable with the placeholder again, that would explain some flicker I didn't know wh/came from. So what happens is: I set the text -> the staticlayout is drawn on a GONE view -> the image is loaded -> invalidate is called but does nothing bcs. view is gone -> View is made visible -> staticlayout is invalidated and the placeholder is drawn again. It also explains why it works sometimes: When the image is not cached, the drawable invalidation happens after. – rupps Sep 02 '14 at 17:17
  • nice investigation ;-) btw where did you work at lucent: nepperville, hilversum? – pskink Sep 02 '14 at 17:24
  • I was mostly based in madrid, spain & moving occassionally to New Jersey & St. Petersburg once it became -sadly- alcatel :P yeah cool investigation, thank you for pointing the extra-draw call when switching GONE->Visible, that is what caused strange re-callings to draw – rupps Sep 02 '14 at 17:28
  • ok so the problem is solved now? – pskink Sep 02 '14 at 18:02
  • i'm re-strucuring the drawable class so the loaded bitmaps are stored in a parent view's HashMap rather than in the drawable itself, because the visibility change sometimes/always? cause the spans to be recreated. this way it won't matter because the drawable can ask the parent view for a stored bitmap, in theory it would solve the problem ... but it's getting a little trickier .. – rupps Sep 02 '14 at 18:11
  • SOLVED !!! it's amazing what caused all the mess was that unexpected Visiblity change -calls to staticlayout.draw were duplicated with a nanosecond delay-. In fact, in the Text View OnDraw, if I skip the whole onDraw when Visibility is GONE, things work straight ahead because the onDraw is called automatically by setVisibility VISIBLE! Coupled with the storing of drawables at the View level, I think the routine is now bullet-proof! Thanks for pointing the fact that Visibility change triggers onDraw, I wouldnt have found it! – rupps Sep 02 '14 at 18:33
  • nice to hear that :-) btw if you want some fancy stuff with spans take a look at my answer here: http://stackoverflow.com/questions/21933671/get-suggestion-for-line-breaks-based-on-text-font-and-available-width – pskink Sep 02 '14 at 18:39
  • Also, it works much better if on the TextSwitcher i modify setText so it *posts* an invalidate right after showing the next view. I am using a custom ultra-lightweight textview with a custom textswitcher modified to use them instead of the stock heavy textviews. – rupps Sep 02 '14 at 18:47
  • Taking a look at your code now, I see you're doing crazy things like adding whole views to the span, seems very powerful and advanced, needs a detailed study from my side but I feel it's gonna be terribly useful! you can dynamically instantiate UI components, even a whole UI, from a setText in a TextView! – rupps Sep 02 '14 at 18:49
  • quite nice, huh? it of course should be a custom FrameLayout or ViewGroup that adds a main TextView and the extra Buttons/Spinners etc but the idea would be the same, and it looks MUCH better that ordinary ClickableSpans/UrlSpans ;-) – pskink Sep 02 '14 at 18:59
  • yup ultra-nice, and it comes really handy to me, as my project is a visual application designer that allows you to drag and drop components and create kind of interactive magazines.. take a look at some screenshots if you want, you'll figure out how interesting is your piece of code to me : https://www.facebook.com/funq.tv – rupps Sep 02 '14 at 19:06
  • so if you found it interesting i suggest to make a custom ViewGroup that implements onLayout positioning the main TextView and the extra View stuff based on what ReplacementSpan suggest in draw() method – pskink Sep 02 '14 at 19:23
  • hmm the applications can be endless... just compiled your example ... I'm thinking to use it for implementing multi-column textviews, something pretty difficult, and with this trick it can be very clean!! recursive textviews, love the idea ;O – rupps Sep 02 '14 at 19:39

0 Answers0