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 abstract class Markwon { * 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.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