Created
November 24, 2015 10:00
-
-
Save ralphilius/b93eb2155de84079b712 to your computer and use it in GitHub Desktop.
Glide to load big image using BitmapRegionDecoder
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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