Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

how to combine android-gif-drawable with glide #805

Closed
t2314862168 opened this issue Dec 10, 2015 · 17 comments
Closed

how to combine android-gif-drawable with glide #805

t2314862168 opened this issue Dec 10, 2015 · 17 comments
Labels

Comments

@t2314862168
Copy link

I want to decode gif by android-gif-drawable, but load picture with glide.
android-gif-drawable can pass byte array to decode gif and my code is:

public void test1(final GifImageView gifImageView) throws IOException {
        // String path1 =
        // "http://img.newyx.net/news_img/201306/20/1371714170_1812223777.gif";
        // test gif url
        String path = "http://cdn.duitang.com/uploads/item/201311/20/20131120213622_mJCUy.thumb.600_0.gif";
        URL url = new URL(path);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setConnectTimeout(5 * 1000);
        conn.setRequestMethod("GET");
        if (conn.getResponseCode() == HttpStatus.SC_OK) {
            InputStream sourceIs = conn.getInputStream();
            byte[] b = InputStreamTOByte(sourceIs);
            final GifDrawable gifFromStream = new GifDrawable(b);
            gifImageView.post(new Runnable() {
                @Override
                public void run() {
                    gifImageView.setImageDrawable(gifFromStream);
                }
            });
        }
    }

    public static byte[] InputStreamTOByte(InputStream in) throws IOException {
        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
        int BUFFER_SIZE = 1024;
        byte[] data = new byte[BUFFER_SIZE];
        int count = -1;
        while ((count = in.read(data, 0, BUFFER_SIZE)) != -1) {
            outStream.write(data, 0, count);
        }
        data = null;
        return outStream.toByteArray();
    }

and I use path url to test, it can display normal. Then I use glide to decode the same path url.
My code is this:

