Include Subsampling Scale Image View as library to allow preloading tiles when a max bitmap size is provided.

This commit is contained in:
inorichi
2015-12-21 18:09:09 +01:00
parent 80a59548a5
commit 322f54380d
19 changed files with 3499 additions and 3 deletions

View File

@@ -0,0 +1,20 @@
package com.davemorrissey.labs.subscaleview.decoder;
import android.support.annotation.NonNull;
/**
* Compatibility factory to instantiate decoders with empty public constructors.
* @param <T> The base type of the decoder this factory will produce.
*/
public class CompatDecoderFactory <T> implements DecoderFactory<T> {
private Class<? extends T> clazz;
public CompatDecoderFactory(@NonNull Class<? extends T> clazz) {
this.clazz = clazz;
}
@Override
public T make() throws IllegalAccessException, InstantiationException {
return clazz.newInstance();
}
}

View File

@@ -0,0 +1,13 @@
package com.davemorrissey.labs.subscaleview.decoder;
/**
* Interface for decoder (and region decoder) factories.
* @param <T> the class of decoder that will be produced.
*/
public interface DecoderFactory<T> {
/**
* Produce a new instance of a decoder with type {@link T}.
* @return a new instance of your decoder.
*/
T make() throws IllegalAccessException, InstantiationException;
}

View File

@@ -0,0 +1,28 @@
package com.davemorrissey.labs.subscaleview.decoder;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
/**
* Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder}
* based on the Skia library to be replaced with a custom class.
*/
public interface ImageDecoder {
/**
* Decode an image. When possible, initial setup work once in this method. This method
* must return the dimensions of the image. The URI can be in one of the following formats:
* File: file:///scard/picture.jpg
* Asset: file:///android_asset/picture.png
* Resource: android.resource://com.example.app/drawable/picture
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param uri URI of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
Bitmap decode(Context context, Uri uri) throws Exception;
}

View File

@@ -0,0 +1,50 @@
package com.davemorrissey.labs.subscaleview.decoder;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Point;
import android.graphics.Rect;
import android.net.Uri;
/**
* Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder}
* based on the Skia library to be replaced with a custom class.
*/
public interface ImageRegionDecoder {
/**
* Initialise the decoder. When possible, initial setup work once in this method. This method
* must return the dimensions of the image. The URI can be in one of the following formats:
* File: file:///scard/picture.jpg
* Asset: file:///android_asset/picture.png
* Resource: android.resource://com.example.app/drawable/picture
* @param context Application context. A reference may be held, but must be cleared on recycle.
* @param uri URI of the image.
* @return Dimensions of the image.
* @throws Exception if initialisation fails.
*/
Point init(Context context, Uri uri) throws Exception;
/**
* Decode a region of the image with the given sample size. This method is called off the UI thread so it can safely
* load the image on the current thread. It is called from an {@link android.os.AsyncTask} running in a single
* threaded executor, and while a synchronization lock is held on this object, so will never be called concurrently
* even if the decoder implementation supports it.
* @param sRect Source image rectangle to decode.
* @param sampleSize Sample size.
* @return The decoded region. It is safe to return null if decoding fails.
*/
Bitmap decodeRegion(Rect sRect, int sampleSize);
/**
* Status check. Should return false before initialisation and after recycle.
* @return true if the decoder is ready to be used.
*/
boolean isReady();
/**
* This method will be called when the decoder is no longer required. It should clean up any resources still in use.
*/
void recycle();
}

View File

@@ -0,0 +1,79 @@
package com.davemorrissey.labs.subscaleview.decoder;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.text.TextUtils;
import java.io.InputStream;
import java.util.List;
/**
* Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageDecoder}
* using Android's {@link android.graphics.BitmapFactory}, based on the Skia library. This
* works well in most circumstances and has reasonable performance, however it has some problems
* with grayscale, indexed and CMYK images.
*/
public class SkiaImageDecoder implements ImageDecoder {
private static final String FILE_PREFIX = "file://";
private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/";
private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://";
@Override
public Bitmap decode(Context context, Uri uri) throws Exception {
String uriString = uri.toString();
BitmapFactory.Options options = new BitmapFactory.Options();
Bitmap bitmap;
options.inPreferredConfig = Bitmap.Config.RGB_565;
if (uriString.startsWith(RESOURCE_PREFIX)) {
Resources res;
String packageName = uri.getAuthority();
if (context.getPackageName().equals(packageName)) {
res = context.getResources();
} else {
PackageManager pm = context.getPackageManager();
res = pm.getResourcesForApplication(packageName);
}
int id = 0;
List<String> segments = uri.getPathSegments();
int size = segments.size();
if (size == 2 && segments.get(0).equals("drawable")) {
String resName = segments.get(1);
id = res.getIdentifier(resName, "drawable", packageName);
} else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {
try {
id = Integer.parseInt(segments.get(0));
} catch (NumberFormatException ignored) {
}
}
bitmap = BitmapFactory.decodeResource(context.getResources(), id, options);
} else if (uriString.startsWith(ASSET_PREFIX)) {
String assetName = uriString.substring(ASSET_PREFIX.length());
bitmap = BitmapFactory.decodeStream(context.getAssets().open(assetName), null, options);
} else if (uriString.startsWith(FILE_PREFIX)) {
bitmap = BitmapFactory.decodeFile(uriString.substring(FILE_PREFIX.length()), options);
} else {
InputStream inputStream = null;
try {
ContentResolver contentResolver = context.getContentResolver();
inputStream = contentResolver.openInputStream(uri);
bitmap = BitmapFactory.decodeStream(inputStream, null, options);
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (Exception e) { }
}
}
}
if (bitmap == null) {
throw new RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported");
}
return bitmap;
}
}

