Add TextSetter interface
This commit is contained in:
		
							parent
							
								
									822f16510e
								
							
						
					
					
						commit
						7e12552060
					
				| @ -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 | ||||
|  | ||||
| @ -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); | ||||
| 
 | ||||
|  | ||||
| @ -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) | ||||
|  | ||||
| @ -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,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); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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(); | ||||
|                 } | ||||
|             }); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -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()); | ||||
|     } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Dimitry Ivanov
						Dimitry Ivanov