Add TextSetter interface

This commit is contained in:
Dimitry Ivanov 2019-07-26 15:34:48 +03:00
parent 822f16510e
commit 7e12552060
6 changed files with 193 additions and 5 deletions

View File

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

View File

@ -119,6 +119,29 @@ public abstract class Markwon {
@Nullable
public abstract <P extends MarkwonPlugin> P getPlugin(@NonNull Class<P> 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}.
* <p>
@ -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);

View File

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

View File

@ -21,12 +21,18 @@ class MarkwonImpl extends Markwon {
private final MarkwonVisitor visitor;
private final List<MarkwonPlugin> 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<MarkwonPlugin> plugins) {
this.bufferType = bufferType;
this.textSetter = textSetter;
this.parser = parser;
this.visitor = visitor;
this.plugins = plugins;
@ -78,18 +84,33 @@ 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);
}
// @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);
}
}
}
@Override
public boolean hasPlugin(@NonNull Class<? extends MarkwonPlugin> type) {

View File

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

View File

@ -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.<MarkwonPlugin>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<TextView> textViewArgumentCaptor =
ArgumentCaptor.forClass(TextView.class);
final ArgumentCaptor<Spanned> spannedArgumentCaptor =
ArgumentCaptor.forClass(Spanned.class);
final ArgumentCaptor<TextView.BufferType> bufferTypeArgumentCaptor =
ArgumentCaptor.forClass(TextView.BufferType.class);
final ArgumentCaptor<Runnable> 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());
}
}