Glide: Preload next image for single ImageView

Created on 5 Jan 2016  路  23Comments  路  Source: bumptech/glide

I have a specific use case where I animate an ImageView it does a flip animation and I see the next image. The issue is that while the next image is loading there is a bunch of white space.

I tried this implementation here:

Glide
.with(context)
.load(imageUrl)
.thumbnail(Glide // this thumbnail request has to have the same RESULT cache key
        .with(context) // as the outer request, which usually simply means
        .load(oldImage) // same size/transformation(e.g. centerCrop)/format(e.g. asBitmap)
        .fitCenter() // have to be explicit here to match outer load exactly
)
.listener(new RequestListener<String, GlideDrawable>() {
    @Override public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
        if (isFirstResource) {
            return false; // thumbnail was not shown, do as usual
        }
        return new DrawableCrossFadeFactory<Drawable>(/* customize animation here */)
                .build(false, false) // force crossFade() even if coming from memory cache
                .animate(resource, (ViewAdapter)target);
    }
})
//.fitCenter() // this is implicitly added when .into() is called if there's no scaleType in xml or the value is fitCenter there
.into(imageView);

oldImage = imageUrl;

But while this makes the previous image stick around and not disappear when the animation occurs and the next image loads, the previous image sticks around and the user is kind of stuck waiting looking at the previous image AFTER the animation is complete which is somewhat awkward.

I was wondering if there is a way to preload the next image and save it in memory and upon animation start just set the next image in Glide.with(...).load(mNextImageRequest)....

Is this possible?

question

Most helpful comment

Here is my refactor. It also accounts for white-ness, see comment in startNext.
Notice that Glide.with is only called once! (and copied 3 times, after that it's reused).

Full version: https://github.com/TWiStErRob/glide-support/tree/master/src/glide3/java/com/bumptech/glide/supportapp/github/_861_preload_loop

public class LoadCycler<M, T extends Drawable> {
    private Iterable<M> models = Collections.emptyList();
    private Iterator<M> data;
    private GenericRequestBuilder<M, ?, ?, T> prev, curr, next;
    private final Target<? super T> target;
    private GlideAnimationFactory<Drawable> factory = new DrawableCrossFadeFactory<>();
    private boolean isLoading = false;

    public LoadCycler(GenericRequestBuilder<M, ?, ?, T> request, ImageView imageView) {
        this(request, new DrawableImageViewTarget(imageView));
    }
    public LoadCycler(GenericRequestBuilder<M, ?, ?, T> request, Target<? super T> target) {
        this.target = target;
        prev = copy(request);
        curr = copy(request);
        next = copy(request);
    }
    private GenericRequestBuilder<M, ?, ?, T> copy(GenericRequestBuilder<M, ?, ?, T> request) {
        return request.clone().dontAnimate().thumbnail(null).listener(null).load(null);
    }

    protected M getNextModel() {
        if (data == null || !data.hasNext()) {
            data = models.iterator();
        }
        return data.hasNext()? data.next() : null;
    }

    private void rotate() {
        GenericRequestBuilder<M, ?, ?, T> temp = prev;
        prev = curr;
        curr = next;
        next = temp;
    }

    public boolean startNext() {
        if (isLoading) {
            return false; // prevent showing white when next is called too fast
            // the current load must have finished before going to the next one
            // this ensures that the current load will always be in memory cache so it can be used as thumbnail
        }
        rotate();
        prev.thumbnail(null).listener(null);
        next.load(getNextModel());
        @SuppressWarnings("unchecked") Target<T> target = (Target)this.target;
        isLoading = true;
        curr.thumbnail(prev).listener(animator).into(target);
        return true;
    }

    public void setData(Iterable<M> models) {
        this.models = models != null? models : Collections.<M>emptyList();
        prev.load(null);
        curr.load(null);
        next.load(getNextModel());
    }

    public void setAnimation(GlideAnimationFactory<Drawable> factory) {
        this.factory = factory;
    }

    private final SizeReadyCallback preloader = new SizeReadyCallback() {
        @Override public void onSizeReady(int width, int height) {
            next.preload(width, height);
        }
    };

    private final RequestListener<M, T> animator = new RequestListener<M, T>() {
        @Override public boolean onException(Exception e, M model, Target<T> target, boolean isFirstResource) {
            return false;
        }
        @Override public boolean onResourceReady(T resource, M model, Target<T> target, boolean ignore, boolean thumb) {
            isLoading = false;
            target.getSize(preloader); // calls onSizeReady
            return factory != null && factory.build(false, thumb).animate(resource, (GlideAnimation.ViewAdapter)target);
        }
    };
}

usage:

public class TestFragment extends Fragment {
    private ImageView imageView;
    private LoadCycler<String, ?> cycler;

    @Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... }

    @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        imageView = (ImageView)view.findViewById(R.id.image);
        setupCycler();
    }

    @Override public void onStart() {
        super.onStart();
        loadDataAsync();
    }

    private void setupCycler() {
        cycler = new LoadCycler<>(Glide.with(this).fromString().centerCrop(), imageView);
        //cycler.setAnimation(new DrawableCrossFadeFactory<>(1000));
    }

    private void loadDataAsync() {
        new AsyncTask<Void, Void, List<String>>() {
            @Override protected List<String> doInBackground(Void... params) {
                return Arrays.asList(
                        "http://placehold.it/200x200.gif?text=0",
                        "http://placehold.it/200x200.gif?text=1",
                        "http://placehold.it/200x200.gif?text=2",
                        "http://placehold.it/200x200.gif?text=3"
                );
            }
            @Override protected void onPostExecute(List<String> data) {
                cycler.setData(data);
                next();
            }
        }.execute();
    }

    private void next() {
        if (!isResumed()) return;
        cycler.startNext();
        //autoAdvance();
    }

    private void autoAdvance() {
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override public void run() { next(); }
        }, 2000);
    }
}

