diff --git a/gradle.properties b/gradle.properties index 5797d1d0..e9c33f08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -8,7 +8,7 @@ android.enableJetifier=true android.enableBuildCache=true android.buildCacheDir=build/pre-dex-cache -VERSION_NAME=4.0.2 +VERSION_NAME=4.1.0-SNAPSHOT GROUP=io.noties.markwon POM_DESCRIPTION=Markwon markdown for Android diff --git a/markwon-core/src/main/java/io/noties/markwon/Markwon.java b/markwon-core/src/main/java/io/noties/markwon/Markwon.java index ac702599..431790c5 100644 --- a/markwon-core/src/main/java/io/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/io/noties/markwon/Markwon.java @@ -119,6 +119,29 @@ public abstract class Markwon { @Nullable public abstract

P getPlugin(@NonNull Class

type); + /** + * Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText + * functionality + * + * @see PrecomputedTextSetter + * @since 4.1.0-SNAPSHOT + */ + public interface TextSetter { + /** + * @param textView TextView + * @param markdown prepared markdown + * @param bufferType BufferType specified when building {@link Markwon} instance + * via {@link Builder#bufferType(TextView.BufferType)} + * @param onComplete action to run when set-text is finished (required to call in order + * to execute {@link MarkwonPlugin#afterSetText(TextView)}) + */ + void setText( + @NonNull TextView textView, + @NonNull Spanned markdown, + @NonNull TextView.BufferType bufferType, + @NonNull Runnable onComplete); + } + /** * Builder for {@link Markwon}. *

@@ -138,6 +161,13 @@ public abstract class Markwon { @NonNull Builder bufferType(@NonNull TextView.BufferType bufferType); + /** + * @param textSetter {@link TextSetter} to apply text to a TextView + * @since 4.1.0-SNAPSHOT + */ + @NonNull + Builder textSetter(@NonNull TextSetter textSetter); + @NonNull Builder usePlugin(@NonNull MarkwonPlugin plugin); diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java index 38ad7573..77de961f 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonBuilderImpl.java @@ -25,6 +25,8 @@ class MarkwonBuilderImpl implements Markwon.Builder { private TextView.BufferType bufferType = TextView.BufferType.SPANNABLE; + private Markwon.TextSetter textSetter; + MarkwonBuilderImpl(@NonNull Context context) { this.context = context; } @@ -36,6 +38,13 @@ class MarkwonBuilderImpl implements Markwon.Builder { return this; } + @NonNull + @Override + public Markwon.Builder textSetter(@NonNull Markwon.TextSetter textSetter) { + this.textSetter = textSetter; + return this; + } + @NonNull @Override public Markwon.Builder usePlugin(@NonNull MarkwonPlugin plugin) { @@ -97,6 +106,7 @@ class MarkwonBuilderImpl implements Markwon.Builder { return new MarkwonImpl( bufferType, + textSetter, parserBuilder.build(), visitorBuilder.build(configuration, renderProps), Collections.unmodifiableList(plugins) diff --git a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java index 199beb0b..3a8d6d1f 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java @@ -21,12 +21,18 @@ class MarkwonImpl extends Markwon { private final MarkwonVisitor visitor; private final List plugins; + // @since 4.1.0-SNAPSHOT + @Nullable + private final TextSetter textSetter; + MarkwonImpl( @NonNull TextView.BufferType bufferType, + @Nullable TextSetter textSetter, @NonNull Parser parser, @NonNull MarkwonVisitor visitor, @NonNull List plugins) { this.bufferType = bufferType; + this.textSetter = textSetter; this.parser = parser; this.visitor = visitor; this.plugins = plugins; @@ -78,16 +84,31 @@ class MarkwonImpl extends Markwon { } @Override - public void setParsedMarkdown(@NonNull TextView textView, @NonNull Spanned markdown) { + public void setParsedMarkdown(@NonNull final TextView textView, @NonNull Spanned markdown) { for (MarkwonPlugin plugin : plugins) { plugin.beforeSetText(textView, markdown); } - textView.setText(markdown, bufferType); + // @since 4.1.0-SNAPSHOT + if (textSetter != null) { + textSetter.setText(textView, markdown, bufferType, new Runnable() { + @Override + public void run() { + // on-complete we just must call `afterSetText` on all plugins + for (MarkwonPlugin plugin : plugins) { + plugin.afterSetText(textView); + } + } + }); + } else { - for (MarkwonPlugin plugin : plugins) { - plugin.afterSetText(textView); + // if no text-setter is specified -> just a regular sync operation + textView.setText(markdown, bufferType); + + for (MarkwonPlugin plugin : plugins) { + plugin.afterSetText(textView); + } } } diff --git a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java new file mode 100644 index 00000000..75452e21 --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java @@ -0,0 +1,80 @@ +package io.noties.markwon; + +import android.os.AsyncTask; +import android.os.Build; +import android.text.PrecomputedText; +import android.text.Spanned; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +import java.lang.ref.WeakReference; +import java.util.concurrent.Executor; + +/** + * @see io.noties.markwon.Markwon.TextSetter + * @since 4.1.0-SNAPSHOT + */ +@RequiresApi(Build.VERSION_CODES.P) +public class PrecomputedTextSetter implements Markwon.TextSetter { + + @NonNull + public static PrecomputedTextSetter create() { + return create(AsyncTask.THREAD_POOL_EXECUTOR); + } + + @NonNull + public static PrecomputedTextSetter create(@NonNull Executor executor) { + return new PrecomputedTextSetter(executor); + } + + private final Executor executor; + + @SuppressWarnings("WeakerAccess") + PrecomputedTextSetter(@NonNull Executor executor) { + this.executor = executor; + } + + @Override + public void setText( + @NonNull TextView textView, + @NonNull final Spanned markdown, + @NonNull final TextView.BufferType bufferType, + @NonNull final Runnable onComplete) { + final WeakReference reference = new WeakReference<>(textView); + executor.execute(new Runnable() { + @Override + public void run() { + final PrecomputedText precomputedText = precomputedText(reference.get(), markdown); + if (precomputedText != null) { + apply(reference.get(), precomputedText, bufferType, onComplete); + } + } + }); + } + + @Nullable + private static PrecomputedText precomputedText(@Nullable TextView textView, @NonNull Spanned spanned) { + return textView == null + ? null + : PrecomputedText.create(spanned, textView.getTextMetricsParams()); + } + + private static void apply( + @Nullable final TextView textView, + @NonNull final PrecomputedText precomputedText, + @NonNull final TextView.BufferType bufferType, + @NonNull final Runnable onComplete) { + if (textView != null) { + textView.post(new Runnable() { + @Override + public void run() { + textView.setText(precomputedText, bufferType); + onComplete.run(); + } + }); + } + } +} diff --git a/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java b/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java index 60f022ef..739470b7 100644 --- a/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java @@ -21,6 +21,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; @@ -42,6 +43,7 @@ public class MarkwonImplTest { final MarkwonPlugin plugin = mock(MarkwonPlugin.class); final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), mock(MarkwonVisitor.class), Collections.singletonList(plugin)); @@ -64,6 +66,7 @@ public class MarkwonImplTest { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, parser, mock(MarkwonVisitor.class), Arrays.asList(first, second)); @@ -89,6 +92,7 @@ public class MarkwonImplTest { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), visitor, Collections.singletonList(plugin)); @@ -130,6 +134,7 @@ public class MarkwonImplTest { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), visitor, Collections.emptyList()); @@ -160,6 +165,7 @@ public class MarkwonImplTest { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), visitor, Collections.singletonList(plugin)); @@ -195,6 +201,7 @@ public class MarkwonImplTest { final MarkwonPlugin plugin = mock(MarkwonPlugin.class); final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.EDITABLE, + null, mock(Parser.class), mock(MarkwonVisitor.class, RETURNS_MOCKS), Collections.singletonList(plugin)); @@ -241,6 +248,7 @@ public class MarkwonImplTest { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), mock(MarkwonVisitor.class), plugins); @@ -253,4 +261,43 @@ public class MarkwonImplTest { assertTrue("AbstractMarkwonPlugin", impl.hasPlugin(AbstractMarkwonPlugin.class)); assertTrue("MarkwonPlugin", impl.hasPlugin(MarkwonPlugin.class)); } + + @Test + public void text_setter() { + + final Markwon.TextSetter textSetter = mock(Markwon.TextSetter.class); + final MarkwonPlugin plugin = mock(MarkwonPlugin.class); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.EDITABLE, + textSetter, + mock(Parser.class), + mock(MarkwonVisitor.class), + Collections.singletonList(plugin)); + + final TextView textView = mock(TextView.class); + final Spanned spanned = mock(Spanned.class); + + impl.setParsedMarkdown(textView, spanned); + + final ArgumentCaptor textViewArgumentCaptor = + ArgumentCaptor.forClass(TextView.class); + final ArgumentCaptor spannedArgumentCaptor = + ArgumentCaptor.forClass(Spanned.class); + final ArgumentCaptor bufferTypeArgumentCaptor = + ArgumentCaptor.forClass(TextView.BufferType.class); + final ArgumentCaptor runnableArgumentCaptor = + ArgumentCaptor.forClass(Runnable.class); + + verify(textSetter, times(1)).setText( + textViewArgumentCaptor.capture(), + spannedArgumentCaptor.capture(), + bufferTypeArgumentCaptor.capture(), + runnableArgumentCaptor.capture()); + + assertEquals(textView, textViewArgumentCaptor.getValue()); + assertEquals(spanned, spannedArgumentCaptor.getValue()); + assertEquals(TextView.BufferType.EDITABLE, bufferTypeArgumentCaptor.getValue()); + assertNotNull(runnableArgumentCaptor.getValue()); + } } \ No newline at end of file