From 6f8c1dfaeed1c83c21782216fe3a1eed89cc095b Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Wed, 2 Jan 2019 22:49:54 +0300 Subject: [PATCH] Working with sample application --- .../noties/markwon/recycler/SimpleEntry.java | 14 +- .../ru/noties/markwon/MarkwonBuilderImpl.java | 2 + .../noties/markwon/MovementMethodPlugin.java | 41 ++++ .../ru/noties/markwon/image/ImagesPlugin.java | 5 + .../extension/recycler/TableEntryView.java | 52 +++++ .../main/res/layout/adapter_table_block.xml | 28 +-- .../src/main/res/values/attrs.xml | 2 + sample/build.gradle | 9 + sample/src/main/AndroidManifest.xml | 4 + .../noties/markwon/sample/MainActivity.java | 10 + .../ru/noties/markwon/sample/SampleItem.java | 4 + .../basicplugins/BasicPluginsActivity.java | 204 ++++++++++++++++ .../markwon/sample/core/CoreActivity.java | 86 ------- .../sample/recycler/RecyclerActivity.java | 162 +++++++++++++ .../markwon/sample/recycler/TableEntry.java | 67 ++++++ .../sample/recycler/TableEntryView.java | 218 ++++++++++++++++++ sample/src/main/recycler/assets/README.md | 1 + .../recycler/res/layout/activity_recycler.xml | 5 + .../res/layout/adapter_default_entry.xml | 13 ++ .../res/layout/adapter_fenced_code_block.xml | 24 ++ .../res/layout/adapter_table_block.xml | 22 ++ .../res/layout/view_table_entry_cell.xml | 11 + .../res/layout/view_table_entry_row.xml | 5 + sample/src/main/recycler/res/values/attrs.xml | 9 + .../src/main/res/values/strings-samples.xml | 12 +- 25 files changed, 904 insertions(+), 106 deletions(-) create mode 100644 markwon/src/main/java/ru/noties/markwon/MovementMethodPlugin.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java create mode 100644 sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java create mode 120000 sample/src/main/recycler/assets/README.md create mode 100644 sample/src/main/recycler/res/layout/activity_recycler.xml create mode 100644 sample/src/main/recycler/res/layout/adapter_default_entry.xml create mode 100644 sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml create mode 100644 sample/src/main/recycler/res/layout/adapter_table_block.xml create mode 100644 sample/src/main/recycler/res/layout/view_table_entry_cell.xml create mode 100644 sample/src/main/recycler/res/layout/view_table_entry_row.xml create mode 100644 sample/src/main/recycler/res/values/attrs.xml diff --git a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java index d04a7477..a966ec94 100644 --- a/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java +++ b/markwon-recycler/src/main/java/ru/noties/markwon/recycler/SimpleEntry.java @@ -17,11 +17,15 @@ import java.util.Map; import ru.noties.markwon.Markwon; +/** + * @since 3.0.0 + */ +@SuppressWarnings("WeakerAccess") public class SimpleEntry implements MarkwonAdapter.Entry { - private static final NoCopySpannableFactory FACTORY = new NoCopySpannableFactory(); + public static final Spannable.Factory NO_COPY_SPANNABLE_FACTORY = new NoCopySpannableFactory(); - // small cache, maybe add pre-compute of text, also spannableFactory (so no copying of spans) + // small cache for already rendered nodes private final Map cache = new HashMap<>(); private final int layoutResId; @@ -60,15 +64,15 @@ public class SimpleEntry implements MarkwonAdapter.Entry out = new ArrayList<>(plugins.size() + 1); // add default instance of CorePlugin diff --git a/markwon/src/main/java/ru/noties/markwon/MovementMethodPlugin.java b/markwon/src/main/java/ru/noties/markwon/MovementMethodPlugin.java new file mode 100644 index 00000000..332015e5 --- /dev/null +++ b/markwon/src/main/java/ru/noties/markwon/MovementMethodPlugin.java @@ -0,0 +1,41 @@ +package ru.noties.markwon; + +import android.support.annotation.NonNull; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.method.MovementMethod; +import android.widget.TextView; + +/** + * @since 3.0.0 + */ +public class MovementMethodPlugin extends AbstractMarkwonPlugin { + + /** + * Creates plugin that will ensure that there is movement method registered on a TextView. + * Uses Android system LinkMovementMethod as default + * + * @see #create(MovementMethod) + */ + @NonNull + public static MovementMethodPlugin create() { + return create(LinkMovementMethod.getInstance()); + } + + @NonNull + public static MovementMethodPlugin create(@NonNull MovementMethod movementMethod) { + return new MovementMethodPlugin(movementMethod); + } + + private final MovementMethod movementMethod; + + @SuppressWarnings("WeakerAccess") + MovementMethodPlugin(@NonNull MovementMethod movementMethod) { + this.movementMethod = movementMethod; + } + + @Override + public void beforeSetText(@NonNull TextView textView, @NonNull Spanned markdown) { + textView.setMovementMethod(movementMethod); + } +} diff --git a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java index d32ee10c..f3b96305 100644 --- a/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java +++ b/markwon/src/main/java/ru/noties/markwon/image/ImagesPlugin.java @@ -31,6 +31,11 @@ public class ImagesPlugin extends AbstractMarkwonPlugin { return new ImagesPlugin(context, false); } + /** + * Special scheme that is used {@code file:///android_asset/} + * @param context + * @return + */ @NonNull public static ImagesPlugin createWithAssets(@NonNull Context context) { return new ImagesPlugin(context, true); diff --git a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java index c46b7ba0..0bf133ee 100644 --- a/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java +++ b/sample-custom-extension/src/main/java/ru/noties/markwon/sample/extension/recycler/TableEntryView.java @@ -2,11 +2,16 @@ package ru.noties.markwon.sample.extension.recycler; import android.content.Context; import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.SpannedString; import android.util.AttributeSet; import android.view.LayoutInflater; +import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; import android.widget.TextView; @@ -19,6 +24,10 @@ import ru.noties.markwon.sample.extension.R; public class TableEntryView extends LinearLayout { + // paint and rect to draw borders + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Rect rect = new Rect(); + private LayoutInflater inflater; private int rowEvenBackgroundColor; @@ -43,6 +52,18 @@ public class TableEntryView extends LinearLayout { rowEvenBackgroundColor = array.getColor(R.styleable.TableEntryView_tev_rowEvenBackgroundColor, 0); + + final int stroke = array.getDimensionPixelSize(R.styleable.TableEntryView_tev_borderWidth, 0); + + // half of requested + final float strokeWidth = stroke > 0 + ? stroke / 2.F + : context.getResources().getDisplayMetrics().density / 2.F; + + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(strokeWidth); + paint.setColor(array.getColor(R.styleable.TableEntryView_tev_borderColor, Color.BLACK)); + if (isInEditMode()) { final String data = array.getString(R.styleable.TableEntryView_tev_debugData); if (data != null) { @@ -67,6 +88,8 @@ public class TableEntryView extends LinearLayout { array.recycle(); } } + + setWillNotDraw(false); } public void setTable(@NonNull Table table) { @@ -133,6 +156,35 @@ public class TableEntryView extends LinearLayout { return (TextView) group.getChildAt(index); } + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int rows = getChildCount(); + if (rows == 0) { + return; + } + + // first draw the whole border + rect.set(0, 0, getWidth(), getHeight()); + canvas.drawRect(rect, paint); + + ViewGroup group; + View view; + + int top; + + for (int row = 0; row < rows; row++) { + group = (ViewGroup) getChildAt(row); + top = group.getTop(); + for (int col = 0, cols = group.getChildCount(); col < cols; col++) { + view = group.getChildAt(col); + rect.set(view.getLeft(), top + view.getTop(), view.getRight(), top + view.getBottom()); + canvas.drawRect(rect, paint); + } + } + } + private static int textAlignment(@NonNull Table.Alignment alignment) { final int out; switch (alignment) { diff --git a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml index 4e032c05..287a6b9a 100644 --- a/sample-custom-extension/src/main/res/layout/adapter_table_block.xml +++ b/sample-custom-extension/src/main/res/layout/adapter_table_block.xml @@ -1,21 +1,23 @@ - + + + + + + + + + + + - \ No newline at end of file + \ No newline at end of file diff --git a/sample-custom-extension/src/main/res/values/attrs.xml b/sample-custom-extension/src/main/res/values/attrs.xml index 05f86e93..5ee8b42b 100644 --- a/sample-custom-extension/src/main/res/values/attrs.xml +++ b/sample-custom-extension/src/main/res/values/attrs.xml @@ -3,6 +3,8 @@ + + \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index eb169fa3..ba71cc29 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -29,6 +29,14 @@ android { targetCompatibility JavaVersion.VERSION_1_8 sourceCompatibility JavaVersion.VERSION_1_8 } + + sourceSets { + main { + // let's use different res directory so sample will have _isolated_ resources from others + res.srcDirs += [ './src/main/recycler/res' ] + assets.srcDirs += ['./src/main/recycler/assets'] + } + } } dependencies { @@ -41,6 +49,7 @@ dependencies { implementation project(':markwon-image-gif') implementation project(':markwon-image-svg') implementation project(':markwon-syntax-highlight') + implementation project(':markwon-recycler') deps.with { implementation it['support-recycler-view'] diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index 73b776c3..cbccca0d 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -3,6 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" package="ru.noties.markwon.sample"> + + + + diff --git a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java index d1fe2853..16205066 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/MainActivity.java @@ -17,9 +17,11 @@ import ru.noties.adapt.OnClickViewProcessor; import ru.noties.debug.AndroidLogDebugOutput; import ru.noties.debug.Debug; import ru.noties.markwon.Markwon; +import ru.noties.markwon.sample.basicplugins.BasicPluginsActivity; import ru.noties.markwon.sample.core.CoreActivity; import ru.noties.markwon.sample.customextension.CustomExtensionActivity; import ru.noties.markwon.sample.latex.LatexActivity; +import ru.noties.markwon.sample.recycler.RecyclerActivity; public class MainActivity extends Activity { @@ -79,6 +81,10 @@ public class MainActivity extends Activity { activity = CoreActivity.class; break; + case BASIC_PLUGINS: + activity = BasicPluginsActivity.class; + break; + case LATEX: activity = LatexActivity.class; break; @@ -87,6 +93,10 @@ public class MainActivity extends Activity { activity = CustomExtensionActivity.class; break; + case RECYCLER: + activity = RecyclerActivity.class; + break; + default: throw new IllegalStateException("No Activity is associated with sample-item: " + item); } diff --git a/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java index bf3038ac..e21444ba 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java +++ b/sample/src/main/java/ru/noties/markwon/sample/SampleItem.java @@ -7,10 +7,14 @@ public enum SampleItem { // all usages of markwon without plugins (parse, render, setMarkwon, etc) CORE(R.string.sample_core), + BASIC_PLUGINS(R.string.sample_basic_plugins), + LATEX(R.string.sample_latex), CUSTOM_EXTENSION(R.string.sample_custom_extension), + RECYCLER(R.string.sample_recycler), + ; private final int textResId; diff --git a/sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java b/sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java new file mode 100644 index 00000000..51723d7e --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/basicplugins/BasicPluginsActivity.java @@ -0,0 +1,204 @@ +package ru.noties.markwon.sample.basicplugins; + +import android.app.Activity; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.TextUtils; +import android.text.style.ForegroundColorSpan; +import android.widget.TextView; + +import org.commonmark.node.Heading; +import org.commonmark.node.Paragraph; + +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.MarkwonPlugin; +import ru.noties.markwon.MarkwonSpansFactory; +import ru.noties.markwon.MarkwonVisitor; +import ru.noties.markwon.MovementMethodPlugin; +import ru.noties.markwon.core.MarkwonTheme; +import ru.noties.markwon.image.AsyncDrawableLoader; +import ru.noties.markwon.image.ImageItem; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.image.SchemeHandler; +import ru.noties.markwon.image.network.NetworkSchemeHandler; + +public class BasicPluginsActivity extends Activity { + + private TextView textView; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + textView = new TextView(this); + setContentView(textView); + + step_1(); + + step_2(); + + step_3(); + + step_4(); + + step_5(); + } + + /** + * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care + * of everything else). + *

+ * Please note that when a plugin is registered and it depends on CorePlugin, there is no + * need to explicitly specify it. By default all plugins that extend AbstractMarkwonPlugin do declare + * it\'s dependency on CorePlugin ({@link MarkwonPlugin#priority()}). + *

+ * Order in which plugins are specified to the builder is of little importance as long as each + * plugin clearly states what dependencies it has + */ + private void step_1() { + + final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { + builder.setFactory(Paragraph.class, (configuration, props) -> + new ForegroundColorSpan(Color.GREEN)); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To disable some nodes from rendering another custom plugin can be used + */ + private void step_2() { + + final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { + + // for example to disable rendering of heading: + // try commenting this out to see that otherwise headings will be rendered + builder.on(Heading.class, null); + + // same method can be used to override existing visitor by specifying + // a new NodeVisitor instance + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * To customize core theme plugin can be used again + */ + private void step_3() { + + final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureTheme(@NonNull MarkwonTheme.Builder builder) { + builder + .codeBackgroundColor(Color.BLACK) + .codeTextColor(Color.RED); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * MarkwonConfiguration contains these utilities: + *

    + *
  • SyntaxHighlight
  • + *
  • LinkSpan.Resolver
  • + *
  • UrlProcessor
  • + *
  • ImageSizeResolver
  • + *
+ *

+ * In order to customize them a custom plugin should be used + */ + private void step_4() { + + final String markdown = "[a link without scheme](github.com)"; + + final Markwon markwon = Markwon.builder(this) + // please note that Markwon does not handle MovementMethod, + // so if your markdown has links your should apply MovementMethod manually + // or use MovementMethodPlugin (which uses system LinkMovementMethod by default) + .usePlugin(MovementMethodPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + // for example if specified destination has no scheme info, we will + // _assume_ that it's network request and append HTTPS scheme + builder.urlProcessor(destination -> { + final Uri uri = Uri.parse(destination); + if (TextUtils.isEmpty(uri.getScheme())) { + return "https://" + destination; + } + return destination; + }); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + /** + * Images configuration. Can be used with (or without) ImagesPlugin, which does some basic + * images handling (parsing markdown containing images, obtain an image from network + * file system or assets). Please note that + */ + private void step_5() { + + final String markdown = "![image](myownscheme://en.wikipedia.org/static/images/project-logos/enwiki-2x.png)"; + + final Markwon markwon = Markwon.builder(this) + .usePlugin(ImagesPlugin.create(this)) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureImages(@NonNull AsyncDrawableLoader.Builder builder) { + // we can have a custom SchemeHandler + // here we will just use networkSchemeHandler to redirect call + builder.addSchemeHandler("myownscheme", new SchemeHandler() { + + final NetworkSchemeHandler networkSchemeHandler = NetworkSchemeHandler.create(); + + @Nullable + @Override + public ImageItem handle(@NonNull String raw, @NonNull Uri uri) { + raw = raw.replace("myownscheme", "https"); + return networkSchemeHandler.handle(raw, Uri.parse(raw)); + } + }); + } + }) + .build(); + + markwon.setMarkdown(textView, markdown); + } + + // text lifecycle (after/before) + // rendering lifecycle (before/after) + // renderProps + // process + // priority +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java index 79687390..3f8118c5 100644 --- a/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java +++ b/sample/src/main/java/ru/noties/markwon/sample/core/CoreActivity.java @@ -1,26 +1,16 @@ package ru.noties.markwon.sample.core; import android.app.Activity; -import android.graphics.Color; import android.os.Bundle; -import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Spanned; -import android.text.style.ForegroundColorSpan; import android.widget.TextView; import android.widget.Toast; -import org.commonmark.node.Heading; import org.commonmark.node.Node; -import org.commonmark.node.Paragraph; -import ru.noties.markwon.AbstractMarkwonPlugin; import ru.noties.markwon.Markwon; -import ru.noties.markwon.MarkwonPlugin; -import ru.noties.markwon.MarkwonSpansFactory; -import ru.noties.markwon.MarkwonVisitor; import ru.noties.markwon.core.CorePlugin; -import ru.noties.markwon.core.MarkwonTheme; public class CoreActivity extends Activity { @@ -40,12 +30,6 @@ public class CoreActivity extends Activity { step_3(); step_4(); - - step_5(); - - step_6(); - - step_7(); } /** @@ -135,74 +119,4 @@ public class CoreActivity extends Activity { // apply parsed markdown markwon.setParsedMarkdown(textView, spanned); } - - /** - * In order to apply paragraph spans a custom plugin should be created (CorePlugin will take care - * of everything else). - *

- * Please note that when a plugin is registered and it depends on CorePlugin, there is no - * need to explicitly specify it. By default all plugins that extend AbstractMarkwonPlugin do declare - * it\'s dependency on CorePlugin ({@link MarkwonPlugin#priority()}). - *

- * Order in which plugins are specified to the builder is of little importance as long as each - * plugin clearly states what dependencies it has - */ - private void step_5() { - - final String markdown = "# Hello!\n\nA paragraph?\n\nIt should be!"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) { - builder.setFactory(Paragraph.class, (configuration, props) -> - new ForegroundColorSpan(Color.GREEN)); - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - /** - * To disable some nodes from rendering another custom plugin can be used - */ - private void step_6() { - - final String markdown = "# Heading 1\n\n## Heading 2\n\n**other** content [here](#)"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) { - // for example to disable rendering of heading: - // try commenting this out to see that otherwise headings will be rendered - builder.on(Heading.class, null); - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } - - /** - * To customize core theme plugins can be used again - */ - private void step_7() { - - final String markdown = "`A code` that is rendered differently\n\n```\nHello!\n```"; - - final Markwon markwon = Markwon.builder(this) - .usePlugin(new AbstractMarkwonPlugin() { - @Override - public void configureTheme(@NonNull MarkwonTheme.Builder builder) { - builder - .codeBackgroundColor(Color.BLACK) - .codeTextColor(Color.RED); - } - }) - .build(); - - markwon.setMarkdown(textView, markdown); - } } diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java new file mode 100644 index 00000000..53232393 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/RecyclerActivity.java @@ -0,0 +1,162 @@ +package ru.noties.markwon.sample.recycler; + +import android.app.Activity; +import android.content.Context; +import android.net.Uri; +import android.os.Bundle; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.text.TextUtils; + +import org.commonmark.ext.gfm.tables.TableBlock; +import org.commonmark.node.FencedCodeBlock; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; + +import ru.noties.debug.AndroidLogDebugOutput; +import ru.noties.debug.Debug; +import ru.noties.markwon.AbstractMarkwonPlugin; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.MarkwonConfiguration; +import ru.noties.markwon.core.CorePlugin; +import ru.noties.markwon.ext.tables.TablePlugin; +import ru.noties.markwon.html.HtmlPlugin; +import ru.noties.markwon.image.ImagesPlugin; +import ru.noties.markwon.image.svg.SvgPlugin; +import ru.noties.markwon.recycler.MarkwonAdapter; +import ru.noties.markwon.recycler.SimpleEntry; +import ru.noties.markwon.sample.R; +import ru.noties.markwon.urlprocessor.UrlProcessor; +import ru.noties.markwon.urlprocessor.UrlProcessorRelativeToAbsolute; + +public class RecyclerActivity extends Activity { + + static { + Debug.init(new AndroidLogDebugOutput(true)); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_recycler); + + // create MarkwonAdapter and register two blocks that will be rendered differently + // * fenced code block (can also specify the same Entry for indended code block) + // * table block + final MarkwonAdapter adapter = MarkwonAdapter.builder() + // we can simply use bundled SimpleEntry, that will lookup a TextView + // with `@+id/text` id + .include(FencedCodeBlock.class, new SimpleEntry(R.layout.adapter_fenced_code_block)) + // create own implementation of entry for different rendering + .include(TableBlock.class, new TableEntry()) + // specify default entry (for all other blocks) + .defaultEntry(new SimpleEntry(R.layout.adapter_default_entry)) + .build(); + + final RecyclerView recyclerView = findViewById(R.id.recycler_view); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + recyclerView.setHasFixedSize(true); + recyclerView.setAdapter(adapter); + + final Markwon markwon = markwon(this); + adapter.setMarkdown(markwon, loadReadMe(this)); + + // please note that we should notify updates (adapter doesn't do it implicitly) + adapter.notifyDataSetChanged(); + + // NB, there is no currently available widget to render tables gracefully + // TableEntryView is here for demonstration purposes only (to show that rendering + // tables + } + + @NonNull + private static Markwon markwon(@NonNull Context context) { + return Markwon.builder(context) + .usePlugin(CorePlugin.create()) + .usePlugin(ImagesPlugin.createWithAssets(context)) + .usePlugin(SvgPlugin.create(context.getResources())) + .usePlugin(TablePlugin.create(context)) + .usePlugin(HtmlPlugin.create()) + .usePlugin(new AbstractMarkwonPlugin() { + @Override + public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) { + builder.urlProcessor(new UrlProcessorInitialReadme()); + } + }) + .build(); + } + + @NonNull + private static String loadReadMe(@NonNull Context context) { + InputStream stream = null; + try { + stream = context.getAssets().open("README.md"); + } catch (IOException e) { + e.printStackTrace(); + } + return readStream(stream); + } + + @NonNull + private static String readStream(@Nullable InputStream inputStream) { + + String out = null; + + if (inputStream != null) { + BufferedReader reader = null; + //noinspection TryFinallyCanBeTryWithResources + try { + reader = new BufferedReader(new InputStreamReader(inputStream)); + final StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line) + .append('\n'); + } + out = builder.toString(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // no op + } + } + } + } + + if (out == null) { + throw new RuntimeException("Cannot read stream"); + } + + return out; + } + + private static class UrlProcessorInitialReadme implements UrlProcessor { + + private static final String GITHUB_BASE = "https://github.com/noties/Markwon/raw/master/"; + + private final UrlProcessorRelativeToAbsolute processor + = new UrlProcessorRelativeToAbsolute(GITHUB_BASE); + + @NonNull + @Override + public String process(@NonNull String destination) { + String out; + final Uri uri = Uri.parse(destination); + if (TextUtils.isEmpty(uri.getScheme())) { + out = processor.process(destination); + } else { + out = destination; + } + return out; + } + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java new file mode 100644 index 00000000..8e3070e3 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntry.java @@ -0,0 +1,67 @@ +package ru.noties.markwon.sample.recycler; + +import android.support.annotation.NonNull; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import org.commonmark.ext.gfm.tables.TableBlock; + +import java.util.HashMap; +import java.util.Map; + +import ru.noties.debug.Debug; +import ru.noties.markwon.Markwon; +import ru.noties.markwon.ext.tables.Table; +import ru.noties.markwon.recycler.MarkwonAdapter; +import ru.noties.markwon.sample.R; + +// do not use in real applications, this is just a showcase +public class TableEntry implements MarkwonAdapter.Entry { + + private final Map cache = new HashMap<>(2); + + @NonNull + @Override + public TableNodeHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) { + return new TableNodeHolder(inflater.inflate(R.layout.adapter_table_block, parent, false)); + } + + @Override + public void bindHolder(@NonNull Markwon markwon, @NonNull TableNodeHolder holder, @NonNull TableBlock node) { + + Table table = cache.get(node); + if (table == null) { + table = Table.parse(markwon, node); + cache.put(node, table); + } + + Debug.i(table); + + if (table != null) { + holder.tableEntryView.setTable(table); + // render table + } // we need to do something with null table... + } + + @Override + public long id(@NonNull TableBlock node) { + return node.hashCode(); + } + + @Override + public void clear() { + cache.clear(); + } + + static class TableNodeHolder extends MarkwonAdapter.Holder { + + final TableEntryView tableEntryView; + + TableNodeHolder(@NonNull View itemView) { + super(itemView); + + this.tableEntryView = requireView(R.id.table_entry); + } + } +} diff --git a/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java new file mode 100644 index 00000000..09c2e771 --- /dev/null +++ b/sample/src/main/java/ru/noties/markwon/sample/recycler/TableEntryView.java @@ -0,0 +1,218 @@ +package ru.noties.markwon.sample.recycler; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.SpannedString; +import android.util.AttributeSet; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import java.util.ArrayList; +import java.util.List; + +import ru.noties.markwon.ext.tables.Table; +import ru.noties.markwon.sample.R; + +public class TableEntryView extends LinearLayout { + + // paint and rect to draw borders + private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + private final Rect rect = new Rect(); + + private LayoutInflater inflater; + + private int rowEvenBackgroundColor; + + public TableEntryView(Context context) { + super(context); + init(context, null); + } + + public TableEntryView(Context context, AttributeSet attrs) { + super(context, attrs); + init(context, attrs); + } + + private void init(Context context, @Nullable AttributeSet attrs) { + inflater = LayoutInflater.from(context); + setOrientation(VERTICAL); + + if (attrs != null) { + final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.TableEntryView); + try { + + rowEvenBackgroundColor = array.getColor(R.styleable.TableEntryView_tev_rowEvenBackgroundColor, 0); + + + final int stroke = array.getDimensionPixelSize(R.styleable.TableEntryView_tev_borderWidth, 0); + + // half of requested + final float strokeWidth = stroke > 0 + ? stroke / 2.F + : context.getResources().getDisplayMetrics().density / 2.F; + + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(strokeWidth); + paint.setColor(array.getColor(R.styleable.TableEntryView_tev_borderColor, Color.BLACK)); + + if (isInEditMode()) { + final String data = array.getString(R.styleable.TableEntryView_tev_debugData); + if (data != null) { + + boolean first = true; + + final List rows = new ArrayList<>(); + for (String row : data.split("\\|")) { + final List columns = new ArrayList<>(); + for (String column : row.split(",")) { + columns.add(new Table.Column(Table.Alignment.LEFT, new SpannedString(column))); + } + final boolean header = first; + first = false; + rows.add(new Table.Row(header, columns)); + } + final Table table = new Table(rows); + setTable(table); + } + } + } finally { + array.recycle(); + } + } + + setWillNotDraw(false); + } + + public void setTable(@NonNull Table table) { + final List rows = table.rows(); + for (int i = 0, size = rows.size(); i < size; i++) { + addRow(i, rows.get(i)); + } + requestLayout(); + } + + private void addRow(int index, @NonNull Table.Row row) { + + final ViewGroup group = ensureRow(index); + + final int backgroundColor = !row.header() && (index % 2) == 0 + ? rowEvenBackgroundColor + : 0; + + group.setBackgroundColor(backgroundColor); + + final List columns = row.columns(); + + TextView textView; + Table.Column column; + + for (int i = 0, size = columns.size(); i < size; i++) { + textView = ensureCell(group, i); + column = columns.get(i); + textView.setGravity(textGravity(column.alignment())); + textView.setText(column.content()); + textView.getPaint().setFakeBoldText(row.header()); + } + } + + @NonNull + private ViewGroup ensureRow(int index) { + + final int count = getChildCount(); + if (index >= count) { + + // count=0,index=1, diff=2 + // count=0,index=5, diff=6 + // count=1,index=2, diff=2 + int diff = index - count + 1; + while (diff > 0) { + addView(inflater.inflate(R.layout.view_table_entry_row, this, false)); + diff -= 1; + } + } + + return (ViewGroup) getChildAt(index); + } + + @NonNull + private TextView ensureCell(@NonNull ViewGroup group, int index) { + + final int count = group.getChildCount(); + if (index >= count) { + int diff = index - count + 1; + while (diff > 0) { + group.addView(inflater.inflate(R.layout.view_table_entry_cell, group, false)); + diff -= 1; + } + } + + return (TextView) group.getChildAt(index); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + + final int rows = getChildCount(); + if (rows == 0) { + return; + } + + // first draw the whole border + rect.set(0, 0, getWidth(), getHeight()); + canvas.drawRect(rect, paint); + + ViewGroup group; + View view; + + int top; + + for (int row = 0; row < rows; row++) { + group = (ViewGroup) getChildAt(row); + top = group.getTop(); + for (int col = 0, cols = group.getChildCount(); col < cols; col++) { + view = group.getChildAt(col); + rect.set(view.getLeft(), top + view.getTop(), view.getRight(), top + view.getBottom()); + canvas.drawRect(rect, paint); + } + } + } + + // we will use gravity instead of textAlignment because min sdk is 16 (textAlignment starts at 17) + @SuppressLint("RtlHardcoded") + private static int textGravity(@NonNull Table.Alignment alignment) { + + final int gravity; + + switch (alignment) { + + case LEFT: + gravity = Gravity.LEFT; + break; + + case CENTER: + gravity = Gravity.CENTER_HORIZONTAL; + break; + + case RIGHT: + gravity = Gravity.RIGHT; + break; + + default: + throw new IllegalStateException("Unknown table alignment: " + alignment); + } + + return gravity; + } +} diff --git a/sample/src/main/recycler/assets/README.md b/sample/src/main/recycler/assets/README.md new file mode 120000 index 00000000..1dfab242 --- /dev/null +++ b/sample/src/main/recycler/assets/README.md @@ -0,0 +1 @@ +../../../../../README.md \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/activity_recycler.xml b/sample/src/main/recycler/res/layout/activity_recycler.xml new file mode 100644 index 00000000..1405e07c --- /dev/null +++ b/sample/src/main/recycler/res/layout/activity_recycler.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/adapter_default_entry.xml b/sample/src/main/recycler/res/layout/adapter_default_entry.xml new file mode 100644 index 00000000..1d302e96 --- /dev/null +++ b/sample/src/main/recycler/res/layout/adapter_default_entry.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml b/sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml new file mode 100644 index 00000000..ddc2c802 --- /dev/null +++ b/sample/src/main/recycler/res/layout/adapter_fenced_code_block.xml @@ -0,0 +1,24 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/adapter_table_block.xml b/sample/src/main/recycler/res/layout/adapter_table_block.xml new file mode 100644 index 00000000..4f0536e5 --- /dev/null +++ b/sample/src/main/recycler/res/layout/adapter_table_block.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/view_table_entry_cell.xml b/sample/src/main/recycler/res/layout/view_table_entry_cell.xml new file mode 100644 index 00000000..1c41e218 --- /dev/null +++ b/sample/src/main/recycler/res/layout/view_table_entry_cell.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/sample/src/main/recycler/res/layout/view_table_entry_row.xml b/sample/src/main/recycler/res/layout/view_table_entry_row.xml new file mode 100644 index 00000000..24e7fb9e --- /dev/null +++ b/sample/src/main/recycler/res/layout/view_table_entry_row.xml @@ -0,0 +1,5 @@ + + \ No newline at end of file diff --git a/sample/src/main/recycler/res/values/attrs.xml b/sample/src/main/recycler/res/values/attrs.xml new file mode 100644 index 00000000..1827819d --- /dev/null +++ b/sample/src/main/recycler/res/values/attrs.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/sample/src/main/res/values/strings-samples.xml b/sample/src/main/res/values/strings-samples.xml index 230c3557..43de11d1 100644 --- a/sample/src/main/res/values/strings-samples.xml +++ b/sample/src/main/res/values/strings-samples.xml @@ -4,7 +4,15 @@ # \# Core\n\nSimple usage example - # \# LaTeX\n\nShows how to display a **LaTeX** formula in a Markwon powered application - # \# Custom extension\n\nShows how to create a custom extension to display an icon referenced in markdown as `@ic-android-black-24` + + # \# Basic plugins\n\nShows basic usage of plugins + + # \# LaTeX\n\nShows how to display a **LaTeX** formula + + # \# Custom extension\n\nShows how to create a custom + extension to display an icon referenced in markdown as `@ic-android-black-24` + + # \# Recycler\n\nShow how to render markdown in a RecyclerView. + Renders code blocks wrapped in a HorizontalScrollView. Renders tables in a custom view. \ No newline at end of file