Skip to content

Instantly share code, notes, and snippets.

@ralphilius
Created November 24, 2015 10:00
Show Gist options
  • Save ralphilius/b93eb2155de84079b712 to your computer and use it in GitHub Desktop.
Save ralphilius/b93eb2155de84079b712 to your computer and use it in GitHub Desktop.
Glide to load big image using BitmapRegionDecoder
public class TestFragment extends GlideRecyclerFragment {
protected RecyclerView listView;
@Override public @Nullable View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
RecyclerView view = new RecyclerView(container.getContext());
view.setId(android.R.id.list);
view.setLayoutParams(new MarginLayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
view.setLayoutManager(new LinearLayoutManager(container.getContext()));
return view;
}
@Override public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
listView = (RecyclerView)view.findViewById(android.R.id.list);
new AsyncTask<Void, Void, Point>() {
String url = "http://imgfave-herokuapp-com.global.ssl.fastly.net/image_cache/142083463797243_tall.jpg";
//String url = "https://upload.wikimedia.org/wikipedia/commons/a/a3/Berliner_Fernsehturm,_Sicht_vom_Neptunbrunnen_-_Berlin_Mitte.jpg";
@Override protected Point doInBackground(Void[] params) {
try {
File image = Glide
.with(TestFragment.this)
.load(url)
.downloadOnly(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
.get();
Options opts = new Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(image.getAbsolutePath(), opts);
return new Point(opts.outWidth, opts.outHeight);
} catch (InterruptedException | ExecutionException ignored) {
return null;
}
}
@Override protected void onPostExecute(Point imageSize) {
if (imageSize != null) {
listView.setAdapter(new ImageChunkAdapter(getScreenSize(), url, imageSize));
}
}
}.execute();
}
private Point getScreenSize() {
WindowManager window = (WindowManager)getActivity().getSystemService(Context.WINDOW_SERVICE);
Display display = window.getDefaultDisplay();
Point screen = new Point();
if (VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR2) {
display.getSize(screen);
} else {
screen.set(display.getWidth(), display.getHeight());
}
return screen;
}
}
public class ImageChunkAdapter extends RecyclerView.Adapter<ImageChunkAdapter.ImageChunkViewHolder> {
private final String url;
private final Point image;
private final int imageChunkHeight;
private final float ratio;
public ImageChunkAdapter(Point screen, String url, Point image) {
this.url = url;
this.image = image;
// calculate a chunk's height
this.ratio = screen.x / (float)image.x; // image will be fit to width
// this will result in having the chunkHeight between 1/3 and 2/3 of screen height, making sure it fits in memory
int minScreenChunkHeight = screen.y / 3;
int screenChunkHeight = leastMultiple(screen.x / gcd(screen.x, image.x), minScreenChunkHeight);
// GCD helps to keep this a whole number
// worst case GCD is 1 so screenChunkHeight == screen.x -> imageChunkHeight == image.x
this.imageChunkHeight = Math.round(screenChunkHeight / ratio);
// screen: Point(720, 1280), image: Point(500, 4784), ratio: 1.44, screenChunk: 396 (396.000031), imageChunk: 275 (275)
// screen: Point(1280, 720), image: Point(7388, 16711), ratio: 0.173254, screenChunk: 320 (320.000000), imageChunk: 1847 (1847.000000)
Log.wtf("GLIDE", String.format(Locale.ROOT,
"screen: %s, image: %s, ratio: %f, screenChunk: %d (%f), imageChunk: %d (%f)",
screen, image, ratio,
screenChunkHeight, imageChunkHeight * ratio,
imageChunkHeight, screenChunkHeight / ratio));
}
/** Greatest Common Divisor */
private static int gcd(int a, int b) {
while (b != 0) {
int t = b;
b = a % b;
a = t;
}
return a;
}
/**
* @param base positive whole number
* @param threshold positive whole number
* @return multiple of base that is >= threshold
*/
private static int leastMultiple(int base, int threshold) {
int minMul = Math.max(1, threshold / base);
return base * minMul;
}
@Override public int getItemCount() {
// round up for last partial row
return image.y / imageChunkHeight + (image.y % imageChunkHeight == 0? 0 : 1);
}
@Override public long getItemId(int position) {
return imageChunkHeight * position;
}
@Override public ImageChunkViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
ImageView view = new ImageView(parent.getContext());
view.setScaleType(ScaleType.CENTER);
view.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, (int)(imageChunkHeight * ratio)));
return new ImageChunkViewHolder(view);
}
@Override public void onBindViewHolder(ImageChunkViewHolder holder, int position) {
int left = 0, top = imageChunkHeight * position;
int width = image.x, height = imageChunkHeight;
if (position == getItemCount() - 1 && image.y % imageChunkHeight != 0) {
height = image.y % imageChunkHeight; // height of last partial row, if any
}
Rect rect = new Rect(left, top, left + width, top + height);
float viewWidth = width * ratio;
float viewHeight = height * ratio;
final String bind = String.format(Locale.ROOT, "Binding %s w=%d (%d->%f) h=%d (%d->%f)",
rect.toShortString(),
rect.width(), width, viewWidth,
rect.height(), height, viewHeight);
Context context = holder.itemView.getContext();
// See https://docs.google.com/drawings/d/1KyOJkNd5Dlm8_awZpftzW7KtqgNR6GURvuF6RfB210g/edit?usp=sharing
Glide
.with(context)
.load(url)
.asBitmap()
.placeholder(new ColorDrawable(Color.BLUE))
.error(new ColorDrawable(Color.RED))
// overshoot a little so fitCenter uses width's ratio (see minPercentage)
.override(Math.round(viewWidth), (int)Math.ceil(viewHeight))
.fitCenter()
// Cannot use .imageDecoder, only decoder; see bumptech/glide#708
//.imageDecoder(new RegionStreamDecoder(context, rect))
.decoder(new RegionImageVideoDecoder(context, rect))
.cacheDecoder(new RegionFileDecoder(context, rect))
// Cannot use RESULT cache; see bumptech/glide#707
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.listener(new RequestListener<String, Bitmap>() {
@Override public boolean onException(Exception e, String model, Target<Bitmap> target,
boolean isFirstResource) {
Log.wtf("GLIDE", String.format(Locale.ROOT, "%s %s into %s failed: %s",
bind, model, target, e), e);
return false;
}
@Override public boolean onResourceReady(Bitmap resource, String model, Target<Bitmap> target,
boolean isFromMemoryCache, boolean isFirstResource) {
View v = ((ViewTarget)target).getView();
LayoutParams p = v.getLayoutParams();
String targetString = String.format("%s(%dx%d->%dx%d)",
target, p.width, p.height, v.getWidth(), v.getHeight());
Log.wtf("GLIDE", String.format(Locale.ROOT, "%s %s into %s result %dx%d",
bind, model, targetString, resource.getWidth(), resource.getHeight()));
return false;
}
})
.into(new BitmapImageViewTarget(holder.imageView) {
@Override protected void setResource(Bitmap resource) {
if (resource != null) {
LayoutParams params = view.getLayoutParams();
if (params.height != resource.getHeight()) {
params.height = resource.getHeight();
}
view.setLayoutParams(params);
}
super.setResource(resource);
}
})
;
}
static class ImageChunkViewHolder extends RecyclerView.ViewHolder {
ImageView imageView;
public ImageChunkViewHolder(View itemView) {
super(itemView);
imageView = (ImageView)itemView;
}
}
}
abstract class RegionResourceDecoder<T> implements ResourceDecoder<T, Bitmap> {
private final BitmapPool bitmapPool;
private final Rect region;
public RegionResourceDecoder(Context context, Rect region) {
this(Glide.get(context).getBitmapPool(), region);
}
public RegionResourceDecoder(BitmapPool bitmapPool, Rect region) {
this.bitmapPool = bitmapPool;
this.region = region;
}
@Override public Resource<Bitmap> decode(T source, int width, int height) throws IOException {
Options opts = new Options();
// Algorithm from Glide's Downsampler.getRoundedSampleSize
int sampleSize = (int)Math.ceil((double)region.width() / (double)width);
sampleSize = sampleSize == 0? 0 : Integer.highestOneBit(sampleSize);
sampleSize = Math.max(1, sampleSize);
opts.inSampleSize = sampleSize;
BitmapRegionDecoder decoder = createDecoder(source, width, height);
Bitmap bitmap = decoder.decodeRegion(region, opts);
// probably not worth putting it into the pool because we'd need to get from the pool too to be efficient
return BitmapResource.obtain(bitmap, bitmapPool);
}
protected abstract BitmapRegionDecoder createDecoder(T source, int width, int height) throws IOException;
@Override public String getId() {
return getClass().getName() + region; // + region is important for RESULT caching
}
}
class RegionImageVideoDecoder extends RegionResourceDecoder<ImageVideoWrapper> {
public RegionImageVideoDecoder(Context context, Rect region) {
super(context, region);
}
@Override protected BitmapRegionDecoder createDecoder(ImageVideoWrapper source, int width, int height) throws IOException {
try {
return BitmapRegionDecoder.newInstance(source.getStream(), false);
} catch (Exception ignore) {
return BitmapRegionDecoder.newInstance(source.getFileDescriptor().getFileDescriptor(), false);
}
}
}
class RegionFileDecoder extends RegionResourceDecoder<File> {
public RegionFileDecoder(Context context, Rect region) {
super(context, region);
}
@Override protected BitmapRegionDecoder createDecoder(File source, int width, int height) throws IOException {
return BitmapRegionDecoder.newInstance(source.getAbsolutePath(), false);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment