From a6740390d172ade92a707da4a7c0cd6f32f9b28e Mon Sep 17 00:00:00 2001 From: Sam Judd Date: Sat, 30 Nov 2013 13:12:26 -0800 Subject: [PATCH] initial working version of preloading --- .../src/com/bumptech/glide/ListPreloader.java | 128 ++++++++++++++ .../loader/image/ImageManagerLoader.java | 12 +- .../bumptech/glide/resize/ImageManager.java | 164 +++++++++++------- .../glide/samples/flickr/FlickrPhotoGrid.java | 40 +++-- .../samples/flickr/FlickrSearchActivity.java | 2 + .../glide/samples/flickr/api/Api.java | 2 +- 6 files changed, 271 insertions(+), 77 deletions(-) create mode 100644 library/src/com/bumptech/glide/ListPreloader.java diff --git a/library/src/com/bumptech/glide/ListPreloader.java b/library/src/com/bumptech/glide/ListPreloader.java new file mode 100644 index 000000000..a2db7e1b5 --- /dev/null +++ b/library/src/com/bumptech/glide/ListPreloader.java @@ -0,0 +1,128 @@ +package com.bumptech.glide; + +import android.annotation.TargetApi; +import android.content.Context; +import android.graphics.Bitmap; +import android.os.Build; +import android.widget.AbsListView; +import com.bumptech.glide.presenter.target.SimpleTarget; + +import java.util.ArrayDeque; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; + +public abstract class ListPreloader implements AbsListView.OnScrollListener { + private final int maxPreload; + private final Context context; + private final PreloadTargetQueue preloadTargetQueue; + + private int lastEnd; + private int lastStart; + private int lastFirstVisible; + private int totalItemCount; + + public ListPreloader(Context context, int maxPreload) { + this.context = context; + this.maxPreload = maxPreload; + preloadTargetQueue = new PreloadTargetQueue(maxPreload); + } + + @Override + public void onScrollStateChanged(AbsListView absListView, int i) { } + + @Override + public void onScroll(AbsListView absListView, int firstVisible, int visibleCount, int totalCount) { + totalItemCount = totalCount; + if (firstVisible > lastFirstVisible) { + preload(firstVisible + visibleCount, true); + } else if (firstVisible < lastFirstVisible) { + preload(firstVisible, false); + } + lastFirstVisible = firstVisible; + } + + protected abstract int[] getDimens(T item); + + protected abstract List getItems(int start, int end); + + protected abstract Glide.Request getRequest(T item); + + public void preload(int start, boolean increasing) { + preload(start, start + (increasing ? maxPreload : -maxPreload)); + } + + private void preload(int from, int to) { + int start; + int end; + if (from < to) { + start = Math.max(lastEnd, from); + end = to; + } else { + start = to; + end = Math.min(lastStart, from); + } + end = Math.min(totalItemCount, end); + start = Math.min(totalItemCount, Math.max(0, start)); + List items = getItems(start, end); + + // Increasing + if (from < to) { + final int numItems = items.size(); + for (int i = 0; i < numItems; i++) { + preload(items, i); + } + } else { + for (int i = items.size() - 1; i >= 0; i--) { + preload(items, i); + } + } + + lastStart = start; + lastEnd = end; + } + + private void preload(List items, int position) { + final T item = items.get(position); + int[] dimens = getDimens(item); + getRequest(item).into(preloadTargetQueue.next(dimens[0], dimens[1])).with(context); + } + + private static class PreloadTargetQueue { + private final Queue queue; + + @TargetApi(9) + private PreloadTargetQueue(int size) { + if (Build.VERSION.SDK_INT >= 9) { + queue = new ArrayDeque(size); + } else { + queue = new LinkedList(); + } + + for (int i = 0; i < size; i++) { + queue.offer(new PreloadTarget()); + } + } + + public PreloadTarget next(int width, int height) { + final PreloadTarget result = queue.poll(); + queue.offer(result); + result.photoWidth = width; + result.photoHeight = height; + return result; + } + } + + private static class PreloadTarget extends SimpleTarget { + private int photoHeight; + private int photoWidth; + + @Override + protected int[] getSize() { + return new int[] { photoWidth, photoHeight }; + } + + @Override + public void onImageReady(Bitmap bitmap) { } + } +} diff --git a/library/src/com/bumptech/glide/loader/image/ImageManagerLoader.java b/library/src/com/bumptech/glide/loader/image/ImageManagerLoader.java index 1586f98a5..9d80bf0a5 100644 --- a/library/src/com/bumptech/glide/loader/image/ImageManagerLoader.java +++ b/library/src/com/bumptech/glide/loader/image/ImageManagerLoader.java @@ -16,11 +16,10 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; * purposes. */ public class ImageManagerLoader implements ImageLoader { - - protected final ImageManager imageManager; + private final ImageManager imageManager; private final Downsampler downsampler; private Bitmap acquired; - private ImageManager.ImageManagerJob loadToken; + private ImageManager.LoadToken loadToken; public ImageManagerLoader(Context context) { this(context, Downsampler.AT_LEAST); @@ -40,13 +39,14 @@ public class ImageManagerLoader implements ImageLoader { } @Override - public Object fetchImage(String id, StreamLoader streamLoader, Transformation transformation, int width, int height, final ImageReadyCallback cb) { + public Object fetchImage(String id, StreamLoader streamLoader, Transformation transformation, int width, int height, + final ImageReadyCallback cb) { if (!isHandled(width, height)) { throw new IllegalArgumentException(getClass() + " cannot handle width=" + width + " and/or height =" + height); } - loadToken = imageManager.getImage(id, streamLoader, transformation, downsampler, width, height, new LoadedCallback() { - + loadToken = imageManager.getImage(id, streamLoader, transformation, downsampler, width, height, + new LoadedCallback() { @Override public void onLoadCompleted(Bitmap loaded) { onImageReady(loaded, cb.onImageReady(loaded)); diff --git a/library/src/com/bumptech/glide/resize/ImageManager.java b/library/src/com/bumptech/glide/resize/ImageManager.java index 077998ab1..82fbec54f 100644 --- a/library/src/com/bumptech/glide/resize/ImageManager.java +++ b/library/src/com/bumptech/glide/resize/ImageManager.java @@ -9,10 +9,7 @@ import android.app.ActivityManager; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory; -import android.os.Build; -import android.os.Environment; -import android.os.Handler; -import android.os.HandlerThread; +import android.os.*; import com.bumptech.glide.loader.stream.StreamLoader; import com.bumptech.glide.resize.bitmap_recycle.BitmapPool; import com.bumptech.glide.resize.bitmap_recycle.BitmapPoolAdapter; @@ -35,16 +32,23 @@ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ThreadFactory; +import static android.os.Process.THREAD_PRIORITY_BACKGROUND; + /** * A class to coordinate image loading, resizing, recycling, and caching. Depending on the provided options and the * sdk version, uses a combination of an LRU disk cache and an LRU hard memory cache to try to reduce the number of * load and resize operations performed and to maximize the number of times Bitmaps are recycled as opposed to - * allocated. If no options are given defaults to using both a memory and a disk cache and to recycling bitmaps if possible. + * allocated. If no options are given defaults to using both a memory and a disk cache and to recycling bitmaps if + * possible. * *

* Note that Bitmap recycling is only available on Honeycomb and up. @@ -60,6 +64,7 @@ public class ImageManager { private final BitmapReferenceCounter bitmapReferenceCounter; private final int bitmapCompressQuality; private final BitmapPool bitmapPool; + private final Map jobs = new HashMap(); private boolean shutdown = false; private final Handler mainHandler = new Handler(); @@ -75,7 +80,8 @@ public class ImageManager { private static Downsampler DISK_CACHE_DOWNSAMPLER = new Downsampler() { @Override - public Bitmap downsample(RecyclableBufferedInputStream bis, BitmapFactory.Options options, BitmapPool pool, int outWidth, int outHeight) { + public Bitmap downsample(RecyclableBufferedInputStream bis, BitmapFactory.Options options, BitmapPool pool, + int outWidth, int outHeight) { return downsampleWithSize(bis, options, pool, outWidth, outHeight, 1); } @@ -313,11 +319,12 @@ public class ImageManager { private void setDefaults() { if (resizeService == null) { - resizeService = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors()), new ThreadFactory() { + final int numThreads = Math.max(1, Runtime.getRuntime().availableProcessors()); + resizeService = Executors.newFixedThreadPool(numThreads, new ThreadFactory() { @Override public Thread newThread(Runnable runnable) { final Thread result = new Thread(runnable); - result.setPriority(Thread.MIN_PRIORITY); + result.setPriority(THREAD_PRIORITY_BACKGROUND); return result; } }); @@ -356,7 +363,7 @@ public class ImageManager { } private ImageManager(Builder builder) { - HandlerThread bgThread = new HandlerThread("bg_thread", android.os.Process.THREAD_PRIORITY_BACKGROUND); + HandlerThread bgThread = new HandlerThread("bg_thread", THREAD_PRIORITY_BACKGROUND); bgThread.start(); bgHandler = new Handler(bgThread.getLooper()); executor = builder.resizeService; @@ -366,7 +373,6 @@ public class ImageManager { bitmapReferenceCounter = builder.bitmapReferenceCounter; bitmapPool = builder.bitmapPool; resizer = new ImageResizer(builder.bitmapPool, builder.decodeBitmapOptions); - memoryCache.setImageRemovedListener(new MemoryCache.ImageRemovedListener() { @Override public void onImageRemoved(Bitmap removed) { @@ -404,18 +410,25 @@ public class ImageManager { * @return An {@link ImageManagerJob} that must be retained while the job is still relevant and that can be used * to cancel a job if the image is no longer needed */ - public ImageManagerJob getImage(String id, StreamLoader streamLoader, Transformation transformation, Downsampler downsampler, int width, int height, LoadedCallback cb) { + public LoadToken getImage(String id, StreamLoader streamLoader, Transformation transformation, + Downsampler downsampler, int width, int height, LoadedCallback cb) { if (shutdown) return null; final String key = safeKeyGenerator.getSafeKey(id, transformation, downsampler, width, height); - - ImageManagerJob job = null; + LoadToken result = null; if (!returnFromCache(key, cb)) { - ImageManagerRunner runner = new ImageManagerRunner(key, streamLoader, transformation, downsampler, width, height, cb); - runner.execute(); - job = new ImageManagerJob(runner, streamLoader, transformation, downsampler, cb); + ImageManagerJob job = jobs.get(key); + if (job == null) { + ImageManagerRunner runner = new ImageManagerRunner(key, streamLoader, transformation, downsampler, + width, height); + job = new ImageManagerJob(runner, key); + jobs.put(key, job); + runner.execute(); + } + job.addCallback(cb); + result = new LoadToken(cb, job); } - return job; + return result; } /** @@ -464,30 +477,69 @@ public class ImageManager { * A class for tracking a particular job in the {@link ImageManager}. Cancel does not guarantee that the * job will not finish, but rather is a best effort attempt. */ - public static class ImageManagerJob { + private class ImageManagerJob { private final ImageManagerRunner runner; - private StreamLoader sl; - private Transformation transformation; - private Downsampler downsampler; - private LoadedCallback cb; + private final String key; + private final List cbs = new ArrayList(); - public ImageManagerJob(ImageManagerRunner runner, StreamLoader sl, Transformation t, Downsampler d, LoadedCallback cb) { + public ImageManagerJob(ImageManagerRunner runner, String key) { this.runner = runner; - this.sl = sl; - this.transformation = t; - this.downsampler = d; - this.cb = cb; + this.key = key; + } + + public void addCallback(LoadedCallback cb) { + cbs.add(cb); } /** * Try to cancel the job. Does not guarantee that the job will not finish. */ - public void cancel() { - runner.cancel(); - sl = null; - transformation = null; - downsampler = null; - cb = null; + public void cancel(LoadedCallback cb) { + cbs.remove(cb); + if (cbs.size() == 0) { + runner.cancel(); + jobs.remove(key); + } + } + + public void onLoadComplete(Bitmap result) { + for (LoadedCallback cb : cbs) { + bitmapReferenceCounter.acquireBitmap(result); + cb.onLoadCompleted(result); + } + jobs.remove(key); + } + + public void onLoadFailed(Exception e) { + for (LoadedCallback cb : cbs) { + cb.onLoadFailed(e); + } + jobs.remove(key); + } + } + + private void putInDiskCache(String key, final Bitmap bitmap) { + diskCache.put(key, new DiskCache.Writer() { + @Override + public void write(OutputStream os) { + final Bitmap.Config config = bitmap.getConfig(); + final Bitmap.CompressFormat format; + if (config == null || config == Bitmap.Config.ARGB_4444 || config == Bitmap.Config.ARGB_8888) { + format = Bitmap.CompressFormat.PNG; + } else { + format = Bitmap.CompressFormat.JPEG; + } + bitmap.compress(format, bitmapCompressQuality, os); + } + }); + } + + private void putInMemoryCache(String key, final Bitmap bitmap) { + final boolean inCache; + inCache = memoryCache.contains(key); + if (!inCache) { + bitmapReferenceCounter.acquireBitmap(bitmap); + memoryCache.put(key, bitmap); } } @@ -498,12 +550,11 @@ public class ImageManager { private final StreamLoader streamLoader; private final Transformation transformation; private final Downsampler downsampler; - private final LoadedCallback cb; private volatile Future future; private volatile boolean cancelled = false; - public ImageManagerRunner(String key, StreamLoader sl, Transformation t, Downsampler d, int width, int height, LoadedCallback cb) { + public ImageManagerRunner(String key, StreamLoader sl, Transformation t, Downsampler d, int width, int height) { this.key = key; this.height = height; this.width = width; @@ -511,7 +562,6 @@ public class ImageManager { this.streamLoader = sl; this.transformation = t; this.downsampler = d; - this.cb = cb; } private void execute() { @@ -618,9 +668,11 @@ public class ImageManager { //acquire for the callback before putting in to memory cache so that the bitmap is not //released to the pool if the bitmap is synchronously released by the memory cache //we rely on the callback to call releaseBitmap if it doesn't want to use the bitmap - bitmapReferenceCounter.acquireBitmap(result); putInMemoryCache(key, result); - cb.onLoadCompleted(result); + final ImageManagerJob job = jobs.get(key); + if (job != null) { + job.onLoadComplete(result); + } } }); } else { @@ -632,34 +684,26 @@ public class ImageManager { mainHandler.post(new Runnable() { @Override public void run() { - cb.onLoadFailed(e); + final ImageManagerJob job = jobs.get(key); + if (job != null) { + job.onLoadFailed(e); + } } }); } } - private void putInDiskCache(String key, final Bitmap bitmap) { - diskCache.put(key, new DiskCache.Writer() { - @Override - public void write(OutputStream os) { - final Bitmap.Config config = bitmap.getConfig(); - final Bitmap.CompressFormat format; - if (config == null || config == Bitmap.Config.ARGB_4444 || config == Bitmap.Config.ARGB_8888) { - format = Bitmap.CompressFormat.PNG; - } else { - format = Bitmap.CompressFormat.JPEG; - } - bitmap.compress(format, bitmapCompressQuality, os); - } - }); - } + public static class LoadToken { + private final ImageManagerJob job; + private final LoadedCallback cb; - private void putInMemoryCache(String key, final Bitmap bitmap) { - final boolean inCache; - inCache = memoryCache.contains(key); - if (!inCache) { - bitmapReferenceCounter.acquireBitmap(bitmap); - memoryCache.put(key, bitmap); + public LoadToken(LoadedCallback cb, ImageManagerJob job) { + this.cb = cb; + this.job = job; + } + + public void cancel() { + job.cancel(cb); } } } diff --git a/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrPhotoGrid.java b/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrPhotoGrid.java index 67c0db818..8d22f6e7a 100644 --- a/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrPhotoGrid.java +++ b/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrPhotoGrid.java @@ -11,6 +11,8 @@ import android.widget.BaseAdapter; import android.widget.GridView; import android.widget.ImageView; import com.actionbarsherlock.app.SherlockFragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.ListPreloader; import com.bumptech.glide.loader.image.ImageManagerLoader; import com.bumptech.glide.loader.model.Cache; import com.bumptech.glide.loader.transformation.CenterCrop; @@ -22,15 +24,9 @@ import java.net.URL; import java.util.ArrayList; import java.util.List; -/** - * Created with IntelliJ IDEA. - * User: sam - * Date: 1/10/13 - * Time: 9:48 AM - * To change this template use File | Settings | File Templates. - */ public class FlickrPhotoGrid extends SherlockFragment implements PhotoViewer { private static final String IMAGE_SIZE_KEY = "image_size"; + private static final int PRELOAD_COUNT = 10; private PhotoAdapter adapter; private List currentPhotos; @@ -51,8 +47,10 @@ public class FlickrPhotoGrid extends SherlockFragment implements PhotoViewer { photoSize = args.getInt(IMAGE_SIZE_KEY); final View result = inflater.inflate(R.layout.flickr_photo_grid, container, false); - GridView grid = (GridView) result.findViewById(R.id.images); + final GridView grid = (GridView) result.findViewById(R.id.images); grid.setColumnWidth(photoSize); + final FlickrPreloader preloader = new FlickrPreloader(getActivity(), PRELOAD_COUNT); + grid.setOnScrollListener(preloader); adapter = new PhotoAdapter(); grid.setAdapter(adapter); if (currentPhotos != null) @@ -68,8 +66,30 @@ public class FlickrPhotoGrid extends SherlockFragment implements PhotoViewer { adapter.setPhotos(currentPhotos); } - private class PhotoAdapter extends BaseAdapter { + private class FlickrPreloader extends ListPreloader { + public FlickrPreloader(Context context, int toPreload) { + super(context, toPreload); + } + + @Override + protected int[] getDimens(Photo item) { + return new int[] { photoSize, photoSize }; + } + + @Override + protected List getItems(int start, int end) { + return currentPhotos.subList(start, end); + } + @Override + protected Glide.Request getRequest(Photo item) { + return Glide.using(new FlickrModelLoader(getActivity(), urlCache)) + .load(item) + .centerCrop(); + } + } + + private class PhotoAdapter extends BaseAdapter { private List photos = new ArrayList(0); private final LayoutInflater inflater; @@ -135,6 +155,6 @@ public class FlickrPhotoGrid extends SherlockFragment implements PhotoViewer { imagePresenter.setModel(current); return view; } - } + } } diff --git a/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrSearchActivity.java b/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrSearchActivity.java index 522d2440e..cc935bf06 100644 --- a/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrSearchActivity.java +++ b/samples/flickr/src/com/bumptech/glide/samples/flickr/FlickrSearchActivity.java @@ -21,6 +21,7 @@ import com.bumptech.glide.resize.ImageManager; import com.bumptech.glide.resize.cache.DiskCache; import com.bumptech.glide.resize.cache.DiskCacheAdapter; import com.bumptech.glide.resize.cache.DiskLruCacheWrapper; +import com.bumptech.glide.resize.cache.LruMemoryCache; import com.bumptech.glide.samples.flickr.api.Api; import com.bumptech.glide.samples.flickr.api.Photo; import com.bumptech.glide.util.Log; @@ -94,6 +95,7 @@ public class FlickrSearchActivity extends SherlockFragmentActivity { glide.setImageManager(new ImageManager.Builder(this) .setBitmapCompressQuality(70) + .setMemoryCache(new LruMemoryCache(ImageManager.getSafeMemoryCacheSize(this)/4)) .setDiskCache(diskCache)); } diff --git a/samples/flickr/src/com/bumptech/glide/samples/flickr/api/Api.java b/samples/flickr/src/com/bumptech/glide/samples/flickr/api/Api.java index e90d356e4..19de5e3bd 100644 --- a/samples/flickr/src/com/bumptech/glide/samples/flickr/api/Api.java +++ b/samples/flickr/src/com/bumptech/glide/samples/flickr/api/Api.java @@ -90,7 +90,7 @@ public class Api { } private static String getSearchUrl(String text) { - return getUrlForMethod("flickr.photos.search") + "&text=" + text + "&per_page=500"; + return getUrlForMethod("flickr.photos.search") + "&text=" + text + "&per_page=300"; } private static String getPhotoUrl(Photo photo, String sizeKey) { -- GitLab