Obviously async loading is not required, it was just more real-life scenario; you can just do:

cycler = new Cycler...
cycler.setData(...);
cycler.startNext();

usage with custom model (as you say you have objects):

public class Item {
    private final String imageUrl;
    public Item(String imageUrl) { this.imageUrl = imageUrl; }
    public String getImageUrl() { return imageUrl; }
}
// in GlideModule: glide.register(Item.class, InputStream.class, new ItemLoader.Factory());
public class ItemLoader extends BaseGlideUrlLoader<Item> {
    public ItemLoader(Context context) { super(context); }
    @Override protected String getUrl(Item model, int width, int height) { return model.getImageUrl(); }

    public static class Factory implements ModelLoaderFactory<Item, InputStream> {
        @Override public ModelLoader<Item, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new ItemLoader(context);
        }
        @Override public void teardown() { }
    }
}

// inside fragment change:

private LoadCycler<Item, ?> cycler;

.from(Item.class) // instead of .fromString()

new AsyncTask<Void, Void, List<Item>>() { // note List's generic argument
return Arrays.asList( new Item("...") ... // pass objects, not Strings

All 23 comments

There is a .preload() method, so I guess you can fire off the next image in listener.onResourceReady so by the time the trigger comes to go to the next image, it'll be most likely in disk and memory cache:

target.getSize(new SizeReadyCallback() { // make sure that the preloaded size matches the ImageView
    @Override public void onSizeReady(int width, int height) {
        Glide
            .with(context)
            .load(nextImage)
            .fitCenter() // you have to be explicit about the transformation and match what's in the XML
            .preload(width, height);
    }
});

So in the end you'll need to store the "previous" (thumbnail) the "current" (load) and the "next" (preload) urls every time.

I was trying to save the thumbnail, and current the following way, but it wasn't working properly:

if (mPreloadNext == null)
    DrawableRequestBuilder<String> mPreloadNext = Glide.with(context)
                                                     .load(nextUrl)
                                                     .centerCrop();

But your approach is different, are you talking about something like this?

Glide.with(context)
    .load(currentUrl)
    .thumbnail(/* setup thumbnail */)
    .listener(/* setup listener */)
    .centerCrop()
    .preload()
    .getSize(new SizeReadyCallback() {
           @Override public void onSizeReady(int width, int height) {
                Glide.with(context)
                      .load(nextImageUrl)
                      .centerCrop() 
                      .preload(width, height);
           });

Unclear about the following line:

so I guess you can fire off the next image in listener.onResourceReady....

Actually I see what you're saying now the following works! Except for a minor issue, the initial image loaded is always the next image, after that everything else is fine:

Glide.with(context)
.load(currentUrl)
.thumbnail(/* setup thumbnail */)
.listener(new RequestListener<String, GlideDrawable>() {
    @Override public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
        if (isFirstResource) {
            return false; // thumbnail was not shown, do as usual
        }

        // Preload next image
       Glide.with(context)
                 .load(nextImageUrl)
                 .centerCrop()
                 .preload()
                 .getSize(new SizeReadyCallback() {
                              @Override
                              public void onSizeReady(int width, int height) {
                                        Glide.with(context)
                                  .load(nextImageUrl)
                                  .centerCrop() 
                                  .preload(width, height);
                   });

        return new DrawableCrossFadeFactory<Drawable>(/* customize animation here */)
            .build(false, false) // force crossFade() even if coming from memory cache
            .animate(resource, (ViewAdapter)target);)
     })
.centerCrop()
.into(imageView);

No, that's not what I meant, preload() without args is wasteful. preload().getSize() will result in width == Target.SIZE_ORIGINAL && height == Target.SIZE_ORIGINAL which won't ever match a load to an image view with proper sizing. Your "Preload next image" should look like this (you already have a target as method arg):

target.getSize(new SizeReadyCallback() {
      @Override
      public void onSizeReady(int width, int height) {
          Glide.with(context)
              .load(nextImageUrl)
              .centerCrop() 
              .preload(width, height);
});

I was curious, so here's a full Fragment that demonstrates:

  • on first load it scales in (initAnim())
  • when tapping the image it advances to next via crossfade (onClick)
  • each load preloads the next one (onResourceReady)
  • nested anonymous inner classes are minimized (implements)
  • duplicate code is minimized (prev, curr, next)
public class _861_preload_loop extends Fragment implements RequestListener<String, GlideDrawable>, SizeReadyCallback {
    private static final DrawableCrossFadeFactory<Drawable> FACTORY = new DrawableCrossFadeFactory<>(initAnim(), 1000);
    private static Animation initAnim() {
        ScaleAnimation anim = new ScaleAnimation(0, 1, 0, 1, ScaleAnimation.RELATIVE_TO_SELF, .5f, ScaleAnimation.RELATIVE_TO_SELF, .5f);
        anim.setDuration(2000);
        return anim;
    }

    private List<String> urls = Arrays.asList(
            "http://placehold.it/200x200?text=0",
            "http://placehold.it/200x200?text=1",
            "http://placehold.it/200x200?text=2",
            "http://placehold.it/200x200?text=3"
    );
    private DrawableRequestBuilder<String> prev, curr, next;
    private Iterator<String> url;
    private ImageView imageView;

    @Override public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // TODO to handle rotation you may need to use absolute positioning instead of Iterator so you can save/restore the index
        prev = null;
        curr = null;
        next = createNextLoad();
    }

    @Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        imageView = new ImageView(container.getContext());
        imageView.setLayoutParams(new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
        imageView.setScaleType(ScaleType.CENTER_CROP);
        return imageView;
    }

    @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        load();
        imageView.setOnClickListener(new OnClickListener() {
            @Override public void onClick(View v) { load(); }
        });
    }

    private DrawableRequestBuilder<String> createNextLoad() {
        if (url == null || !url.hasNext()) url = urls.iterator(); // restart by wrapping around
        return Glide
                .with(this)
                .load(url.next())
                .dontAnimate() // there's a custom animation in onResourceReady
        ;
    }

    private void load() {
        // take a step forward
        prev = curr;
        curr = next;
        next = createNextLoad();

        // load curr, fading from prev
        if (prev != null) prev.thumbnail(null).listener(null); // forget these to prevent extra work
        curr.thumbnail(prev).listener(this).into(imageView);
    }

    @Override public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
        target.getSize(this); // calls onSizeReady
        return FACTORY.build(false, isFirstResource).animate(resource, (ViewAdapter)target);
    }

    @Override public void onSizeReady(int width, int height) {
        // TODO make sure transformation here matches ImageView's scaleType
        next.centerCrop().preload(width, height);
    }
}

Hey @TWiStErRob I consolidated your code into a custom builder class would you like for me to do a pull request on it?

It looks like this:

    GlidePreload.with(itemView.getContext())
                    .request(prevUrl, currentUrl, nextUrl) // Can specify as many urls as you like here
                    .createNextLoad()
                    .loadInto(imageView);

Or maybe you have other suggestions?

You can't pull on a comment (the above code is not in a GitHub repo [yet]), but you can share it here as a comment like I did if you think it would be useful for others. I don't see how your builder reuses existing loads though: the version I gave creates only n requests for showing n images and links them together as needed.

Actually I just realized that you can solve it with 3 requests instead of n if you just rotate them around:

void load() {
DrawableRequestBuilder<String> temp = prev;
prev = curr;
curr = next;
next = temp;
prev.thumbnail(null).listener(null);
next.load(it.next());
curr.thumbnail(prev).listener(this).into(imageView);
}

This works because you can change a model on an existing request and into() it again.

Ahh ok makes sense this is what I implemented:

public class GlidePreload implements RequestListener<String, GlideDrawable>, SizeReadyCallback {

    // Our CrossFade animation factory
    private static final DrawableCrossFadeFactory<Drawable> FACTORY = new DrawableCrossFadeFactory<>();

    // Our request loads
    private Context mContext;
    private List<String> mUrls = new ArrayList<>();
    private DrawableRequestBuilder<String> mPrevious, mCurrent, mNext;
    private Iterator<String> mUrl;

    private GlidePreload() {}

    /*********************************************************************************************
     *  Public API Method(s)
     *********************************************************************************************/

    public static IRequest with(Context context) {
        return new GlidePreload.PreloadBuilder(context);
    }

    /*********************************************************************************************
     *  Private Inline Method(s)
     *********************************************************************************************/

    private DrawableRequestBuilder<String> createNextLoad() {
        if (mUrl == null || !mUrl.hasNext())
            mUrl = mUrls.iterator();
        return Glide.with(mContext)
                .load(mUrl.next())
                .dontAnimate();
    }

    private void loadInto(ImageView imageView) {
        // Take a step foward
        this.mPrevious = mCurrent;
        this.mCurrent = mNext;
        this.mNext = createNextLoad();

        // Load the current request, fading from the previous request
        if (mPrevious != null)
            mPrevious.thumbnail(null)
                    .listener(null);

        mCurrent.thumbnail(mPrevious)
                .listener(this)
                .into(imageView);
    }

    /*********************************************************************************************
     *  RequestListener Methods
     *********************************************************************************************/

    @Override
    public boolean onException(Exception e, String model, Target<GlideDrawable> target, boolean isFirstResource) {
        return false;
    }

    @Override
    public boolean onResourceReady(GlideDrawable resource, String model, Target<GlideDrawable> target, boolean isFromMemoryCache, boolean isFirstResource) {
        target.getSize(this); // This will call onSizeReady
        return FACTORY.build(false, isFirstResource).animate(resource, (GlideAnimation.ViewAdapter) target);
    }

    @Override
    public void onSizeReady(int width, int height) {
        mNext.centerCrop().preload(width, height);
    }

    /*********************************************************************************************
     *  Interfaces
     *********************************************************************************************/

    public interface IRequest {
        ICreateNextLoad request(String... requests);
    }

    public interface ICreateNextLoad {
        ILoadInto createNextLoad();
    }

    public interface ILoadInto {
        GlidePreload loadInto(ImageView imageView);
    }

    /*********************************************************************************************
     *  Builder Pattern
     *********************************************************************************************/

    private static class PreloadBuilder implements IRequest, ICreateNextLoad, ILoadInto {

        // Our instance that we call interfaces from
        private GlidePreload mInstance = new GlidePreload();

        /*****************************************************************************************
         *  Creational Methods
         *****************************************************************************************/

        public PreloadBuilder(Context context) {
            this.mInstance.mContext = context;
        }

        @Override
        public ICreateNextLoad request(String... requests) {
            this.mInstance.mUrls = Arrays.asList(requests);
            return this;
        }

        @Override
        public ILoadInto createNextLoad() {
            this.mInstance.createNextLoad();
            return this;
        }

        public GlidePreload loadInto(ImageView imageView) {
            this.mInstance.loadInto(imageView);
            return mInstance;
        }

    }
}

But your way is a lot cleaner.

Might still be a good idea to make an enhancement for this?

Also @TWiStErRob the code you listed, would that be used in the load() or createNextLoad() method?

There are still a few issues. It doesn't seem to preload the next image properly. I stepped through it with a debugger and it still shows white.

It's in the load method. What do you mean by enhancement?

Did you match the size and transformation for preload?
Enable Engine logging and you'll see how the images are loaded, check if the keys match for the images.

This implementation will cause a NPE for me:

void load() {
    DrawableRequestBuilder<String> temp = prev;
    prev = curr;
    curr = next;
    next = temp;
    prev.thumbnail(null).listener(null);
    next.load(it.next());
    curr.thumbnail(prev).listener(this).into(imageView);
}

I call my wrapper class in a ViewHolder, and I cycle between two objects like so:

String currentUrl = currentObject.getImageUrl();
String nextUrl = nextObject.getImageUrl();

// Set into preload
GlidePreload.with(context)
              .request(currentUrl, nextUrl)
              .loadInto(imageView);

On initial startup your curr will throw an exception. Also in your new implementation where is createNextLoad() called or is next.load(it.next()) solving that?

.loadInto(imageView) == load() for me and this is my current implementation of it:

    private void loadInto(ImageView imageView) {
        // Take a step foward
        this.mPrevious = mCurrent;
        this.mCurrent = mNext;
        this.mNext = createNextLoad();

        // Load the current request, fading from the previous request
        if (mPrevious != null)
            mPrevious.thumbnail(null)
                    .listener(null);

        mCurrent.thumbnail(mPrevious)
                .listener(this)
                .centerCrop()
                .into(imageView);
    }

Where createNextLoad() does the following:

    private DrawableRequestBuilder<String> createNextLoad() {
        if (mUrl == null || !mUrl.hasNext())
            mUrl = mUrls.iterator();

        mCurrent = Glide.with(mContext)
                .load(mUrl.next())
                .centerCrop()
                .dontAnimate();

        return Glide.with(mContext)
                .load(mUrl.next())
                .centerCrop()
                .dontAnimate();
    }

Everything else is the same.

I have to initialize mCurrent in order to not get a NPE.

This implementation will cause a NPE for me:

Of course, I wrote it on my phone and didn't test. What is missing is that you need to initialize all 3 variables in onCreate, it's a different approach:

    prev = Glide.with(this).fromString().dontAnimate();
    curr = Glide.with(this).fromString().dontAnimate();
    next = Glide.with(this).fromString().dontAnimate();

I'll try to refactor my Fragment into a reusable piece of code, something like your builder.

Here is my refactor. It also accounts for white-ness, see comment in startNext.
Notice that Glide.with is only called once! (and copied 3 times, after that it's reused).

Full version: https://github.com/TWiStErRob/glide-support/tree/master/src/glide3/java/com/bumptech/glide/supportapp/github/_861_preload_loop

public class LoadCycler<M, T extends Drawable> {
    private Iterable<M> models = Collections.emptyList();
    private Iterator<M> data;
    private GenericRequestBuilder<M, ?, ?, T> prev, curr, next;
    private final Target<? super T> target;
    private GlideAnimationFactory<Drawable> factory = new DrawableCrossFadeFactory<>();
    private boolean isLoading = false;

    public LoadCycler(GenericRequestBuilder<M, ?, ?, T> request, ImageView imageView) {
        this(request, new DrawableImageViewTarget(imageView));
    }
    public LoadCycler(GenericRequestBuilder<M, ?, ?, T> request, Target<? super T> target) {
        this.target = target;
        prev = copy(request);
        curr = copy(request);
        next = copy(request);
    }
    private GenericRequestBuilder<M, ?, ?, T> copy(GenericRequestBuilder<M, ?, ?, T> request) {
        return request.clone().dontAnimate().thumbnail(null).listener(null).load(null);
    }

    protected M getNextModel() {
        if (data == null || !data.hasNext()) {
            data = models.iterator();
        }
        return data.hasNext()? data.next() : null;
    }

    private void rotate() {
        GenericRequestBuilder<M, ?, ?, T> temp = prev;
        prev = curr;
        curr = next;
        next = temp;
    }

    public boolean startNext() {
        if (isLoading) {
            return false; // prevent showing white when next is called too fast
            // the current load must have finished before going to the next one
            // this ensures that the current load will always be in memory cache so it can be used as thumbnail
        }
        rotate();
        prev.thumbnail(null).listener(null);
        next.load(getNextModel());
        @SuppressWarnings("unchecked") Target<T> target = (Target)this.target;
        isLoading = true;
        curr.thumbnail(prev).listener(animator).into(target);
        return true;
    }

    public void setData(Iterable<M> models) {
        this.models = models != null? models : Collections.<M>emptyList();
        prev.load(null);
        curr.load(null);
        next.load(getNextModel());
    }

    public void setAnimation(GlideAnimationFactory<Drawable> factory) {
        this.factory = factory;
    }

    private final SizeReadyCallback preloader = new SizeReadyCallback() {
        @Override public void onSizeReady(int width, int height) {
            next.preload(width, height);
        }
    };

    private final RequestListener<M, T> animator = new RequestListener<M, T>() {
        @Override public boolean onException(Exception e, M model, Target<T> target, boolean isFirstResource) {
            return false;
        }
        @Override public boolean onResourceReady(T resource, M model, Target<T> target, boolean ignore, boolean thumb) {
            isLoading = false;
            target.getSize(preloader); // calls onSizeReady
            return factory != null && factory.build(false, thumb).animate(resource, (GlideAnimation.ViewAdapter)target);
        }
    };
}

usage:

public class TestFragment extends Fragment {
    private ImageView imageView;
    private LoadCycler<String, ?> cycler;

    @Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { ... }

    @Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        imageView = (ImageView)view.findViewById(R.id.image);
        setupCycler();
    }

    @Override public void onStart() {
        super.onStart();
        loadDataAsync();
    }

    private void setupCycler() {
        cycler = new LoadCycler<>(Glide.with(this).fromString().centerCrop(), imageView);
        //cycler.setAnimation(new DrawableCrossFadeFactory<>(1000));
    }

    private void loadDataAsync() {
        new AsyncTask<Void, Void, List<String>>() {
            @Override protected List<String> doInBackground(Void... params) {
                return Arrays.asList(
                        "http://placehold.it/200x200.gif?text=0",
                        "http://placehold.it/200x200.gif?text=1",
                        "http://placehold.it/200x200.gif?text=2",
                        "http://placehold.it/200x200.gif?text=3"
                );
            }
            @Override protected void onPostExecute(List<String> data) {
                cycler.setData(data);
                next();
            }
        }.execute();
    }

    private void next() {
        if (!isResumed()) return;
        cycler.startNext();
        //autoAdvance();
    }

    private void autoAdvance() {
        new Handler(Looper.getMainLooper()).postDelayed(new Runnable() {
            @Override public void run() { next(); }
        }, 2000);
    }
}

Obviously async loading is not required, it was just more real-life scenario; you can just do:

cycler = new Cycler...
cycler.setData(...);
cycler.startNext();

usage with custom model (as you say you have objects):

public class Item {
    private final String imageUrl;
    public Item(String imageUrl) { this.imageUrl = imageUrl; }
    public String getImageUrl() { return imageUrl; }
}
// in GlideModule: glide.register(Item.class, InputStream.class, new ItemLoader.Factory());
public class ItemLoader extends BaseGlideUrlLoader<Item> {
    public ItemLoader(Context context) { super(context); }
    @Override protected String getUrl(Item model, int width, int height) { return model.getImageUrl(); }

    public static class Factory implements ModelLoaderFactory<Item, InputStream> {
        @Override public ModelLoader<Item, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new ItemLoader(context);
        }
        @Override public void teardown() { }
    }
}

