Introduce MediaDecoder abstraction for image-loader module

This commit is contained in:
Dimitry Ivanov 2018-07-21 12:44:29 +03:00
parent 4a80616f75
commit 146ba9c575
6 changed files with 340 additions and 135 deletions

View File

@ -15,6 +15,9 @@ import dagger.Provides;
import okhttp3.Cache; import okhttp3.Cache;
import okhttp3.OkHttpClient; import okhttp3.OkHttpClient;
import ru.noties.markwon.il.AsyncDrawableLoader; import ru.noties.markwon.il.AsyncDrawableLoader;
import ru.noties.markwon.il.GifMediaDecoder;
import ru.noties.markwon.il.ImageMediaDecoder;
import ru.noties.markwon.il.SvgMediaDecoder;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDarkula;
import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.markwon.syntax.Prism4jThemeDefault;
@ -46,7 +49,7 @@ class AppModule {
@Singleton @Singleton
OkHttpClient client() { OkHttpClient client() {
return new OkHttpClient.Builder() return new OkHttpClient.Builder()
.cache(new Cache(app.getCacheDir(), 1024L * 20)) .cache(new Cache(app.getCacheDir(), 1024L * 1024 * 20)) // 20 mb
.followRedirects(true) .followRedirects(true)
.retryOnConnectionFailure(true) .retryOnConnectionFailure(true)
.build(); .build();
@ -79,7 +82,11 @@ class AppModule {
.client(client) .client(client)
.executorService(executorService) .executorService(executorService)
.resources(resources) .resources(resources)
.autoPlayGif(false) .mediaDecoders(
SvgMediaDecoder.create(resources),
GifMediaDecoder.create(false),
ImageMediaDecoder.create(resources)
)
.build(); .build();
} }

View File

@ -1,28 +1,22 @@
package ru.noties.markwon.il; package ru.noties.markwon.il;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
import android.os.Looper; import android.os.Looper;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.text.TextUtils; import android.support.annotation.Nullable;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -34,22 +28,21 @@ import okhttp3.OkHttpClient;
import okhttp3.Request; import okhttp3.Request;
import okhttp3.Response; import okhttp3.Response;
import okhttp3.ResponseBody; import okhttp3.ResponseBody;
import pl.droidsonroids.gif.GifDrawable;
import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.AsyncDrawable;
public class AsyncDrawableLoader implements AsyncDrawable.Loader { public class AsyncDrawableLoader implements AsyncDrawable.Loader {
@NonNull
public static AsyncDrawableLoader create() { public static AsyncDrawableLoader create() {
return builder().build(); return builder().build();
} }
@NonNull
public static AsyncDrawableLoader.Builder builder() { public static AsyncDrawableLoader.Builder builder() {
return new Builder(); return new Builder();
} }
private static final String HEADER_CONTENT_TYPE = "Content-Type"; private static final String HEADER_CONTENT_TYPE = "Content-Type";
private static final String CONTENT_TYPE_SVG = "image/svg+xml";
private static final String CONTENT_TYPE_GIF = "image/gif";
private static final String FILE_ANDROID_ASSETS = "android_asset"; private static final String FILE_ANDROID_ASSETS = "android_asset";
@ -58,9 +51,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
private final ExecutorService executorService; private final ExecutorService executorService;
private final Handler mainThread; private final Handler mainThread;
private final Drawable errorDrawable; private final Drawable errorDrawable;
private final List<MediaDecoder> mediaDecoders;
// @since 1.1.0
private final boolean autoPlayGif;
private final Map<String, Future<?>> requests; private final Map<String, Future<?>> requests;
@ -70,7 +61,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
this.executorService = builder.executorService; this.executorService = builder.executorService;
this.mainThread = new Handler(Looper.getMainLooper()); this.mainThread = new Handler(Looper.getMainLooper());
this.errorDrawable = builder.errorDrawable; this.errorDrawable = builder.errorDrawable;
this.autoPlayGif = builder.autoPlayGif; this.mediaDecoders = builder.mediaDecoders;
this.requests = new HashMap<>(3); this.requests = new HashMap<>(3);
} }
@ -109,12 +100,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
public void run() { public void run() {
final Item item; final Item item;
final boolean isFromFile;
final Uri uri = Uri.parse(destination); final Uri uri = Uri.parse(destination);
if ("file".equals(uri.getScheme())) { if ("file".equals(uri.getScheme())) {
item = fromFile(uri); item = fromFile(uri);
isFromFile = true;
} else { } else {
item = fromNetwork(destination); item = fromNetwork(destination);
isFromFile = false;
} }
Drawable result = null; Drawable result = null;
@ -122,13 +116,15 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
if (item != null if (item != null
&& item.inputStream != null) { && item.inputStream != null) {
try { try {
if (CONTENT_TYPE_SVG.equals(item.type)) {
result = handleSvg(item.inputStream); final MediaDecoder mediaDecoder = isFromFile
} else if (CONTENT_TYPE_GIF.equals(item.type)) { ? mediaDecoderFromFile(item.fileName)
result = handleGif(item.inputStream); : mediaDecoderFromContentType(item.contentType);
} else {
result = handleSimple(item.inputStream); if (mediaDecoder != null) {
result = mediaDecoder.decode(item.inputStream);
} }
} finally { } finally {
try { try {
item.inputStream.close(); item.inputStream.close();
@ -161,7 +157,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
}); });
} }
private Item fromFile(Uri uri) { @Nullable
private Item fromFile(@NonNull Uri uri) {
final List<String> segments = uri.getPathSegments(); final List<String> segments = uri.getPathSegments();
if (segments == null if (segments == null
@ -171,19 +168,10 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
} }
final Item out; final Item out;
final String type;
final InputStream inputStream; final InputStream inputStream;
final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0)); final boolean assets = FILE_ANDROID_ASSETS.equals(segments.get(0));
final String lastSegment = uri.getLastPathSegment(); final String fileName = uri.getLastPathSegment();
if (lastSegment.endsWith(".svg")) {
type = CONTENT_TYPE_SVG;
} else if (lastSegment.endsWith(".gif")) {
type = CONTENT_TYPE_GIF;
} else {
type = null;
}
if (assets) { if (assets) {
final StringBuilder path = new StringBuilder(); final StringBuilder path = new StringBuilder();
@ -212,7 +200,7 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
} }
if (inputStream != null) { if (inputStream != null) {
out = new Item(type, inputStream); out = new Item(fileName, null, inputStream);
} else { } else {
out = null; out = null;
} }
@ -220,7 +208,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out; return out;
} }
private Item fromNetwork(String destination) { @Nullable
private Item fromNetwork(@NonNull String destination) {
Item out = null; Item out = null;
@ -241,15 +230,8 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
if (body != null) { if (body != null) {
final InputStream inputStream = body.byteStream(); final InputStream inputStream = body.byteStream();
if (inputStream != null) { if (inputStream != null) {
final String type;
final String contentType = response.header(HEADER_CONTENT_TYPE); final String contentType = response.header(HEADER_CONTENT_TYPE);
if (!TextUtils.isEmpty(contentType) out = new Item(null, contentType, inputStream);
&& contentType.startsWith(CONTENT_TYPE_SVG)) {
type = CONTENT_TYPE_SVG;
} else {
type = contentType;
}
out = new Item(type, inputStream);
} }
} }
} }
@ -257,94 +239,31 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
return out; return out;
} }
private Drawable handleSvg(InputStream stream) { @Nullable
private MediaDecoder mediaDecoderFromFile(@NonNull String fileName) {
final Drawable out; MediaDecoder out = null;
SVG svg = null; for (MediaDecoder mediaDecoder : mediaDecoders) {
try { if (mediaDecoder.canDecodeByFileName(fileName)) {
svg = SVG.getFromInputStream(stream); out = mediaDecoder;
} catch (SVGParseException e) { break;
e.printStackTrace();
}
if (svg == null) {
out = null;
} else {
final float w = svg.getDocumentWidth();
final float h = svg.getDocumentHeight();
final float density = resources.getDisplayMetrics().density;
final int width = (int) (w * density + .5F);
final int height = (int) (h * density + .5F);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.scale(density, density);
svg.renderToCanvas(canvas);
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
}
return out;
}
private Drawable handleGif(InputStream stream) {
Drawable out = null;
final byte[] bytes = readBytes(stream);
if (bytes != null) {
try {
out = new GifDrawable(bytes);
DrawableUtils.intrinsicBounds(out);
// @since 1.1.0
if (!autoPlayGif) {
((GifDrawable) out).pause();
}
} catch (IOException e) {
e.printStackTrace();
} }
} }
return out; return out;
} }
private Drawable handleSimple(InputStream stream) { @Nullable
private MediaDecoder mediaDecoderFromContentType(@Nullable String contentType) {
final Drawable out; MediaDecoder out = null;
final Bitmap bitmap = BitmapFactory.decodeStream(stream); for (MediaDecoder mediaDecoder : mediaDecoders) {
if (bitmap != null) { if (mediaDecoder.canDecodeByContentType(contentType)) {
out = new BitmapDrawable(resources, bitmap); out = mediaDecoder;
DrawableUtils.intrinsicBounds(out); break;
} else {
out = null;
} }
return out;
}
private static byte[] readBytes(InputStream stream) {
byte[] out = null;
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final int length = 1024 * 8;
final byte[] buffer = new byte[length];
int read;
while ((read = stream.read(buffer, 0, length)) != -1) {
outputStream.write(buffer, 0, read);
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
} }
return out; return out;
@ -358,61 +277,92 @@ public class AsyncDrawableLoader implements AsyncDrawable.Loader {
private Drawable errorDrawable; private Drawable errorDrawable;
// @since 1.1.0 // @since 1.1.0
private boolean autoPlayGif = true; private final List<MediaDecoder> mediaDecoders = new ArrayList<>(3);
@NonNull
public Builder client(@NonNull OkHttpClient client) { public Builder client(@NonNull OkHttpClient client) {
this.client = client; this.client = client;
return this; return this;
} }
/**
* Supplied resources argument will be used to open files from assets directory
* and to create default {@link MediaDecoder}\'s which require resources instance
*
* @return self
*/
@NonNull
public Builder resources(@NonNull Resources resources) { public Builder resources(@NonNull Resources resources) {
this.resources = resources; this.resources = resources;
return this; return this;
} }
public Builder executorService(ExecutorService executorService) { @NonNull
public Builder executorService(@NonNull ExecutorService executorService) {
this.executorService = executorService; this.executorService = executorService;
return this; return this;
} }
public Builder errorDrawable(Drawable errorDrawable) { @NonNull
public Builder errorDrawable(@NonNull Drawable errorDrawable) {
this.errorDrawable = errorDrawable; this.errorDrawable = errorDrawable;
return this; return this;
} }
/**
* @param autoPlayGif flag indicating if loaded gif should automatically start when displayed
* @return self
* @since 1.1.0
*/
@NonNull @NonNull
public Builder autoPlayGif(boolean autoPlayGif) { public Builder mediaDecoders(@NonNull List<MediaDecoder> mediaDecoders) {
this.autoPlayGif = autoPlayGif; this.mediaDecoders.clear();
this.mediaDecoders.addAll(mediaDecoders);
return this;
}
@NonNull
public Builder mediaDecoders(MediaDecoder... mediaDecoders) {
this.mediaDecoders.clear();
if (mediaDecoders != null
&& mediaDecoders.length > 0) {
Collections.addAll(this.mediaDecoders, mediaDecoders);
}
return this; return this;
} }
@NonNull @NonNull
public AsyncDrawableLoader build() { public AsyncDrawableLoader build() {
if (client == null) { if (client == null) {
client = new OkHttpClient(); client = new OkHttpClient();
} }
if (resources == null) { if (resources == null) {
resources = Resources.getSystem(); resources = Resources.getSystem();
} }
if (executorService == null) { if (executorService == null) {
// we will use executor from okHttp // we will use executor from okHttp
executorService = client.dispatcher().executorService(); executorService = client.dispatcher().executorService();
} }
// add default media decoders if not specified
if (mediaDecoders.size() == 0) {
mediaDecoders.add(SvgMediaDecoder.create(resources));
mediaDecoders.add(GifMediaDecoder.create(true));
mediaDecoders.add(ImageMediaDecoder.create(resources));
}
return new AsyncDrawableLoader(this); return new AsyncDrawableLoader(this);
} }
} }
private static class Item { private static class Item {
final String type;
final String fileName;
final String contentType;
final InputStream inputStream; final InputStream inputStream;
Item(String type, InputStream inputStream) { Item(@Nullable String fileName, @Nullable String contentType, @Nullable InputStream inputStream) {
this.type = type; this.fileName = fileName;
this.contentType = contentType;
this.inputStream = inputStream; this.inputStream = inputStream;
} }
} }

View File

@ -0,0 +1,90 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import pl.droidsonroids.gif.GifDrawable;
/**
* @since 1.1.0
*/
public class GifMediaDecoder extends MediaDecoder {
protected static final String CONTENT_TYPE_GIF = "image/gif";
protected static final String FILE_EXTENSION_GIF = ".gif";
@NonNull
public static GifMediaDecoder create(boolean autoPlayGif) {
return new GifMediaDecoder(autoPlayGif);
}
private final boolean autoPlayGif;
protected GifMediaDecoder(boolean autoPlayGif) {
this.autoPlayGif = autoPlayGif;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return CONTENT_TYPE_GIF.equals(contentType);
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return fileName.endsWith(FILE_EXTENSION_GIF);
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
Drawable out = null;
final byte[] bytes = readBytes(inputStream);
if (bytes != null) {
try {
out = newGifDrawable(bytes);
DrawableUtils.intrinsicBounds(out);
if (!autoPlayGif) {
((GifDrawable) out).pause();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return out;
}
@NonNull
protected Drawable newGifDrawable(@NonNull byte[] bytes) throws IOException {
return new GifDrawable(bytes);
}
@Nullable
protected static byte[] readBytes(@NonNull InputStream stream) {
byte[] out = null;
try {
final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
final int length = 1024 * 8;
final byte[] buffer = new byte[length];
int read;
while ((read = stream.read(buffer, 0, length)) != -1) {
outputStream.write(buffer, 0, read);
}
out = outputStream.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return out;
}
}

View File

@ -0,0 +1,58 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* This class can be used as the last {@link MediaDecoder} to _try_ to handle all rest cases.
* Here we just assume that supplied InputStream is of image type and try to decode it.
*
* @since 1.1.0
*/
public class ImageMediaDecoder extends MediaDecoder {
@NonNull
public static ImageMediaDecoder create(@NonNull Resources resources) {
return new ImageMediaDecoder(resources);
}
private final Resources resources;
ImageMediaDecoder(Resources resources) {
this.resources = resources;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return true;
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return true;
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
final Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
if (bitmap != null) {
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
} else {
out = null;
}
return out;
}
}

View File

@ -0,0 +1,20 @@
package ru.noties.markwon.il;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import java.io.InputStream;
/**
* @since 1.1.0
*/
public abstract class MediaDecoder {
public abstract boolean canDecodeByContentType(@Nullable String contentType);
public abstract boolean canDecodeByFileName(@NonNull String fileName);
@Nullable
public abstract Drawable decode(@NonNull InputStream inputStream);
}

View File

@ -0,0 +1,80 @@
package ru.noties.markwon.il;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import com.caverock.androidsvg.SVG;
import com.caverock.androidsvg.SVGParseException;
import java.io.InputStream;
/**
* @since 1.1.0
*/
public class SvgMediaDecoder extends MediaDecoder {
private static final String CONTENT_TYPE_SVG = "image/svg+xml";
private static final String FILE_EXTENSION_SVG = ".svg";
@NonNull
public static SvgMediaDecoder create(@NonNull Resources resources) {
return new SvgMediaDecoder(resources);
}
private final Resources resources;
SvgMediaDecoder(Resources resources) {
this.resources = resources;
}
@Override
public boolean canDecodeByContentType(@Nullable String contentType) {
return contentType != null && contentType.startsWith(CONTENT_TYPE_SVG);
}
@Override
public boolean canDecodeByFileName(@NonNull String fileName) {
return fileName.endsWith(FILE_EXTENSION_SVG);
}
@Nullable
@Override
public Drawable decode(@NonNull InputStream inputStream) {
final Drawable out;
SVG svg = null;
try {
svg = SVG.getFromInputStream(inputStream);
} catch (SVGParseException e) {
e.printStackTrace();
}
if (svg == null) {
out = null;
} else {
final float w = svg.getDocumentWidth();
final float h = svg.getDocumentHeight();
final float density = resources.getDisplayMetrics().density;
final int width = (int) (w * density + .5F);
final int height = (int) (h * density + .5F);
final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_4444);
final Canvas canvas = new Canvas(bitmap);
canvas.scale(density, density);
svg.renderToCanvas(canvas);
out = new BitmapDrawable(resources, bitmap);
DrawableUtils.intrinsicBounds(out);
}
return out;
}
}