diff --git a/app/src/main/java/ru/noties/markwon/AppModule.java b/app/src/main/java/ru/noties/markwon/AppModule.java index a0c751cf..e4797f90 100644 --- a/app/src/main/java/ru/noties/markwon/AppModule.java +++ b/app/src/main/java/ru/noties/markwon/AppModule.java @@ -107,4 +107,10 @@ class AppModule { Prism4jThemeDarkula prism4jThemeDarkula() { return Prism4jThemeDarkula.create(); } + + @Singleton + @Provides + GifProcessor gifProcessor() { + return GifProcessor.create(); + } } diff --git a/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java new file mode 100644 index 00000000..78d286af --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifAwareAsyncDrawable.java @@ -0,0 +1,58 @@ +package ru.noties.markwon; + +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.spans.AsyncDrawable; + +public class GifAwareAsyncDrawable extends AsyncDrawable { + + public interface OnGifResultListener { + void onGifResult(@NonNull GifAwareAsyncDrawable drawable); + } + + private final Drawable gifPlaceholder; + private OnGifResultListener onGifResultListener; + private boolean isGif; + + public GifAwareAsyncDrawable( + @NonNull Drawable gifPlaceholder, + @NonNull String destination, + @NonNull Loader loader, + @Nullable ImageSizeResolver imageSizeResolver, + @Nullable ImageSize imageSize) { + super(destination, loader, imageSizeResolver, imageSize); + this.gifPlaceholder = gifPlaceholder; + } + + public void onGifResultListener(@Nullable OnGifResultListener onGifResultListener) { + this.onGifResultListener = onGifResultListener; + } + + @Override + public void setResult(@NonNull Drawable result) { + super.setResult(result); + isGif = result instanceof GifDrawable; + if (isGif && onGifResultListener != null) { + onGifResultListener.onGifResult(this); + } + } + + @Override + public void draw(@NonNull Canvas canvas) { + super.draw(canvas); + + if (isGif) { + final GifDrawable drawable = (GifDrawable) getResult(); + if (!drawable.isPlaying()) { + gifPlaceholder.setBounds(drawable.getBounds()); + gifPlaceholder.draw(canvas); + } + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java new file mode 100644 index 00000000..f070e9fc --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifAwareSpannableFactory.java @@ -0,0 +1,36 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import ru.noties.markwon.renderer.ImageSize; +import ru.noties.markwon.renderer.ImageSizeResolver; +import ru.noties.markwon.spans.AsyncDrawable; +import ru.noties.markwon.spans.AsyncDrawableSpan; +import ru.noties.markwon.spans.SpannableTheme; + +public class GifAwareSpannableFactory extends SpannableFactoryDef { + + private final GifPlaceholder gifPlaceholder; + + public GifAwareSpannableFactory(@NonNull GifPlaceholder gifPlaceholder) { + this.gifPlaceholder = gifPlaceholder; + } + + @Nullable + @Override + public Object image(@NonNull SpannableTheme theme, @NonNull String destination, @NonNull AsyncDrawable.Loader loader, @NonNull ImageSizeResolver imageSizeResolver, @Nullable ImageSize imageSize, boolean replacementTextIsLink) { + return new AsyncDrawableSpan( + theme, + new GifAwareAsyncDrawable( + gifPlaceholder, + destination, + loader, + imageSizeResolver, + imageSize + ), + AsyncDrawableSpan.ALIGN_BOTTOM, + replacementTextIsLink + ); + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifPlaceholder.java b/app/src/main/java/ru/noties/markwon/GifPlaceholder.java new file mode 100644 index 00000000..0ee66d0c --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifPlaceholder.java @@ -0,0 +1,77 @@ +package ru.noties.markwon; + +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.support.annotation.ColorInt; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public class GifPlaceholder extends Drawable { + + private final Drawable icon; + private final Paint paint; + + private float left; + private float top; + + public GifPlaceholder(@NonNull Drawable icon, @ColorInt int background) { + this.icon = icon; + if (icon.getBounds().isEmpty()) { + icon.setBounds(0, 0, icon.getIntrinsicWidth(), icon.getIntrinsicHeight()); + } + + if (background != 0) { + paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + paint.setColor(background); + } else { + paint = null; + } + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + + final int w = bounds.width(); + final int h = bounds.height(); + + this.left = (w - icon.getBounds().width()) / 2; + this.top = (h - icon.getBounds().height()) / 2; + } + + @Override + public void draw(@NonNull Canvas canvas) { + + if (paint != null) { + canvas.drawRect(getBounds(), paint); + } + + final int save = canvas.save(); + try { + canvas.translate(left, top); + icon.draw(canvas); + } finally { + canvas.restoreToCount(save); + } + } + + @Override + public void setAlpha(int alpha) { + // no op + } + + @Override + public void setColorFilter(@Nullable ColorFilter colorFilter) { + // no op + } + + @Override + public int getOpacity() { + return PixelFormat.OPAQUE; + } +} diff --git a/app/src/main/java/ru/noties/markwon/GifProcessor.java b/app/src/main/java/ru/noties/markwon/GifProcessor.java new file mode 100644 index 00000000..7d2cd7c6 --- /dev/null +++ b/app/src/main/java/ru/noties/markwon/GifProcessor.java @@ -0,0 +1,125 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spannable; +import android.text.Spanned; +import android.text.style.ClickableSpan; +import android.view.View; +import android.widget.TextView; + +import pl.droidsonroids.gif.GifDrawable; +import ru.noties.markwon.spans.AsyncDrawableSpan; + +public abstract class GifProcessor { + + public abstract void process(@NonNull TextView textView); + + @NonNull + public static GifProcessor create() { + return new Impl(); + } + + static class Impl extends GifProcessor { + + @Override + public void process(@NonNull final TextView textView) { + + // here is what we will do additionally: + // we query for all asyncDrawableSpans + // we check if they are inside clickableSpan + // if not we apply onGifListener + + final Spannable spannable = spannable(textView); + if (spannable == null) { + return; + } + + final AsyncDrawableSpan[] asyncDrawableSpans = + spannable.getSpans(0, spannable.length(), AsyncDrawableSpan.class); + if (asyncDrawableSpans == null + || asyncDrawableSpans.length == 0) { + return; + } + + int start; + int end; + ClickableSpan[] clickableSpans; + + for (final AsyncDrawableSpan asyncDrawableSpan : asyncDrawableSpans) { + + start = spannable.getSpanStart(asyncDrawableSpan); + end = spannable.getSpanEnd(asyncDrawableSpan); + + if (start < 0 + || end < 0) { + continue; + } + + clickableSpans = spannable.getSpans(start, end, ClickableSpan.class); + if (clickableSpans != null + && clickableSpans.length > 0) { + continue; + } + + ((GifAwareAsyncDrawable) asyncDrawableSpan.getDrawable()).onGifResultListener(new GifAwareAsyncDrawable.OnGifResultListener() { + @Override + public void onGifResult(@NonNull GifAwareAsyncDrawable drawable) { + addGifClickSpan(textView, asyncDrawableSpan, drawable); + } + }); + } + } + + @Nullable + private static Spannable spannable(@NonNull TextView textView) { + final CharSequence charSequence = textView.getText(); + if (charSequence instanceof Spannable) { + return (Spannable) charSequence; + } + return null; + } + + private static void addGifClickSpan( + @NonNull TextView textView, + @NonNull AsyncDrawableSpan span, + @NonNull GifAwareAsyncDrawable drawable) { + + // important thing here is to obtain new spannable from textView + // as with each `setText()` new spannable is created and keeping reference + // to an older one won't affect textView + final Spannable spannable = spannable(textView); + if (spannable == null) { + return; + } + + final int start = spannable.getSpanStart(span); + final int end = spannable.getSpanEnd(span); + if (start < 0 + || end < 0) { + return; + } + + final GifDrawable gifDrawable = (GifDrawable) drawable.getResult(); + spannable.setSpan(new GifToggleClickableSpan(gifDrawable), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static class GifToggleClickableSpan extends ClickableSpan { + + private final GifDrawable gifDrawable; + + GifToggleClickableSpan(@NonNull GifDrawable gifDrawable) { + this.gifDrawable = gifDrawable; + } + + @Override + public void onClick(View widget) { + if (gifDrawable.isPlaying()) { + gifDrawable.pause(); + } else { + gifDrawable.start(); + } + } + } + } +} diff --git a/app/src/main/java/ru/noties/markwon/MainActivity.java b/app/src/main/java/ru/noties/markwon/MainActivity.java index 9d97bcfc..19882a74 100644 --- a/app/src/main/java/ru/noties/markwon/MainActivity.java +++ b/app/src/main/java/ru/noties/markwon/MainActivity.java @@ -28,8 +28,11 @@ public class MainActivity extends Activity { @Inject UriProcessor uriProcessor; + @Inject + GifProcessor gifProcessor; + @Override - protected void onCreate(Bundle savedInstanceState) { + protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); App.component(this) @@ -67,7 +70,11 @@ public class MainActivity extends Activity { markdownRenderer.render(MainActivity.this, themes.isLight(), uri(), text, new MarkdownRenderer.MarkdownReadyListener() { @Override public void onMarkdownReady(CharSequence markdown) { + Markwon.setText(textView, markdown, BetterLinkMovementMethod.getInstance()); + + gifProcessor.process(textView); + Views.setVisible(progress, false); } }); diff --git a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java index 140056c3..23ff268b 100644 --- a/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java +++ b/app/src/main/java/ru/noties/markwon/MarkdownRenderer.java @@ -15,9 +15,9 @@ import javax.inject.Inject; import ru.noties.debug.Debug; import ru.noties.markwon.spans.AsyncDrawable; import ru.noties.markwon.spans.SpannableTheme; -import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jSyntaxHighlight; import ru.noties.markwon.syntax.Prism4jTheme; +import ru.noties.markwon.syntax.Prism4jThemeDarkula; import ru.noties.markwon.syntax.Prism4jThemeDefault; import ru.noties.prism4j.Prism4j; @@ -82,6 +82,11 @@ public class MarkdownRenderer { ? prism4jTheme.background() : 0x0Fffffff; + final GifPlaceholder gifPlaceholder = new GifPlaceholder( + context.getResources().getDrawable(R.drawable.ic_play_circle_filled_18dp_white), + 0x20000000 + ); + final SpannableConfiguration configuration = SpannableConfiguration.builder(context) .asyncDrawableLoader(loader) .urlProcessor(urlProcessor) @@ -90,6 +95,7 @@ public class MarkdownRenderer { .codeBackgroundColor(background) .codeTextColor(prism4jTheme.textColor()) .build()) + .factory(new GifAwareSpannableFactory(gifPlaceholder)) .build(); final long start = SystemClock.uptimeMillis(); diff --git a/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png new file mode 100644 index 00000000..7354e795 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_play_circle_filled_18dp_white.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png new file mode 100644 index 00000000..c0358ebe Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_play_circle_filled_18dp_white.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png b/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png new file mode 100644 index 00000000..53dadfdf Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_play_circle_filled_18dp_white.png differ