// inside fragment change:

private LoadCycler<Item, ?> cycler;

.from(Item.class) // instead of .fromString()

new AsyncTask<Void, Void, List<Item>>() { // note List's generic argument
return Arrays.asList( new Item("...") ... // pass objects, not Strings

That's an awesome example maybe you should add it to the wiki!

Quick question, this snippet and comment here:

target.getSize(new SizeReadyCallback() { // make sure that the preloaded size matches the ImageView
    @Override public void onSizeReady(int width, int height) {
        Glide
            .with(context)
            .load(nextImage)
            .fitCenter() // you have to be explicit about the transformation and match what's in the XML
            .preload(width, height);
    }
});

You mention you have to be explicit about the transformation, does that apply to your last example as well?

Yes, you have be to explicit, but only once (no need to modify LoadCycler). If you want .centerCrop() or .fitCenter() you have to add it like I did in setupCycler. The magic of GenericRequestBuilder.into(ImageView) is not applied, since the target is explicitly created as well and handed to .into(Target); check sources of GRB to see what I mean.

Got it I see the explicit call is when initializing the cycler inside the setupCycler() method.

@Teovald's solution in case someone needs an alternative: https://gist.github.com/Teovald/f8db7faa9ae6328be327

When i use this code (copy paste :) ) the first image shows 2 time . first without any effect and the second time with centercropeffect . i use circular image view instead of image view .
the images are in assets folder but i will upload them later .
i tried alternative solution https://gist.github.com/Teovald/f8db7faa9ae6328be327 but the same thing happened .


    private void setupCycler() {
        cycler = new LoadCycler<>(Glide.with(this).fromString().thumbnail(0.5f).override(600, 600).diskCacheStrategy(DiskCacheStrategy.ALL).fitCenter(), imageView);
//        cycler.setAnimation(new DrawableCrossFadeFactory<>(2000));
        setRepeatingAsyncTask();
    }

    private void loadDataAsync() {
        new AsyncTask<Void, Void, List<String>>() {
            @Override
            protected List<String> doInBackground(Void... params) {
                return Arrays.asList("file:///android_asset/slidpics/1.jpg", "file:///android_asset/slidpics/2.jpg", "file:///android_asset/slidpics/3.jpg", "file:///android_asset/slidpics/4.jpg");
            }

            @Override
            protected void onPostExecute(List<String> data) {
                cycler.setData(data);
                next();
            }
        }.execute();
    }

    public void next() {
        if (!isResumed) return;
        cycler.startNext();
        imageView.startAnimation(myFadeInAnimation);
//        autoAdvance();
    }

    private void setRepeatingAsyncTask() {

        final Handler handler = new Handler();
        Timer timer = new Timer();

        TimerTask task = new TimerTask() {
            @Override
            public void run() {
                handler.post(new Runnable() {
                    public void run() {
                        try {
                            next();

                        } catch (Exception e) {
                            // error, do something
                        }
                    }
                });
            }
        };

        timer.schedule(task, 6000, 6 * 1000);

    }

i tried using imageview the fitcenter take effect just before next image animation and no circle crop

 cycler = new LoadCycler<>(Glide.with(this)
                .fromString()
                .thumbnail(0.5f)
                .override(600, 600)
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .bitmapTransform( new CropCircleTransformation(getApplicationContext()))
                .fitCenter(), imageView);

have no effect

@eikagroup

  • make sure you use the bottom-most version (or full version: https://github.com/TWiStErRob/glide-support/tree/master/src/glide3/java/com/bumptech/glide/supportapp/github/_861_preload_loop)
  • try without the circular image view, it doesn't support crossfade
  • double-check that all your images in drawables are distinct

tnx
removed .fitCenter() and wow .(goodby viewpager) its the best solution for image slider in android , that i found till now .
it seems .fitCenter() is the problem . it disable transforms and fixy work only before coming of next image .

  cycler = new LoadCycler<>(Glide.with(this)
                .fromString()
                .thumbnail(0.5f)
                .override(600, 600)
                .diskCacheStrategy(DiskCacheStrategy.ALL)
              //  .bitmapTransform( new CropCircleTransformation(this))
                .fitCenter()
                , imageView);

about circleimageview
the default image wasn't circle and using it was easier :) . for now i use another glide to show default image for times that there is no Internet connection

If you don't use a transformation that changes the size you might end up with unnecessarily large images in memory. To combine sizing with other transformations look at https://github.com/wasabeef/glide-transformations/pull/40#issuecomment-218405816

Also thumbnail has no effect, because the cycler uses that to animate between images, it's also not necessary because the next image is preloaded.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

piedpiperlol picture piedpiperlol  路  3Comments

sant527 picture sant527  路  3Comments

kooeasy picture kooeasy  路  3Comments

r4m1n picture r4m1n  路  3Comments

technoir42 picture technoir42  路  3Comments