From 19091b5675006d74ed407b210ecfa379a4c46952 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 29 May 2019 17:44:05 +0300 Subject: [PATCH] Add image loading plugin based on picasso library --- build.gradle | 3 +- .../ru/noties/markwon/core/CorePlugin.java | 52 +++++ .../noties/markwon/image/AsyncDrawable.java | 18 +- .../markwon/image/AsyncDrawableScheduler.java | 2 + .../markwon/image/ImageSpanFactory.java | 0 markwon-image-picasso/build.gradle | 21 +++ markwon-image-picasso/gradle.properties | 4 + .../src/main/AndroidManifest.xml | 1 + .../image/picasso/PicassoImagesPlugin.java | 177 ++++++++++++++++++ .../image/AsyncDrawableLoaderBuilder.java | 12 +- .../ru/noties/markwon/image/ImagesPlugin.java | 50 ----- .../markwon/image/file/FileSchemeHandler.java | 11 ++ sample/build.gradle | 2 + .../sample/recycler/RecyclerActivity.java | 26 +-- settings.gradle | 1 + 15 files changed, 311 insertions(+), 69 deletions(-) rename {markwon-image => markwon-core}/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java (100%) create mode 100644 markwon-image-picasso/build.gradle create mode 100644 markwon-image-picasso/gradle.properties create mode 100644 markwon-image-picasso/src/main/AndroidManifest.xml create mode 100644 markwon-image-picasso/src/main/java/ru/noties/markwon/image/picasso/PicassoImagesPlugin.java diff --git a/build.gradle b/build.gradle index 63782269..a7574d80 100644 --- a/build.gradle +++ b/build.gradle @@ -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'] = [ diff --git a/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java b/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java index 4882441a..8f471a1e 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java +++ b/markwon-core/src/main/java/ru/noties/markwon/core/CorePlugin.java @@ -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() { + @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, diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java index 239dad4e..10e4d45d 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawable.java @@ -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 + + '}'; + } } diff --git a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java index dd1735cf..83a4b150 100644 --- a/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java +++ b/markwon-core/src/main/java/ru/noties/markwon/image/AsyncDrawableScheduler.java @@ -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; } diff --git a/markwon-image/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java b/markwon-core/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java similarity index 100% rename from markwon-image/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java rename to markwon-core/src/main/java/ru/noties/markwon/image/ImageSpanFactory.java diff --git a/markwon-image-picasso/build.gradle b/markwon-image-picasso/build.gradle new file mode 100644 index 00000000..b646c01c --- /dev/null +++ b/markwon-image-picasso/build.gradle @@ -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) \ No newline at end of file diff --git a/markwon-image-picasso/gradle.properties b/markwon-image-picasso/gradle.properties new file mode 100644 index 00000000..70a5e05a --- /dev/null +++ b/markwon-image-picasso/gradle.properties @@ -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 \ No newline at end of file diff --git a/markwon-image-picasso/src/main/AndroidManifest.xml b/markwon-image-picasso/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a32bdc6e --- /dev/null +++ b/markwon-image-picasso/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/markwon-image-picasso/src/main/java/ru/noties/markwon/image/picasso/PicassoImagesPlugin.java b/markwon-image-picasso/src/main/java/ru/noties/markwon/image/picasso/PicassoImagesPlugin.java new file mode 100644 index 00000000..5eeb5b6d --- /dev/null +++ b/markwon-image-picasso/src/main/java/ru/noties/markwon/image/picasso/PicassoImagesPlugin.java @@ -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 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); + } + } + } +} diff --git a/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java b/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java index 6bbe234b..845d3e48 100644 --- a/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java +++ b/markwon-image/src/main/java/ru/noties/markwon/image/AsyncDrawableLoaderBuilder.java @@ -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(); } diff --git a/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index e8cd8324..77950ad5 100644 --- a/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon-image/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -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() { - @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); diff --git a/markwon-image/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java b/markwon-image/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java index 681fb29f..f09906a1 100644 --- a/markwon-image/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java +++ b/markwon-image/src/main/java/ru/noties/markwon/image/file/FileSchemeHandler.java @@ -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); diff --git a/sample/build.gradle b/sample/build.gradle index a16436b9..86934833 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -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'] diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java index 7b7cc091..c187ea93 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -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()) diff --git a/settings.gradle b/settings.gradle index f0568523..e5b09011 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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',