Update sample application to add play-pause functionality for gifs

This commit is contained in:
Dimitry Ivanov 2018-07-21 16:35:05 +03:00
parent bd08178a55
commit 830ec9180b
10 changed files with 317 additions and 2 deletions

View File

@ -107,4 +107,10 @@ class AppModule {
Prism4jThemeDarkula prism4jThemeDarkula() {
return Prism4jThemeDarkula.create();
}
@Singleton
@Provides
GifProcessor gifProcessor() {
return GifProcessor.create();
}
}

View File

@ -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);
}
}
}
}

View File

@ -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
);
}
}

View File

@ -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;
}
}

View File

@ -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();
}
}
}
}
}

View File

@ -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);
}
});

View File

@ -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();

Binary file not shown.

After

Width:  |  Height:  |  Size: 322 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 378 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 536 B