View File

@@ -0,0 +1,101 @@
package com.davemorrissey.labs.subscaleview.decoder;
import android.content.ContentResolver;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.graphics.*;
import android.graphics.Bitmap.Config;
import android.net.Uri;
import android.text.TextUtils;
import java.io.InputStream;
import java.util.List;
/**
* Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder}
* using Android's {@link android.graphics.BitmapRegionDecoder}, based on the Skia library. This
* works well in most circumstances and has reasonable performance due to the cached decoder instance,
* however it has some problems with grayscale, indexed and CMYK images.
*/
public class SkiaImageRegionDecoder implements ImageRegionDecoder {
private BitmapRegionDecoder decoder;
private final Object decoderLock = new Object();
private static final String FILE_PREFIX = "file://";
private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/";
private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://";
@Override
public Point init(Context context, Uri uri) throws Exception {
String uriString = uri.toString();
if (uriString.startsWith(RESOURCE_PREFIX)) {
Resources res;
String packageName = uri.getAuthority();
if (context.getPackageName().equals(packageName)) {
res = context.getResources();
} else {
PackageManager pm = context.getPackageManager();
res = pm.getResourcesForApplication(packageName);
}
int id = 0;
List<String> segments = uri.getPathSegments();
int size = segments.size();
if (size == 2 && segments.get(0).equals("drawable")) {
String resName = segments.get(1);
id = res.getIdentifier(resName, "drawable", packageName);
} else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) {
try {
id = Integer.parseInt(segments.get(0));
} catch (NumberFormatException ignored) {
}
}
decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false);
} else if (uriString.startsWith(ASSET_PREFIX)) {
String assetName = uriString.substring(ASSET_PREFIX.length());
decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false);
} else if (uriString.startsWith(FILE_PREFIX)) {
decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false);
} else {
InputStream inputStream = null;
try {
ContentResolver contentResolver = context.getContentResolver();
inputStream = contentResolver.openInputStream(uri);
decoder = BitmapRegionDecoder.newInstance(inputStream, false);
} finally {
if (inputStream != null) {
try { inputStream.close(); } catch (Exception e) { }
}
}
}
return new Point(decoder.getWidth(), decoder.getHeight());
}
@Override
public Bitmap decodeRegion(Rect sRect, int sampleSize) {
synchronized (decoderLock) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
options.inPreferredConfig = Config.RGB_565;
Bitmap bitmap = decoder.decodeRegion(sRect, options);
if (bitmap == null) {
throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported");
}
return bitmap;
}
}
@Override
public boolean isReady() {
return decoder != null && !decoder.isRecycled();
}
@Override
public void recycle() {
decoder.recycle();
}
}