// first
// Glide.with(MainActivity.this).load(path).into(imageView);
// second
Glide.with(MainActivity.this)
    .load(path)
    .asGif()
    .toBytes()
    .into(new SimpleTarget<byte[]>() {
        @Override
        public void onResourceReady(final byte[] resource,
                GlideAnimation<? super byte[]> glideAnimation) {
            GifDrawable gifFromStream;
            try {
                gifFromStream = new GifDrawable(resource);
                gifImageView.setImageDrawable(gifFromStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    });

No matter the first way or the second way, pictures can't display.

@TWiStErRob
Copy link
Collaborator

You can squeeze it through Glide's pipeline and take benefit from networking and caching:

// at usage it's as simple as:
glide.load(url).into(imageView);

// make this a field and initialize it once (this is good in list adapters or when used a lot)
GenericRequestBuilder<String, InputStream, byte[], GifDrawable> glide = Glide
        .with(context)
        .using(new StreamStringLoader(context), InputStream.class)
        .from(String.class) // change this if you have a different model like a File and use StreamFileLoader above
        .as(byte[].class)
        .transcode(new GifDrawableByteTranscoder(), GifDrawable.class) // pass it on
        .diskCacheStrategy(DiskCacheStrategy.SOURCE) // cache original
        .decoder(new StreamByteArrayResourceDecoder())  // load original
;

(transcode happens after decode, but in code you can't reorder them)

And the two classes you need to fill the gaps:

public class StreamByteArrayResourceDecoder implements ResourceDecoder<InputStream, byte[]> {
    @Override public Resource<byte[]> decode(InputStream in, int width, int height) throws IOException {
        ByteArrayOutputStream bytes = new ByteArrayOutputStream();
        byte[] buffer = new byte[1024];
        int count;
        while ((count = in.read(buffer)) != -1) {
            bytes.write(buffer, 0, count);
        }
        return new BytesResource(bytes.toByteArray());
    }
    @Override public String getId() {
        return getClass().getName();
    }
}

public class GifDrawableByteTranscoder implements ResourceTranscoder<byte[], GifDrawable> {
    @Override public Resource<GifDrawable> transcode(Resource<byte[]> toTranscode) {
        try {
            return new DrawableResource<GifDrawable>(new GifDrawable(toTranscode.get()));
        } catch (IOException ex) {
            Log.e("GifDrawable", "Cannot decode bytes", ex);
            return null;
        }
    }
    @Override public String getId() {
        return getClass().getName();
    }
}

@t2314862168
Copy link
Author

@TWiStErRob
Thank you for your instant reply.
When I used your method ,I encountered problem.
And My code adapted from yours changing:

public class GifDrawableByteTranscoder implements ResourceTranscoder<byte[], pl.droidsonroids.gif.GifDrawable> {
    @Override
    public Resource<pl.droidsonroids.gif.GifDrawable> transcode(Resource<byte[]> toTranscode) {
        try {
            // because DrawableResource is an abstract class , so i used MyDrawableResource
            return new MyDrawableResource(new pl.droidsonroids.gif.GifDrawable(toTranscode.get()));
        } catch (IOException ex) {
            Log.e("GifDrawable", "Cannot decode bytes", ex);
            return null;
        }
    }

    @Override
    public String getId() {
        return getClass().getName();
    }

    static class MyDrawableResource extends DrawableResource<pl.droidsonroids.gif.GifDrawable> {
        public MyDrawableResource(pl.droidsonroids.gif.GifDrawable drawable) {
            super(drawable);
        }
        @Override public int getSize() {
            return (int) drawable.getInputSourceByteCount();
        }

        @Override public void recycle() {
            drawable.stop();
            drawable.recycle();
        }
    }
}

And i also add the uses-permission to write and read EXTERNAL_STORAGE .
When running the project , it came the error:

java.lang.NullPointerException: SourceEncoder must not be null, try .sourceEncoder(Encoder) or .diskCacheStrategy(NONE/RESULT)

and then if use diskCacheStrategy(NONE) ,it's error message is:

java.lang.NullPointerException: Attempt to invoke virtual method 'android.graphics.drawable.Drawable android.graphics.drawable.Drawable$ConstantState.newDrawable()' on a null object reference.

If you know the solution, please help me,thanks!

@TWiStErRob
Copy link
Collaborator

Hmm, weirdly I didn't get an error for DrawableResource being abstract, you did it right.

Yes, I forgot, that SOURCE needs to know how to save the incoming resource, you need to add: (I also edited above):

.sourceEncoder(new StreamEncoder())

it helps to cache the stream coming from the internet so you can even view the images offline later or they'll just simply load faster.

Re ConstantState.newDrawable() NPE: if you check the stacktrace you'll see what's wrong:

return (T) drawable.getConstantState().newDrawable();

Also see the huge comment on it. Note that the default drawable implementation returns null, so the drawable integrated with DrawableResource must have a constant state, which this one doesn't have. You can try if you can get away with:

class MyDrawableResource implements Resource<GifDrawable> {
    // ... other methods and storage
    @Override public GifDrawable get() { return this.drawable; }
}

@t2314862168
Copy link
Author

@TWiStErRob Thank you for your patience to help!
The problem has been solved, but your answer ignore a point is that if we didn't set cacheDecoder when running the code would throw NPE, so I created the code:

.cacheDecoder(new FileToStreamDecoder<byte[]>(new StreamByteArrayResourceDecoder()))

and my all code is like that:

// make this a field and initialize it once (this is good in list adapters or when used a lot)
GenericRequestBuilder<String, InputStream, byte[], GifDrawable> glide = Glide
        .with(context)
        .using(new StreamStringLoader(context), InputStream.class)
        .from(String.class) // change this if you have a different model like a File and use StreamFileLoader above
        .as(byte[].class)
        .transcode(new GifDrawableByteTranscoder(), GifDrawable.class) // pass it on
        .diskCacheStrategy(DiskCacheStrategy.SOURCE) // cache original
        .decoder(new StreamByteArrayResourceDecoder())  // load original
        .sourceEncoder(new StreamEncoder())
        .cacheDecoder(new FileToStreamDecoder<byte[]>(new StreamByteArrayResourceDecoder()));
// just load local gif , even your picture suffix is jpg or png , it also can treate it as gif to show
GenericRequestBuilder<File, InputStream, byte[], GifDrawable> glideLocal = Glide
        .with(context)
        .using(new StreamFileLoader(context), InputStream.class)
        .from(File.class) // change this if you have a different model like a File and use StreamFileLoader above
        .as(byte[].class)
        .transcode(new GifDrawableByteTranscoder(), GifDrawable.class) // pass it on
        .diskCacheStrategy(DiskCacheStrategy.SOURCE) // cache original
        .decoder(new StreamByteArrayResourceDecoder())  // load original
        .sourceEncoder(new StreamEncoder())
        .cacheDecoder(new FileToStreamDecoder<byte[]>(new StreamByteArrayResourceDecoder()));
String localGifUrl =  Environment.getExternalStorageDirectory().toString() + "/bfc.jpg";
File file = new File(localGifUrl);
if (file.exists() && file.isFile()) {
    glideLocal.load(file).into(network_gifimageview0);
} 
// at usage it's as simple as:
//glide.load(path0).into(network_gifimageview0);
glide.load(path1).into(network_gifimageview1);
glide.load(path2).into(network_gifimageview2);
glide.load(path3).into(network_gifimageview3);
glide.load(path4).into(network_gifimageview4);

And the fixed drawable resource:

static class MyDrawableResource implements Resource<pl.droidsonroids.gif.GifDrawable> {
    private pl.droidsonroids.gif.GifDrawable drawable;

    public MyDrawableResource(pl.droidsonroids.gif.GifDrawable gifDrawable) {
        this.drawable = gifDrawable;
    }

    @Override
    public GifDrawable get() {
        return drawable;
    }

    @Override
    public int getSize() {
        return (int) drawable.getInputSourceByteCount();
    }

    @Override
    public void recycle() {
        drawable.stop();
        drawable.recycle();
    }
}

I also found that it was more slowly than the way using InputStream directly, I wonder it happened because using the diskCacheStrategy and decode?

@TWiStErRob
Copy link
Collaborator

Nice, well done. You're now an advanced user. All this (and more) is hidden behind the curtains when you call the usual simple Glide.with.load.into.

You can get rid of the duplicate code if you use glide.load(localGifUrl).into(iv0) as load(String) can handle a lot of things (file path, http, https, resource, content uri, ...)

Regarding speed: it may be the cache, Glide saves input to SOURCE cache first and the starts loading from there, this is for consistency. You can use NONE if that's what you want, but beware that future loads will go to the network again. You have to decide if you can live with the initial slowness to have it faster later. Mind you for local files it's not advised to use SOURCE cache as it just copies the file on the SD card.

You could also skip the file existence checks, Glide will silently fail if it can't load: leaving the ImageView empty if the file is not there or invalid. Those two checks are blocking I/O, which you shouldn't do on UI thread.

Incorporating all my comments, consider:

GenericRequestBuilder<String... glide = // that long Glide init
String localGifUrl =  Environment.getExternalStorageDirectory().toString() + "/bfc.jpg";
glide.clone().discCacheStrategy(NONE).load(localGifUrl).into(network_gifimageview0);
glide.clone().load(path1).into(network_gifimageview1);
...

This way you get caching where it's needed.

@t2314862168
Copy link
Author

@TWiStErRob
If I have any problems later I will continue to ask , and then i decide to close the issue ,thank you very much for your suggestion!

@favn1585
Copy link

favn1585 commented Sep 2, 2016

Thank you guys. This info is very useful! I think that we can even move it to Wiki!

@ghost
Copy link

ghost commented Nov 24, 2016

When used above methods in recyclerview, not all images are animating.
If I render same gif from network, then only one of them is animating, but this is not the case when rendering different gifs
@TWiStErRob Could you please help me with that?

@favn1585
Copy link

@ranjit-parity you should load each gif with some unique string. You can just add some random string at the end of url with unique param

@ghost
Copy link

ghost commented Nov 24, 2016

@favn1585 I dont think that's correct solution, why would i change my gif url.
There must be something that I'm missing from above code. Maybe @t2314862168 or @TWiStErRob can tell.

@favn1585
Copy link

We had the same issue and we fix it by complete url with postId. So it's not random url for each row

@ghost
Copy link

ghost commented Nov 24, 2016

@favn1585 Your way works but is slowing down the loading time, takes more time to load image than before, can you suggest more alternatives?

@favn1585
Copy link

favn1585 commented Nov 24, 2016

It cannot slow down loading time. This is just because you load each image for different url even if you have 2 same images in neighbour items. I spend some time on it a this is the only solution that we have for now

@TWiStErRob
Copy link
Collaborator

@ranjit-parity GifDrawable doesn't support the standard Drawable contract of newDrawable with shared state (=frames). See how DrawableResource works and compare it to the above code. In your case, you can't get away with GifDrawable get() { return this.drawable; } if you want the same image multiple times. Every time you load the same image after it has loaded once, get is called and that "newly created" Drawable is used. As far as I know a Drawable can only be used in one View at a time, because there are callbacks registered.

@favn1585's url hacking (equiv API is .signature(new StringSignature(blah))) doesn't work as well, because adding a signature/changing the url forces Glide to re-request the image from the network. It cannot hit the SOURCE cache.

As I see it now, you cannot fix this inside Glide, nor really work around it. The GifDrawable needs to be fixed, to support the Android contracts. As an alternative you can try to implement "cloning" outside the GifDrawable. You need to implement get() correctly. One way I see is to extend GifDrawable, and store a reference to the byte[] passed in. In your Glide Resource class uses that saved field to create a totally new instance of GifDrawable. However it feels like this may bring about more problems, for example freeing the resources.

Note that all this started, because this use case is trying to integrate a library with Glide, which supports the same features (loading from different sources). Essentially in this use case you're using Glide as a networking library, and the using another library to visualize what you've downloaded. Glide does not support this use case in all cases and my above code is more of a demonstration of the power of the Glide API. Whenever I use the word "squeeze" it means a workaround for something that potentially has better alternative solutions.

@ghost
Copy link

ghost commented Nov 25, 2016

@TWiStErRob I'm going with snapshot-4.0 version and its doing great. So using glide for final code changes.

@satheeshwaran
Copy link

satheeshwaran commented May 14, 2017

Hi I am using Glide to load GIFs in my app on a recycler view and it was laggy when using vanilla Glide so I ended up with this thread and now GIFs load super fast.


GenericRequestBuilder<String, InputStream, byte[], GifDrawable> glide = glideRequest
                        .using(new StreamStringLoader(MemeCategoryDetailsActivity.this), InputStream.class)
                        .from(String.class) // change this if you have a different model like a File and use StreamFileLoader above
                        .as(byte[].class)
                        .transcode(new GifDrawableByteTranscoder(),GifDrawable.class) // pass it on
                        .diskCacheStrategy(DiskCacheStrategy.SOURCE) // cache original
                        .decoder(new StreamByteArrayResourceDecoder())  // load original
                        .sourceEncoder(new StreamEncoder())
                        .cacheDecoder(new FileToStreamDecoder<byte[]>(new StreamByteArrayResourceDecoder()));

And I load GIfs like this,



glide
.load(category.memes.get(position).memeRemoteURL)
.placeholder(R.drawable.ic_gif)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.into(holder.memeImage);

Now I want to load the thumbnail of the GIF before it downloads the actual GIF but I am not able to add .thubmnail to the GenericRequestBuilder glide object.

Error:(104, 29) error: no suitable method found for thumbnail(DrawableRequestBuilder)
method GenericRequestBuilder.thumbnail(GenericRequestBuilder<?,?,?,GifDrawable>) is not applicable
(argument mismatch; DrawableRequestBuilder cannot be converted to GenericRequestBuilder<?,?,?,GifDrawable>)
method GenericRequestBuilder.thumbnail(float) is not applicable
(argument mismatch; DrawableRequestBuilder cannot be converted to float)

I want to add thumbnails like mentioned in the below threads,

https://futurestud.io/tutorials/glide-placeholders-fade-animations

#749

@rexih
Copy link

rexih commented Aug 14, 2019

@ranjit-parity GifDrawable doesn't support the standard Drawable contract of newDrawable with shared state (=frames). See how DrawableResource works and compare it to the above code. In your case, you can't get away with GifDrawable get() { return this.drawable; } if you want the same image multiple times. Every time you load the same image after it has loaded once, get is called and that "newly created" Drawable is used. As far as I know a Drawable can only be used in one View at a time, because there are callbacks registered.

@favn1585's url hacking (equiv API is .signature(new StringSignature(blah))) doesn't work as well, because adding a signature/changing the url forces Glide to re-request the image from the network. It cannot hit the SOURCE cache.

As I see it now, you cannot fix this inside Glide, nor really work around it. The GifDrawable needs to be fixed, to support the Android contracts. As an alternative you can try to implement "cloning" outside the GifDrawable. You need to implement get() correctly. One way I see is to extend GifDrawable, and store a reference to the byte[] passed in. In your Glide Resource class uses that saved field to create a totally new instance of GifDrawable. However it feels like this may bring about more problems, for example freeing the resources.

Note that all this started, because this use case is trying to integrate a library with Glide, which supports the same features (loading from different sources). Essentially in this use case you're using Glide as a networking library, and the using another library to visualize what you've downloaded. Glide does not support this use case in all cases and my above code is more of a demonstration of the power of the Glide API. Whenever I use the word "squeeze" it means a workaround for something that potentially has better alternative solutions.

It cannot slow down loading time. This is just because you load each image for different url even if you have 2 same images in neighbour items. I spend some time on it a this is the only solution that we have for now

================
i have the same demand of combining glide and android-gif-drawable. after checking the issue, i learnt how to use glide advance.

when facing the problem of using same gif url in list, the above solution seems not work, so i tried anothor way to solve the problem:

use a CustomViewTarget to load gif byte[] into GifDrawable;
dont use dontAnimate (then it will use glide gif decoder to retrive the byte[] resource)

class GifViewTarget(
    view: ImageView
) : CustomViewTarget<ImageView, ByteArray>(view) {

    override fun onResourceCleared(placeholder: Drawable?) {
        view.setImageDrawable(placeholder)
    }

    override fun onLoadFailed(errorDrawable: Drawable?) {
        view.setImageDrawable(errorDrawable)
    }

    override fun onResourceReady(resource: ByteArray, transition: Transition<in ByteArray>?) {
        try {
            val gifDrawable = GifDrawable(resource)
            // layoutImageView(gifDrawable)
            view.setImageDrawable(gifDrawable)
        } catch (e: IOException) {
            e.printStackTrace()
        }
    }
}

then load gif to imageview like this

Glide
                .with(this)
                .`as`(ByteArray::class.java)
                .load("url")
                .apply(RequestOptions().set(GifOptions.DISABLE_ANIMATION, false))
                .into(GifViewTarget(iv))

when Glide select the decoder,it will iterate all the candidates ,in
com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder

  @Override
  public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
    return !options.get(GifOptions.DISABLE_ANIMATION)
        && ImageHeaderParserUtils.getType(parsers, source) == ImageType.GIF;
  }

when animate not canceled and byteArray begins with gif feature , handles will return true and the gif decoder will be selected

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants