Add image loading plugin based on picasso library

This commit is contained in:
Dimitry Ivanov 2019-05-29 17:44:05 +03:00
parent e35d3ad044
commit 19091b5675
15 changed files with 311 additions and 69 deletions

View File

@ -71,7 +71,8 @@ ext {
'prism4j' : 'ru.noties:prism4j:1.1.0',
'debug' : 'ru.noties:debug:3.0.0@jar',
'adapt' : 'ru.noties:adapt:1.1.0',
'dagger' : "com.google.dagger:dagger:$daggerVersion"
'dagger' : "com.google.dagger:dagger:$daggerVersion",
'picasso' : "com.squareup.picasso:picasso:2.71828"
]
deps['annotationProcessor'] = [

View File

@ -14,6 +14,7 @@ import org.commonmark.node.Emphasis;
import org.commonmark.node.FencedCodeBlock;
import org.commonmark.node.HardLineBreak;
import org.commonmark.node.Heading;
import org.commonmark.node.Image;
import org.commonmark.node.IndentedCodeBlock;
import org.commonmark.node.Link;
import org.commonmark.node.ListBlock;
@ -30,6 +31,8 @@ import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.RenderProps;
import ru.noties.markwon.SpanFactory;
import ru.noties.markwon.core.factory.BlockQuoteSpanFactory;
import ru.noties.markwon.core.factory.CodeBlockSpanFactory;
import ru.noties.markwon.core.factory.CodeSpanFactory;
@ -40,6 +43,7 @@ import ru.noties.markwon.core.factory.ListItemSpanFactory;
import ru.noties.markwon.core.factory.StrongEmphasisSpanFactory;
import ru.noties.markwon.core.factory.ThematicBreakSpanFactory;
import ru.noties.markwon.core.spans.OrderedListItemSpan;
import ru.noties.markwon.image.ImageProps;
/**
* @see CoreProps
@ -64,6 +68,7 @@ public class CorePlugin extends AbstractMarkwonPlugin {
code(builder);
fencedCodeBlock(builder);
indentedCodeBlock(builder);
image(builder);
bulletList(builder);
orderedList(builder);
listItem(builder);
@ -197,6 +202,53 @@ public class CorePlugin extends AbstractMarkwonPlugin {
});
}
// @since 4.0.0-SNAPSHOT
// his method is moved from ImagesPlugin. Alternative implementations must set SpanFactory
// for Image node in order for this visitor to function
private static void image(MarkwonVisitor.Builder builder) {
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) {
// if there is no image spanFactory, ignore
final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Image.class);
if (spanFactory == null) {
visitor.visitChildren(image);
return;
}
final int length = visitor.length();
visitor.visitChildren(image);
// we must check if anything _was_ added, as we need at least one char to render
if (length == visitor.length()) {
visitor.builder().append('\uFFFC');
}
final MarkwonConfiguration configuration = visitor.configuration();
final Node parent = image.getParent();
final boolean link = parent instanceof Link;
final String destination = configuration
.urlProcessor()
.process(image.getDestination());
final RenderProps props = visitor.renderProps();
// apply image properties
// Please note that we explicitly set IMAGE_SIZE to null as we do not clear
// properties after we applied span (we could though)
ImageProps.DESTINATION.set(props, destination);
ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link);
ImageProps.IMAGE_SIZE.set(props, null);
visitor.setSpans(length, spanFactory.getSpans(configuration, props));
}
});
}
@VisibleForTesting
static void visitCodeBlock(
@NonNull MarkwonVisitor visitor,

View File

@ -261,7 +261,8 @@ public class AsyncDrawable extends Drawable {
if (hasResult()) {
out = result.getIntrinsicWidth();
} else {
out = 0;
// @since 4.0.0-SNAPSHOT, must not be zero in order to receive canvas dimensions
out = 1;
}
return out;
}
@ -272,7 +273,8 @@ public class AsyncDrawable extends Drawable {
if (hasResult()) {
out = result.getIntrinsicHeight();
} else {
out = 0;
// @since 4.0.0-SNAPSHOT, must not be zero in order to receive canvas dimensions
out = 1;
}
return out;
}
@ -290,4 +292,16 @@ public class AsyncDrawable extends Drawable {
? imageSizeResolver.resolveImageSize(imageSize, result.getBounds(), canvasWidth, textSize)
: result.getBounds();
}
@Override
public String toString() {
return "AsyncDrawable{" +
"destination='" + destination + '\'' +
", imageSize=" + imageSize +
", result=" + result +
", canvasWidth=" + canvasWidth +
", textSize=" + textSize +
", waitingForDimensions=" + waitingForDimensions +
'}';
}
}

View File

@ -23,6 +23,7 @@ public abstract class AsyncDrawableScheduler {
// hm... we need the same thing for unschedule then... we can check if last hash is !null,
// if it's not -> unschedule, else ignore
// @since 4.0.0-SNAPSHOT
final Integer lastTextHashCode =
(Integer) textView.getTag(R.id.markwon_drawables_scheduler_last_text_hashcode);
final int textHashCode = textView.getText().hashCode();
@ -67,6 +68,7 @@ public abstract class AsyncDrawableScheduler {
// must be called when text manually changed in TextView
public static void unschedule(@NonNull TextView view) {
// @since 4.0.0-SNAPSHOT
if (view.getTag(R.id.markwon_drawables_scheduler_last_text_hashcode) == null) {
return;
}

View File

@ -0,0 +1,21 @@
apply plugin: 'com.android.library'
android {
compileSdkVersion config['compile-sdk']
buildToolsVersion config['build-tools']
defaultConfig {
minSdkVersion config['min-sdk']
targetSdkVersion config['target-sdk']
versionCode 1
versionName version
}
}
dependencies {
api project(':markwon-core')
api deps['picasso']
}
registerArtifact(this)

View File

@ -0,0 +1,4 @@
POM_NAME=Image
POM_ARTIFACT_ID=image-picasso
POM_DESCRIPTION=Markwon image loading module (based on Picasso library)
POM_PACKAGING=aar

View File

@ -0,0 +1 @@
<manifest package="ru.noties.markwon.image.picasso" />

View File

@ -0,0 +1,177 @@
package ru.noties.markwon.image.picasso;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Spanned;
import android.widget.TextView;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import com.squareup.picasso.Target;
import org.commonmark.node.Image;
import java.util.HashMap;
import java.util.Map;
import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory;
import ru.noties.markwon.image.AsyncDrawable;
import ru.noties.markwon.image.AsyncDrawableLoader;
import ru.noties.markwon.image.AsyncDrawableScheduler;
import ru.noties.markwon.image.DrawableUtils;
import ru.noties.markwon.image.ImageSpanFactory;
/**
* @since 4.0.0-SNAPSHOT
*/
public class PicassoImagesPlugin extends AbstractMarkwonPlugin {
public interface PicassoStore {
@NonNull
RequestCreator load(@NonNull AsyncDrawable drawable);
void cancel(@NonNull AsyncDrawable drawable);
}
@NonNull
public static PicassoImagesPlugin create(@NonNull Context context) {
return create(new Picasso.Builder(context).build());
}
@NonNull
public static PicassoImagesPlugin create(@NonNull final Picasso picasso) {
return create(new PicassoStore() {
@NonNull
@Override
public RequestCreator load(@NonNull AsyncDrawable drawable) {
return picasso.load(drawable.getDestination())
.tag(drawable);
}
@Override
public void cancel(@NonNull AsyncDrawable drawable) {
picasso.cancelTag(drawable);
}
});
}
@NonNull
public static PicassoImagesPlugin create(@NonNull PicassoStore picassoStore) {
return new PicassoImagesPlugin(picassoStore);
}
private final PicassoAsyncDrawableLoader picassoAsyncDrawableLoader;
PicassoImagesPlugin(@NonNull PicassoStore picassoStore) {
this.picassoAsyncDrawableLoader = new PicassoAsyncDrawableLoader(picassoStore);
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.asyncDrawableLoader(picassoAsyncDrawableLoader);
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(Image.class, new ImageSpanFactory());
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
AsyncDrawableScheduler.unschedule(textView);
}
@Override
public void afterSetText(@NonNull TextView textView) {
AsyncDrawableScheduler.schedule(textView);
}
private static class PicassoAsyncDrawableLoader extends AsyncDrawableLoader {
private final PicassoStore picassoStore;
private final Map<AsyncDrawable, AsyncDrawableTarget> cache = new HashMap<>(2);
PicassoAsyncDrawableLoader(@NonNull PicassoStore picassoStore) {
this.picassoStore = picassoStore;
}
@Override
public void load(@NonNull AsyncDrawable drawable) {
// we must store hard-reference to target (otherwise it will be garbage-collected
// ad picasso internally stores a target in a weak-reference)
final AsyncDrawableTarget target = new AsyncDrawableTarget(drawable);
cache.put(drawable, target);
picassoStore.load(drawable)
.into(target);
}
@Override
public void cancel(@NonNull AsyncDrawable drawable) {
cache.remove(drawable);
picassoStore.cancel(drawable);
}
@Nullable
@Override
public Drawable placeholder(@NonNull AsyncDrawable drawable) {
return null;
}
private class AsyncDrawableTarget implements Target {
private final AsyncDrawable drawable;
AsyncDrawableTarget(@NonNull AsyncDrawable drawable) {
this.drawable = drawable;
}
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
if (cache.remove(drawable) != null) {
if (drawable.isAttached() && bitmap != null) {
final BitmapDrawable bitmapDrawable = new BitmapDrawable(Resources.getSystem(), bitmap);
DrawableUtils.applyIntrinsicBounds(bitmapDrawable);
drawable.setResult(bitmapDrawable);
}
}
}
@Override
public void onBitmapFailed(Exception e, Drawable errorDrawable) {
if (cache.remove(drawable) != null) {
if (errorDrawable != null
&& drawable.isAttached()) {
DrawableUtils.applyIntrinsicBoundsIfEmpty(errorDrawable);
drawable.setResult(errorDrawable);
}
}
e.printStackTrace();
}
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) {
if (placeHolderDrawable != null
&& canDeliver()) {
DrawableUtils.applyIntrinsicBoundsIfEmpty(placeHolderDrawable);
drawable.setResult(placeHolderDrawable);
}
}
private boolean canDeliver() {
return drawable.isAttached() && cache.containsKey(drawable);
}
}
}
}

View File

@ -73,13 +73,17 @@ class AsyncDrawableLoaderBuilder {
isBuilt = true;
// if we have no schemeHandlers -> we cannot show anything
// OR if we have no media decoders
if (schemeHandlers.size() == 0
|| (mediaDecoders.size() == 0 && defaultMediaDecoder == null)) {
// we must have schemeHandlers registered (we will provide
// default media decoder if it's absent)
if (schemeHandlers.size() == 0) {
return new AsyncDrawableLoaderNoOp();
}
// @since 4.0.0-SNAPSHOT
if (defaultMediaDecoder == null) {
defaultMediaDecoder = DefaultImageMediaDecoder.create();
}
if (executorService == null) {
executorService = Executors.newCachedThreadPool();
}

View File

@ -7,17 +7,12 @@ import android.text.Spanned;
import android.widget.TextView;
import org.commonmark.node.Image;
import org.commonmark.node.Link;
import org.commonmark.node.Node;
import java.util.concurrent.ExecutorService;
import ru.noties.markwon.AbstractMarkwonPlugin;
import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonSpansFactory;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.RenderProps;
import ru.noties.markwon.SpanFactory;
public class ImagesPlugin extends AbstractMarkwonPlugin {
@ -147,51 +142,6 @@ public class ImagesPlugin extends AbstractMarkwonPlugin {
builder.setFactory(Image.class, new ImageSpanFactory());
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(Image.class, new MarkwonVisitor.NodeVisitor<Image>() {
@Override
public void visit(@NonNull MarkwonVisitor visitor, @NonNull Image image) {
// if there is no image spanFactory, ignore
final SpanFactory spanFactory = visitor.configuration().spansFactory().get(Image.class);
if (spanFactory == null) {
visitor.visitChildren(image);
return;
}
final int length = visitor.length();
visitor.visitChildren(image);
// we must check if anything _was_ added, as we need at least one char to render
if (length == visitor.length()) {
visitor.builder().append('\uFFFC');
}
final MarkwonConfiguration configuration = visitor.configuration();
final Node parent = image.getParent();
final boolean link = parent instanceof Link;
final String destination = configuration
.urlProcessor()
.process(image.getDestination());
final RenderProps props = visitor.renderProps();
// apply image properties
// Please note that we explicitly set IMAGE_SIZE to null as we do not clear
// properties after we applied span (we could though)
ImageProps.DESTINATION.set(props, destination);
ImageProps.REPLACEMENT_TEXT_IS_LINK.set(props, link);
ImageProps.IMAGE_SIZE.set(props, null);
visitor.setSpans(length, spanFactory.getSpans(configuration, props));
}
});
}
@Override
public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) {
AsyncDrawableScheduler.unschedule(textView);

View File

@ -1,5 +1,6 @@
package ru.noties.markwon.image.file;
import android.content.Context;
import android.content.res.AssetManager;
import android.net.Uri;
import android.support.annotation.NonNull;
@ -35,6 +36,16 @@ public class FileSchemeHandler extends SchemeHandler {
return new FileSchemeHandler(assetManager);
}
/**
* @see #createWithAssets(AssetManager)
* @see ru.noties.markwon.urlprocessor.UrlProcessorAndroidAssets
* @since 4.0.0-SNAPSHOT
*/
@NonNull
public static FileSchemeHandler createWithAssets(@NonNull Context context) {
return new FileSchemeHandler(context.getAssets());
}
@NonNull
public static FileSchemeHandler create() {
return new FileSchemeHandler(null);

View File

@ -44,6 +44,8 @@ dependencies {
implementation project(':markwon-recycler')
implementation project(':markwon-recycler-table')
implementation project(':markwon-image-picasso')
deps.with {
implementation it['support-recycler-view']
implementation it['okhttp']

View File

@ -2,6 +2,8 @@ package ru.noties.markwon.sample.recycler;
import android.app.Activity;
import android.content.Context;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
@ -10,6 +12,9 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.RequestCreator;
import org.commonmark.ext.gfm.tables.TableBlock;
import org.commonmark.node.FencedCodeBlock;
@ -26,11 +31,8 @@ import ru.noties.markwon.MarkwonConfiguration;
import ru.noties.markwon.MarkwonVisitor;
import ru.noties.markwon.core.CorePlugin;
import ru.noties.markwon.html.HtmlPlugin;
import ru.noties.markwon.image.DefaultImageMediaDecoder;
import ru.noties.markwon.image.ImagesPlugin;
import ru.noties.markwon.image.file.FileSchemeHandler;
import ru.noties.markwon.image.network.OkHttpNetworkSchemeHandler;
import ru.noties.markwon.image.svg.SvgMediaDecoder;
import ru.noties.markwon.image.AsyncDrawable;
import ru.noties.markwon.image.picasso.PicassoImagesPlugin;
import ru.noties.markwon.recycler.MarkwonAdapter;
import ru.noties.markwon.recycler.SimpleEntry;
import ru.noties.markwon.recycler.table.TableEntry;
@ -77,13 +79,13 @@ public class RecyclerActivity extends Activity {
private static Markwon markwon(@NonNull Context context) {
return Markwon.builder(context)
.usePlugin(CorePlugin.create())
.usePlugin(ImagesPlugin.create(plugin -> {
plugin
.addSchemeHandler(FileSchemeHandler.createWithAssets(context.getAssets()))
.addSchemeHandler(OkHttpNetworkSchemeHandler.create())
.addMediaDecoder(SvgMediaDecoder.create())
.defaultMediaDecoder(DefaultImageMediaDecoder.create());
}))
// .usePlugin(ImagesPlugin.create(plugin -> {
// plugin
// .addSchemeHandler(FileSchemeHandler.createWithAssets(context))
// .addSchemeHandler(OkHttpNetworkSchemeHandler.create())
// .addMediaDecoder(SvgMediaDecoder.create());
// }))
.usePlugin(PicassoImagesPlugin.create(context))
// important to use TableEntryPlugin instead of TablePlugin
.usePlugin(TableEntryPlugin.create(context))
.usePlugin(HtmlPlugin.create())

View File

@ -7,6 +7,7 @@ include ':app', ':sample',
':markwon-ext-tasklist',
':markwon-html',
':markwon-image',
':markwon-image-picasso',
':markwon-recycler',
':markwon-recycler-table',
':markwon-syntax-highlight',