diff --git a/CHANGELOG.md b/CHANGELOG.md
index ac975d4b..60e2c357 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,14 @@
# Changelog
+# 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)
+* 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])
* Fix `JLatexMathPlugin` to update resulting formula bounds when `fitCanvas=true` and
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.content.Intent;
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 javax.inject.Inject;
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 class MainActivity extends Activity {
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/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/gradle.properties b/gradle.properties
index 5797d1d0..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.0.2
+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 946d760e..6ff67d77 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 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 ac702599..9277688b 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 androidx.annotation.Nullable;
import org.commonmark.node.Node;
+import java.util.List;
+
import io.noties.markwon.core.CorePlugin;
/**
@@ -119,6 +121,42 @@ public abstract class Markwon {
@Nullable
public abstract
P getPlugin(@NonNull Class
type);
+ /**
+ * @since 4.1.0
+ */
+ @NonNull
+ public abstract
P requirePlugin(@NonNull Class
type);
+
+ /**
+ * @return a list of registered {@link MarkwonPlugin}
+ * @since 4.1.0
+ */
+ @NonNull
+ public abstract List extends MarkwonPlugin> getPlugins();
+
+ /**
+ * Interface to set text on a TextView. Primary goal is to give a way to use PrecomputedText
+ * functionality
+ *
+ * @see PrecomputedTextSetterCompat
+ * @since 4.1.0
+ */
+ 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 +176,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
+ */
+ @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..9080527f 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 androidx.annotation.Nullable;
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
@@ -21,12 +23,18 @@ class MarkwonImpl extends Markwon {
private final MarkwonVisitor visitor;
private final List plugins;
+ // @since 4.1.0
+ @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 +86,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
+ 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);
+ }
}
}
@@ -108,4 +131,21 @@ class MarkwonImpl extends Markwon {
//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 extends MarkwonPlugin> getPlugins() {
+ return Collections.unmodifiableList(plugins);
+ }
}
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..721c4e6e
--- /dev/null
+++ b/markwon-core/src/main/java/io/noties/markwon/PrecomputedTextSetterCompat.java
@@ -0,0 +1,119 @@
+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
+ */
+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) {
+ if (textView != null) {
+ textView.post(new Runnable() {
+ @Override
+ public void run() {
+ textView.setText(text, bufferType);
+ onComplete.run();
+ }
+ });
+ }
+ }
+}
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..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,11 +57,14 @@ public abstract class AsyncDrawableScheduler {
textView.setTag(R.id.markwon_drawables_scheduler, listener);
}
+ // @since 4.1.0
+ 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 @@ public abstract class AsyncDrawableScheduler {
private static class DrawableCallbackImpl implements Drawable.Callback {
+ // @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
+
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 abstract class AsyncDrawableScheduler {
// 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
+ // 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 abstract class AsyncDrawableScheduler {
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());
+ }
+ }
}
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..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.mockito.stubbing.Answer;
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;
@@ -21,7 +22,9 @@ 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.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
@@ -42,6 +45,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 +68,7 @@ public class MarkwonImplTest {
final MarkwonImpl impl = new MarkwonImpl(
TextView.BufferType.SPANNABLE,
+ null,
parser,
mock(MarkwonVisitor.class),
Arrays.asList(first, second));
@@ -89,6 +94,7 @@ public class MarkwonImplTest {
final MarkwonImpl impl = new MarkwonImpl(
TextView.BufferType.SPANNABLE,
+ null,
mock(Parser.class),
visitor,
Collections.singletonList(plugin));
@@ -130,6 +136,7 @@ public class MarkwonImplTest {
final MarkwonImpl impl = new MarkwonImpl(
TextView.BufferType.SPANNABLE,
+ null,
mock(Parser.class),
visitor,
Collections.emptyList());
@@ -160,6 +167,7 @@ public class MarkwonImplTest {
final MarkwonImpl impl = new MarkwonImpl(
TextView.BufferType.SPANNABLE,
+ null,
mock(Parser.class),
visitor,
Collections.singletonList(plugin));
@@ -195,6 +203,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 +250,7 @@ public class MarkwonImplTest {
final MarkwonImpl impl = new MarkwonImpl(
TextView.BufferType.SPANNABLE,
+ null,
mock(Parser.class),
mock(MarkwonVisitor.class),
plugins);
@@ -253,4 +263,103 @@ 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());
+ }
+
+ @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 extends MarkwonPlugin> 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..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
@@ -34,9 +34,22 @@ abstract class TableRowsScheduler {
}
final TableRowSpan.Invalidator invalidator = new TableRowSpan.Invalidator() {
+
+ // @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() {
+ @Override
+ public void run() {
+ view.setText(view.getText());
+ }
+ };
+
@Override
public void invalidate() {
- view.setText(view.getText());
+ // @since 4.1.0 post invalidation (combine multiple calls)
+ view.removeCallbacks(runnable);
+ view.post(runnable);
}
};
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.customextension.CustomExtensionActivity;
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 @@ public class MainActivity extends Activity {
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