From 7e1255206016f372dc13cf33f3c5aad0e7502629 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Fri, 26 Jul 2019 15:34:48 +0300 Subject: [PATCH 1/6] Add TextSetter interface --- gradle.properties | 2 +- .../main/java/io/noties/markwon/Markwon.java | 30 +++++++ .../io/noties/markwon/MarkwonBuilderImpl.java | 10 +++ .../java/io/noties/markwon/MarkwonImpl.java | 31 +++++-- .../noties/markwon/PrecomputedTextSetter.java | 80 +++++++++++++++++++ .../io/noties/markwon/MarkwonImplTest.java | 47 +++++++++++ 6 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java 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 static Builder builderNoCore(@NonNull Context context) { @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 interface Builder { @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 @@ public Markwon.Builder bufferType(@NonNull TextView.BufferType bufferType) { 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 @@ public Markwon build() { 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 @@ public void setMarkdown(@NonNull TextView textView, @NonNull String markdown) { } @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); - - for (MarkwonPlugin plugin : plugins) { - plugin.afterSetText(textView); + // @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 { + + // 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 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 void parse_calls_plugin_process_markdown() { 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 void parse_markwon_processed() { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, parser, mock(MarkwonVisitor.class), Arrays.asList(first, second)); @@ -89,6 +92,7 @@ public void render_calls_plugins() { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), visitor, Collections.singletonList(plugin)); @@ -130,6 +134,7 @@ public void render_clears_visitor() { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), visitor, Collections.emptyList()); @@ -160,6 +165,7 @@ public Object answer(InvocationOnMock invocation) { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), visitor, Collections.singletonList(plugin)); @@ -195,6 +201,7 @@ public void set_parsed_markdown() { 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 @@ final class Second extends AbstractMarkwonPlugin { final MarkwonImpl impl = new MarkwonImpl( TextView.BufferType.SPANNABLE, + null, mock(Parser.class), mock(MarkwonVisitor.class), plugins); @@ -253,4 +261,43 @@ final class Second extends AbstractMarkwonPlugin { 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 From 54335dce6e5b0430bb08b6b188e81b7c5ed05fdd Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 27 Jul 2019 14:32:51 +0300 Subject: [PATCH 2/6] Change text-setter to use precomputed-text-compat (androix.core) --- CHANGELOG.md | 5 + build.gradle | 1 + markwon-core/build.gradle | 4 + .../main/java/io/noties/markwon/Markwon.java | 2 +- .../noties/markwon/PrecomputedTextSetter.java | 80 ------------ .../markwon/PrecomputedTextSetterCompat.java | 120 ++++++++++++++++++ sample/build.gradle | 1 + sample/src/main/AndroidManifest.xml | 1 + .../noties/markwon/sample/MainActivity.java | 5 + .../java/io/noties/markwon/sample/Sample.java | 4 +- .../precomputed/PrecomputedActivity.java | 38 ++++++ .../src/main/res/values/strings-samples.xml | 2 + 12 files changed, 181 insertions(+), 82 deletions(-) delete mode 100644 markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java create mode 100644 markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java create mode 100644 sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java diff --git a/CHANGELOG.md b/CHANGELOG.md index ac975d4b..7cc0a9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +# 4.1.0-SNAPSHOT +* Add `Markwon.TextSetter` interface to be able to use PrecomputedText/PrecomputedTextCompat +* Add `PrecomputedTextSetterCompat` and `compileOnly` dependency on `androidx.core:core` +(clients must have this dependency in the classpath) + # 4.0.2 * Fix `JLatexMathPlugin` formula placeholder (cannot have line breaks) ([#149]) * Fix `JLatexMathPlugin` to update resulting formula bounds when `fitCanvas=true` and diff --git a/build.gradle b/build.gradle index 8c49f472..57cc1bab 100644 --- a/build.gradle +++ b/build.gradle @@ -59,6 +59,7 @@ ext { deps = [ 'x-annotations' : 'androidx.annotation:annotation:1.1.0', 'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0', + 'x-core' : 'androidx.core:core:1.0.2', 'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion", 'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion", 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", diff --git a/markwon-core/build.gradle b/markwon-core/build.gradle index 946d760e..8d771378 100644 --- a/markwon-core/build.gradle +++ b/markwon-core/build.gradle @@ -18,6 +18,10 @@ dependencies { deps.with { api it['x-annotations'] api it['commonmark'] + + // @since 4.1.0-SNAPSHOT to allow PrecomputedTextSetterCompat + // note that this dependency must be added on a client side explicitly + compileOnly it['x-core'] } deps['test'].with { 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 431790c5..b02767c2 100644 --- a/markwon-core/src/main/java/io/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/io/noties/markwon/Markwon.java @@ -123,7 +123,7 @@ public static Builder builderNoCore(@NonNull Context context) { * Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText * functionality * - * @see PrecomputedTextSetter + * @see PrecomputedTextSetterCompat * @since 4.1.0-SNAPSHOT */ public interface TextSetter { diff --git a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java deleted file mode 100644 index 75452e21..00000000 --- a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetter.java +++ /dev/null @@ -1,80 +0,0 @@ -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/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java new file mode 100644 index 00000000..c073a2d5 --- /dev/null +++ b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java @@ -0,0 +1,120 @@ +package io.noties.markwon; + +import android.os.Build; +import android.text.Spanned; +import android.util.Log; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.text.PrecomputedTextCompat; + +import java.lang.ref.WeakReference; +import java.util.concurrent.Executor; + +/** + * Please note this class requires `androidx.core:core` artifact being explicitly added to your dependencies. + * Please do not use with `markwon-recycler` as it will lead to bad item rendering (due to async nature) + * + * @see io.noties.markwon.Markwon.TextSetter + * @since 4.1.0-SNAPSHOT + */ +public class PrecomputedTextSetterCompat implements Markwon.TextSetter { + + /** + * @param executor for background execution of text pre-computation + */ + @NonNull + public static PrecomputedTextSetterCompat create(@NonNull Executor executor) { + return new PrecomputedTextSetterCompat(executor); + } + + private final Executor executor; + + @SuppressWarnings("WeakerAccess") + PrecomputedTextSetterCompat(@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) { + + // insert version check and do not execute on a device < 21 + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + // it's still no-op, so there is no need to start background execution + applyText(textView, markdown, bufferType, onComplete); + return; + } + + final WeakReference reference = new WeakReference<>(textView); + executor.execute(new Runnable() { + @Override + public void run() { + try { + final PrecomputedTextCompat precomputedTextCompat = precomputedText(reference.get(), markdown); + if (precomputedTextCompat != null) { + applyText(reference.get(), precomputedTextCompat, bufferType, onComplete); + } + } catch (Throwable t) { + Log.e("PrecomputdTxtSetterCmpt", "Exception during pre-computing text", t); + // apply initial markdown + applyText(reference.get(), markdown, bufferType, onComplete); + } + } + }); + } + + @Nullable + private static PrecomputedTextCompat precomputedText(@Nullable TextView textView, @NonNull Spanned spanned) { + + if (textView == null) { + return null; + } + + final PrecomputedTextCompat.Params params; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + // use native parameters on P + params = new PrecomputedTextCompat.Params(textView.getTextMetricsParams()); + } else { + + final PrecomputedTextCompat.Params.Builder builder = + new PrecomputedTextCompat.Params.Builder(textView.getPaint()); + + // please note that text-direction initialization is omitted + // by default it will be determined by the first locale-specific character + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // another miss on API surface, this can easily be done by the compat class itself + builder + .setBreakStrategy(textView.getBreakStrategy()) + .setHyphenationFrequency(textView.getHyphenationFrequency()); + } + + params = builder.build(); + } + + return PrecomputedTextCompat.create(spanned, params); + } + + private static void applyText( + @Nullable final TextView textView, + @NonNull final Spanned text, + @NonNull final TextView.BufferType bufferType, + @NonNull final Runnable onComplete) { + Log.e("TXT", String.format("thread: %s, attached: %s", Thread.currentThread(), textView.isAttachedToWindow())); + if (textView != null) { + textView.post(new Runnable() { + @Override + public void run() { + textView.setText(text, bufferType); + onComplete.run(); + } + }); + } + } +} diff --git a/sample/build.gradle b/sample/build.gradle index c3ff21dd..51a912f4 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -49,6 +49,7 @@ dependencies { deps.with { implementation it['x-recycler-view'] + implementation it['x-core'] // for precomputedTextCompat implementation it['okhttp'] implementation it['prism4j'] implementation it['debug'] diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 5434d3f5..6492812f 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ + diff --git a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java index 2614b239..a13427be 100644 --- a/sample/src/main/java/io/noties/markwon/sample/MainActivity.java +++ b/sample/src/main/java/io/noties/markwon/sample/MainActivity.java @@ -24,6 +24,7 @@ import io.noties.markwon.sample.customextension2.CustomExtensionActivity2; import io.noties.markwon.sample.html.HtmlActivity; import io.noties.markwon.sample.latex.LatexActivity; +import io.noties.markwon.sample.precomputed.PrecomputedActivity; import io.noties.markwon.sample.recycler.RecyclerActivity; import io.noties.markwon.sample.simpleext.SimpleExtActivity; @@ -112,6 +113,10 @@ static Intent sampleItemIntent(@NonNull Context context, @NonNull Sample item) { activity = CustomExtensionActivity2.class; break; + case PRECOMPUTED_TEXT: + activity = PrecomputedActivity.class; + break; + default: throw new IllegalStateException("No Activity is associated with sample-item: " + item); } diff --git a/sample/src/main/java/io/noties/markwon/sample/Sample.java b/sample/src/main/java/io/noties/markwon/sample/Sample.java index e892a5ce..3102a1f2 100644 --- a/sample/src/main/java/io/noties/markwon/sample/Sample.java +++ b/sample/src/main/java/io/noties/markwon/sample/Sample.java @@ -19,7 +19,9 @@ public enum Sample { SIMPLE_EXT(R.string.sample_simple_ext), - CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2); + CUSTOM_EXTENSION_2(R.string.sample_custom_extension_2), + + PRECOMPUTED_TEXT(R.string.sample_precomputed_text); private final int textResId; diff --git a/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java b/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java new file mode 100644 index 00000000..2628bffd --- /dev/null +++ b/sample/src/main/java/io/noties/markwon/sample/precomputed/PrecomputedActivity.java @@ -0,0 +1,38 @@ +package io.noties.markwon.sample.precomputed; + +import android.app.Activity; +import android.os.Bundle; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import java.util.concurrent.Executors; + +import io.noties.markwon.Markwon; +import io.noties.markwon.PrecomputedTextSetterCompat; +import io.noties.markwon.sample.R; + +public class PrecomputedActivity extends Activity { + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_text_view); + + final Markwon markwon = Markwon.builder(this) + // please note that precomputedTextCompat is no-op on devices lower than L (21) + .textSetter(PrecomputedTextSetterCompat.create(Executors.newCachedThreadPool())) + .build(); + + final TextView textView = findViewById(R.id.text_view); + final String markdown = "# Hello!\n\n" + + "This _displays_ how to implement and use `PrecomputedTextCompat` with the **Markwon**\n\n" + + "> consider using PrecomputedText only if your markdown content is large enough\n> \n" + + "> **please note** that it works badly with `markwon-recycler` due to asynchronous nature"; + + // please note that _sometimes_ (if done without `post` here) further `textView.post` + // (that is used in PrecomputedTextSetterCompat to deliver result to main-thread) won't be called + // making the result of pre-computation absent and text-view clear (no text) + textView.post(() -> markwon.setMarkdown(textView, markdown)); + } +} diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml index c1206e4b..b2fc98d2 100644 --- a/sample/src/main/res/values/strings-samples.xml +++ b/sample/src/main/res/values/strings-samples.xml @@ -23,4 +23,6 @@ # \# Custom extension 2\n\nAutomatically convert `#1` and `@user` to Github links + # \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat + \ No newline at end of file From 620da87694db60ef1aaa3407db30a89af48f1806 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sat, 27 Jul 2019 15:40:38 +0300 Subject: [PATCH 3/6] Removed loggin statement in precomputed-text-setter-compat --- .../main/java/io/noties/markwon/PrecomputedTextSetterCompat.java | 1 - 1 file changed, 1 deletion(-) diff --git a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java index c073a2d5..e2188199 100644 --- a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java +++ b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java @@ -106,7 +106,6 @@ private static void applyText( @NonNull final Spanned text, @NonNull final TextView.BufferType bufferType, @NonNull final Runnable onComplete) { - Log.e("TXT", String.format("thread: %s, attached: %s", Thread.currentThread(), textView.isAttachedToWindow())); if (textView != null) { textView.post(new Runnable() { @Override From 2a43797023e3769f1e7f4058cab16e52e79c0ea1 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 1 Aug 2019 12:27:38 +0300 Subject: [PATCH 4/6] TablePlugin defer table-row invalidation --- CHANGELOG.md | 3 + .../io/noties/markwon/app/MainActivity.java | 5 ++ app/src/main/res/layout/activity_main.xml | 5 +- .../main/java/io/noties/markwon/Markwon.java | 15 +++++ .../java/io/noties/markwon/MarkwonImpl.java | 19 ++++++ .../io/noties/markwon/MarkwonImplTest.java | 62 +++++++++++++++++++ .../ext/tables/TableRowsScheduler.java | 15 ++++- 7 files changed, 122 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7cc0a9ce..ab7a9ce0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ * Add `Markwon.TextSetter` interface to be able to use PrecomputedText/PrecomputedTextCompat * Add `PrecomputedTextSetterCompat` and `compileOnly` dependency on `androidx.core:core` (clients must have this dependency in the classpath) +* Add `requirePlugin(Class)` and `getPlugins` for `Markwon` instance +* TablePlugin -> defer table invalidation (via `View.post`), so only one invalidation +happens with each draw-call # 4.0.2 * Fix `JLatexMathPlugin` formula placeholder (cannot have line breaks) ([#149]) diff --git a/app/src/main/java/io/noties/markwon/app/MainActivity.java b/app/src/main/java/io/noties/markwon/app/MainActivity.java index c72542b3..c0cb1390 100644 --- a/app/src/main/java/io/noties/markwon/app/MainActivity.java +++ b/app/src/main/java/io/noties/markwon/app/MainActivity.java @@ -5,6 +5,7 @@ import android.net.Uri; import android.os.Bundle; import android.text.Spanned; +import android.text.method.LinkMovementMethod; import android.view.View; import android.widget.TextView; @@ -15,6 +16,7 @@ import io.noties.debug.Debug; import io.noties.markwon.Markwon; +import io.noties.markwon.utils.NoCopySpannableFactory; public class MainActivity extends Activity { @@ -60,6 +62,9 @@ public void onClick(View v) { appBarRenderer.render(appBarState()); + textView.setMovementMethod(LinkMovementMethod.getInstance()); + textView.setSpannableFactory(NoCopySpannableFactory.getInstance()); + markdownLoader.load(uri(), new MarkdownLoader.OnMarkdownTextLoaded() { @Override public void apply(final String text) { diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index da49a67b..46a28b85 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -18,9 +18,12 @@ android:id="@+id/text" android:layout_width="match_parent" android:layout_height="wrap_content" + android:breakStrategy="simple" + android:hyphenationFrequency="none" android:lineSpacingExtra="2dip" android:textSize="16sp" - tools:text="yo\nman" /> + tools:text="yo\nman" + tools:ignore="UnusedAttribute" /> 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 b02767c2..fa54a3b3 100644 --- a/markwon-core/src/main/java/io/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/io/noties/markwon/Markwon.java @@ -9,6 +9,8 @@ import org.commonmark.node.Node; +import java.util.List; + import io.noties.markwon.core.CorePlugin; /** @@ -119,6 +121,19 @@ public static Builder builderNoCore(@NonNull Context context) { @Nullable public abstract

P getPlugin(@NonNull Class

type); + /** + * @since 4.1.0-SNAPSHOT + */ + @NonNull + public abstract

P requirePlugin(@NonNull Class

type); + + /** + * @return a list of registered {@link MarkwonPlugin} + * @since 4.1.0-SNAPSHOT + */ + @NonNull + public abstract List getPlugins(); + /** * Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText * functionality 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 3a8d6d1f..3d9c1dcd 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java @@ -9,7 +9,9 @@ import org.commonmark.node.Node; import org.commonmark.parser.Parser; +import java.util.Collections; import java.util.List; +import java.util.Locale; /** * @since 3.0.0 @@ -129,4 +131,21 @@ public

P getPlugin(@NonNull Class

type) { //noinspection unchecked return (P) out; } + + @NonNull + @Override + public

P requirePlugin(@NonNull Class

type) { + final P plugin = getPlugin(type); + if (plugin == null) { + throw new IllegalStateException(String.format(Locale.US, "Requested plugin `%s` is not " + + "registered with this Markwon instance", type.getName())); + } + return plugin; + } + + @NonNull + @Override + public List getPlugins() { + return Collections.unmodifiableList(plugins); + } } 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 739470b7..8e24e578 100644 --- a/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java +++ b/markwon-core/src/test/java/io/noties/markwon/MarkwonImplTest.java @@ -14,6 +14,7 @@ import org.robolectric.RobolectricTestRunner; import org.robolectric.annotation.Config; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -23,6 +24,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; @@ -300,4 +302,64 @@ public void text_setter() { assertEquals(TextView.BufferType.EDITABLE, bufferTypeArgumentCaptor.getValue()); assertNotNull(runnableArgumentCaptor.getValue()); } + + @Test + public void require_plugin_throws() { + // if plugin is `required`, but it's not added -> an exception is thrown + + final class NotPresent extends AbstractMarkwonPlugin { + } + + final List plugins = + Arrays.asList(mock(MarkwonPlugin.class), mock(MarkwonPlugin.class)); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + null, + mock(Parser.class), + mock(MarkwonVisitor.class), plugins); + + // should be returned + assertNotNull(impl.requirePlugin(MarkwonPlugin.class)); + + try { + impl.requirePlugin(NotPresent.class); + fail(); + } catch (Throwable t) { + assertTrue(t.getMessage(), t.getMessage().contains(NotPresent.class.getName())); + } + } + + @Test + public void plugins_unmodifiable() { + // returned plugins list must not be modifiable + + // modifiable list (created from Arrays.asList -> which returns non) + final List plugins = new ArrayList<>( + Arrays.asList(mock(MarkwonPlugin.class), mock(MarkwonPlugin.class))); + + // validate that list is modifiable + plugins.add(mock(MarkwonPlugin.class)); + assertEquals(3, plugins.size()); + + final MarkwonImpl impl = new MarkwonImpl( + TextView.BufferType.SPANNABLE, + null, + mock(Parser.class), + mock(MarkwonVisitor.class), + plugins); + + final List list = impl.getPlugins(); + + // instance check (different list) + //noinspection SimplifiableJUnitAssertion + assertTrue(plugins != list); + + try { + list.add(null); + fail(); + } catch (UnsupportedOperationException e) { + assertTrue(e.getMessage(), true); + } + } } \ No newline at end of file diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java index e3086c20..d2be1e29 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java @@ -34,9 +34,22 @@ public void onViewDetachedFromWindow(View v) { } final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() { + + // @since 4.1.0-SNAPSHOT + // let's stack-up invalidation calls (so invalidation happens, + // but not with each table-row-span draw call) + final Runnable runnable = new Runnable() { + @Override + public void run() { + view.setText(view.getText()); + } + }; + @Override public void invalidate() { - view.setText(view.getText()); + // @since 4.1.0-SNAPSHOT post invalidation (combine multiple calls) + view.removeCallbacks(runnable); + view.post(runnable); } }; From b6fa66914f55390cb4003ca8e6e294a373d73959 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Thu, 1 Aug 2019 12:44:26 +0300 Subject: [PATCH 5/6] AsyncDrawable defer invalidation --- CHANGELOG.md | 1 + .../markwon/image/AsyncDrawableScheduler.java | 45 +++++++++++++++++-- 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab7a9ce0..df2cd26e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ * Add `requirePlugin(Class)` and `getPlugins` for `Markwon` instance * TablePlugin -> defer table invalidation (via `View.post`), so only one invalidation happens with each draw-call +* AsyncDrawableSpan -> defer invalidation # 4.0.2 * Fix `JLatexMathPlugin` formula placeholder (cannot have line breaks) ([#149]) diff --git a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java index a7d664e3..ad33ca06 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java @@ -57,11 +57,14 @@ public void onViewDetachedFromWindow(View v) { textView.setTag(R.id.markwon_drawables_scheduler, listener); } + // @since 4.1.0-SNAPSHOT + final DrawableCallbackImpl.Invalidator invalidator = new TextViewInvalidator(textView); + AsyncDrawable drawable; for (AsyncDrawableSpan span : spans) { drawable = span.getDrawable(); - drawable.setCallback2(new DrawableCallbackImpl(textView, drawable.getBounds())); + drawable.setCallback2(new DrawableCallbackImpl(textView, invalidator, drawable.getBounds())); } } } @@ -109,11 +112,23 @@ private AsyncDrawableScheduler() { private static class DrawableCallbackImpl implements Drawable.Callback { + // @since 4.1.0-SNAPSHOT + // interface to be used when bounds change and view must be invalidated + interface Invalidator { + void invalidate(); + } + private final TextView view; + private final Invalidator invalidator; // @since 4.1.0-SNAPSHOT + private Rect previousBounds; - DrawableCallbackImpl(TextView view, Rect initialBounds) { + DrawableCallbackImpl( + @NonNull TextView view, + @NonNull Invalidator invalidator, + Rect initialBounds) { this.view = view; + this.invalidator = invalidator; this.previousBounds = new Rect(initialBounds); } @@ -136,8 +151,10 @@ public void run() { // but if the size has changed, then we need to update the whole layout... if (!previousBounds.equals(rect)) { - // the only method that seems to work when bounds have changed - view.setText(view.getText()); + // @since 4.1.0-SNAPSHOT + // invalidation moved to upper level (so invalidation can be deferred, + // and multiple calls combined) + invalidator.invalidate(); previousBounds = new Rect(rect); } else { @@ -156,4 +173,24 @@ public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) { view.removeCallbacks(what); } } + + private static class TextViewInvalidator implements DrawableCallbackImpl.Invalidator, Runnable { + + private final TextView textView; + + TextViewInvalidator(@NonNull TextView textView) { + this.textView = textView; + } + + @Override + public void invalidate() { + textView.removeCallbacks(this); + textView.post(this); + } + + @Override + public void run() { + textView.setText(textView.getText()); + } + } } From a2d35a1553949509d9d3f4b73d9a6aa9bbd5c1d4 Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Tue, 6 Aug 2019 18:13:13 +0300 Subject: [PATCH 6/6] Prepare 4.1.0 release --- CHANGELOG.md | 2 +- gradle.properties | 2 +- markwon-core/build.gradle | 2 +- markwon-core/src/main/java/io/noties/markwon/Markwon.java | 8 ++++---- .../src/main/java/io/noties/markwon/MarkwonImpl.java | 4 ++-- .../io/noties/markwon/PrecomputedTextSetterCompat.java | 2 +- .../io/noties/markwon/image/AsyncDrawableScheduler.java | 8 ++++---- .../io/noties/markwon/ext/tables/TableRowsScheduler.java | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df2cd26e..60e2c357 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -# 4.1.0-SNAPSHOT +# 4.1.0 * Add `Markwon.TextSetter` interface to be able to use PrecomputedText/PrecomputedTextCompat * Add `PrecomputedTextSetterCompat` and `compileOnly` dependency on `androidx.core:core` (clients must have this dependency in the classpath) diff --git a/gradle.properties b/gradle.properties index e9c33f08..b76c9e11 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.1.0-SNAPSHOT +VERSION_NAME=4.1.0 GROUP=io.noties.markwon POM_DESCRIPTION=Markwon markdown for Android diff --git a/markwon-core/build.gradle b/markwon-core/build.gradle index 8d771378..6ff67d77 100644 --- a/markwon-core/build.gradle +++ b/markwon-core/build.gradle @@ -19,7 +19,7 @@ dependencies { api it['x-annotations'] api it['commonmark'] - // @since 4.1.0-SNAPSHOT to allow PrecomputedTextSetterCompat + // @since 4.1.0 to allow PrecomputedTextSetterCompat // note that this dependency must be added on a client side explicitly compileOnly it['x-core'] } 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 fa54a3b3..9277688b 100644 --- a/markwon-core/src/main/java/io/noties/markwon/Markwon.java +++ b/markwon-core/src/main/java/io/noties/markwon/Markwon.java @@ -122,14 +122,14 @@ public static Builder builderNoCore(@NonNull Context context) { public abstract

P getPlugin(@NonNull Class

type); /** - * @since 4.1.0-SNAPSHOT + * @since 4.1.0 */ @NonNull public abstract

P requirePlugin(@NonNull Class

type); /** * @return a list of registered {@link MarkwonPlugin} - * @since 4.1.0-SNAPSHOT + * @since 4.1.0 */ @NonNull public abstract List getPlugins(); @@ -139,7 +139,7 @@ public static Builder builderNoCore(@NonNull Context context) { * functionality * * @see PrecomputedTextSetterCompat - * @since 4.1.0-SNAPSHOT + * @since 4.1.0 */ public interface TextSetter { /** @@ -178,7 +178,7 @@ public interface Builder { /** * @param textSetter {@link TextSetter} to apply text to a TextView - * @since 4.1.0-SNAPSHOT + * @since 4.1.0 */ @NonNull Builder textSetter(@NonNull TextSetter textSetter); 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 3d9c1dcd..9080527f 100644 --- a/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java +++ b/markwon-core/src/main/java/io/noties/markwon/MarkwonImpl.java @@ -23,7 +23,7 @@ class MarkwonImpl extends Markwon { private final MarkwonVisitor visitor; private final List plugins; - // @since 4.1.0-SNAPSHOT + // @since 4.1.0 @Nullable private final TextSetter textSetter; @@ -92,7 +92,7 @@ public void setParsedMarkdown(@NonNull final TextView textView, @NonNull Spanned plugin.beforeSetText(textView, markdown); } - // @since 4.1.0-SNAPSHOT + // @since 4.1.0 if (textSetter != null) { textSetter.setText(textView, markdown, bufferType, new Runnable() { @Override diff --git a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java index e2188199..721c4e6e 100644 --- a/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java +++ b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java @@ -17,7 +17,7 @@ * Please do not use with `markwon-recycler` as it will lead to bad item rendering (due to async nature) * * @see io.noties.markwon.Markwon.TextSetter - * @since 4.1.0-SNAPSHOT + * @since 4.1.0 */ public class PrecomputedTextSetterCompat implements Markwon.TextSetter { diff --git a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java index ad33ca06..3c62c961 100644 --- a/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java +++ b/markwon-core/src/main/java/io/noties/markwon/image/AsyncDrawableScheduler.java @@ -57,7 +57,7 @@ public void onViewDetachedFromWindow(View v) { textView.setTag(R.id.markwon_drawables_scheduler, listener); } - // @since 4.1.0-SNAPSHOT + // @since 4.1.0 final DrawableCallbackImpl.Invalidator invalidator = new TextViewInvalidator(textView); AsyncDrawable drawable; @@ -112,14 +112,14 @@ private AsyncDrawableScheduler() { private static class DrawableCallbackImpl implements Drawable.Callback { - // @since 4.1.0-SNAPSHOT + // @since 4.1.0 // interface to be used when bounds change and view must be invalidated interface Invalidator { void invalidate(); } private final TextView view; - private final Invalidator invalidator; // @since 4.1.0-SNAPSHOT + private final Invalidator invalidator; // @since 4.1.0 private Rect previousBounds; @@ -151,7 +151,7 @@ public void run() { // but if the size has changed, then we need to update the whole layout... if (!previousBounds.equals(rect)) { - // @since 4.1.0-SNAPSHOT + // @since 4.1.0 // invalidation moved to upper level (so invalidation can be deferred, // and multiple calls combined) invalidator.invalidate(); diff --git a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java index d2be1e29..68432c57 100644 --- a/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java +++ b/markwon-ext-tables/src/main/java/io/noties/markwon/ext/tables/TableRowsScheduler.java @@ -35,7 +35,7 @@ public void onViewDetachedFromWindow(View v) { final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() { - // @since 4.1.0-SNAPSHOT + // @since 4.1.0 // let's stack-up invalidation calls (so invalidation happens, // but not with each table-row-span draw call) final Runnable runnable = new Runnable() { @@ -47,7 +47,7 @@ public void run() { @Override public void invalidate() { - // @since 4.1.0-SNAPSHOT post invalidation (combine multiple calls) + // @since 4.1.0 post invalidation (combine multiple calls) view.removeCallbacks(runnable); view.post(runnable); }