Merge pull request #226 from noties/v4.3.1

V4.3.1
This commit is contained in:
Dimitry 2020-04-01 09:53:56 +02:00 committed by GitHub
commit a26c13c93a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 1032 additions and 157 deletions

View File

@ -1,5 +1,18 @@
# Changelog # Changelog
# 4.3.1
* Fix DexGuard optimization issue ([#216])<br>Thanks [@francescocervone]
* module `images`: `GifSupport` and `SvgSupport` use `Class.forName` instead access to full qualified class name
* `ext-table`: fix links in tables ([#224])
* `ext-table`: proper borders (equal for all sides)
* module `core`: Add `PrecomputedFutureTextSetterCompat`<br>Thanks [@KirkBushman]
[#216]: https://github.com/noties/Markwon/pull/216
[#224]: https://github.com/noties/Markwon/issues/224
[@francescocervone]: https://github.com/francescocervone
[@KirkBushman]: https://github.com/KirkBushman
# 4.3.0 # 4.3.0
* add `MarkwonInlineParserPlugin` in `inline-parser` module * add `MarkwonInlineParserPlugin` in `inline-parser` module
* `JLatexMathPlugin` now supports inline LaTeX structures via `MarkwonInlineParserPlugin` * `JLatexMathPlugin` now supports inline LaTeX structures via `MarkwonInlineParserPlugin`
@ -12,8 +25,7 @@ dependency (must be explicitly added to `Markwon` whilst configuring)
* `LinkResolverDef` defaults to `https` when a link does not have scheme information ([#75]) * `LinkResolverDef` defaults to `https` when a link does not have scheme information ([#75])
* add `option` abstraction for `sample` module allowing switching of multiple cases in runtime via menu * add `option` abstraction for `sample` module allowing switching of multiple cases in runtime via menu
* non-empty bounds for `AsyncDrawable` when no dimensions are not yet available ([#189]) * non-empty bounds for `AsyncDrawable` when no dimensions are not yet available ([#189])
* `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201]) * `linkify` - option to use `LinkifyCompat` in `LinkifyPlugin` ([#201])<br>Thanks to [@drakeet]
<br>Thanks to [@drakeet]
* `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them * `MarkwonVisitor.BlockHandler` and `BlockHandlerDef` implementation to control how blocks insert new lines after them

View File

@ -0,0 +1,59 @@
package io.noties.markwon.debug;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import io.noties.markwon.app.R;
import io.noties.markwon.utils.ColorUtils;
public class ColorBlendView extends View {
private final Rect rect = new Rect();
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private int background;
private int foreground;
public ColorBlendView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
if (attrs != null) {
final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.ColorBlendView);
try {
background = array.getColor(R.styleable.ColorBlendView_cbv_background, 0);
foreground = array.getColor(R.styleable.ColorBlendView_cbv_foreground, 0);
} finally {
array.recycle();
}
}
paint.setStyle(Paint.Style.FILL);
setWillNotDraw(false);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
final int side = getWidth() / 11;
rect.set(0, 0, side, getHeight());
canvas.translate(getPaddingLeft(), 0F);
for (int i = 0; i < 11; i++) {
final float alpha = i / 10F;
paint.setColor(ColorUtils.blend(foreground, background, alpha));
canvas.drawRect(rect, paint);
canvas.translate(side, 0F);
}
}
}

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#f00">
<io.noties.markwon.debug.ColorBlendView
android:layout_width="match_parent"
android:layout_height="64dip"
app:cbv_background="#fff"
app:cbv_foreground="#f0f"/>
<io.noties.markwon.debug.ColorBlendView
android:layout_width="match_parent"
android:layout_height="64dip"
app:cbv_background="#000"
app:cbv_foreground="#f0f"/>
<io.noties.markwon.debug.ColorBlendView
android:layout_width="match_parent"
android:layout_height="64dip"
app:cbv_background="#fff"
app:cbv_foreground="#00f"/>
<io.noties.markwon.debug.ColorBlendView
android:layout_width="match_parent"
android:layout_height="64dip"
app:cbv_background="#000"
app:cbv_foreground="#00f"/>
</LinearLayout>

View File

@ -8,4 +8,9 @@
<attr name="fcdv_checked" format="boolean" /> <attr name="fcdv_checked" format="boolean" />
</declare-styleable> </declare-styleable>
<declare-styleable name="ColorBlendView">
<attr name="cbv_foreground" format="color" />
<attr name="cbv_background" format="color" />
</declare-styleable>
</resources> </resources>

View File

@ -66,6 +66,7 @@ ext {
'x-annotations' : 'androidx.annotation:annotation:1.1.0', 'x-annotations' : 'androidx.annotation:annotation:1.1.0',
'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0', 'x-recycler-view' : 'androidx.recyclerview:recyclerview:1.0.0',
'x-core' : 'androidx.core:core:1.0.2', 'x-core' : 'androidx.core:core:1.0.2',
'x-appcompat' : 'androidx.appcompat:appcompat:1.1.0',
'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion", 'commonmark' : "com.atlassian.commonmark:commonmark:$commonMarkVersion",
'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion", 'commonmark-strikethrough': "com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:$commonMarkVersion",
'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion", 'commonmark-table' : "com.atlassian.commonmark:commonmark-ext-gfm-tables:$commonMarkVersion",

View File

@ -8,7 +8,7 @@ android.enableJetifier=true
android.enableBuildCache=true android.enableBuildCache=true
android.buildCacheDir=build/pre-dex-cache android.buildCacheDir=build/pre-dex-cache
VERSION_NAME=4.3.0 VERSION_NAME=4.3.1
GROUP=io.noties.markwon GROUP=io.noties.markwon
POM_DESCRIPTION=Markwon markdown for Android POM_DESCRIPTION=Markwon markdown for Android

View File

@ -22,6 +22,7 @@ dependencies {
// @since 4.1.0 to allow PrecomputedTextSetterCompat // @since 4.1.0 to allow PrecomputedTextSetterCompat
// note that this dependency must be added on a client side explicitly // note that this dependency must be added on a client side explicitly
compileOnly it['x-core'] compileOnly it['x-core']
compileOnly it['x-appcompat']
} }
deps['test'].with { deps['test'].with {

View File

@ -0,0 +1,65 @@
package io.noties.markwon;
import android.text.Spanned;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.text.PrecomputedTextCompat;
import java.util.concurrent.Executor;
import java.util.concurrent.Future;
/**
* Please note this class requires `androidx.core:core` artifact being explicitly added to your dependencies.
* This is intended to be used in a RecyclerView.
*
* @see io.noties.markwon.Markwon.TextSetter
* @since 4.3.1
*/
public class PrecomputedFutureTextSetterCompat implements Markwon.TextSetter {
/**
* @param executor for background execution of text pre-computation,
* if not provided the standard, single threaded one will be used.
*/
@NonNull
public static PrecomputedFutureTextSetterCompat create(@Nullable Executor executor) {
return new PrecomputedFutureTextSetterCompat(executor);
}
@NonNull
public static PrecomputedFutureTextSetterCompat create() {
return new PrecomputedFutureTextSetterCompat(null);
}
@Nullable
private final Executor executor;
@SuppressWarnings("WeakerAccess")
PrecomputedFutureTextSetterCompat(@Nullable Executor executor) {
this.executor = executor;
}
@Override
public void setText(
@NonNull TextView textView,
@NonNull Spanned markdown,
@NonNull TextView.BufferType bufferType,
@NonNull Runnable onComplete) {
if (textView instanceof AppCompatTextView) {
final AppCompatTextView appCompatTextView = (AppCompatTextView) textView;
final Future<PrecomputedTextCompat> future = PrecomputedTextCompat.getTextFuture(
markdown,
appCompatTextView.getTextMetricsParamsCompat(),
executor);
appCompatTextView.setTextFuture(future);
// `setTextFuture` is actually a synchronous call, so we should call onComplete now
onComplete.run();
} else {
throw new IllegalStateException("TextView provided is not an instance of AppCompatTextView, " +
"cannot call setTextFuture(), textView: " + textView);
}
}
}

View File

@ -1,11 +1,33 @@
package io.noties.markwon.utils; package io.noties.markwon.utils;
import android.graphics.Color;
import androidx.annotation.ColorInt;
import androidx.annotation.FloatRange;
import androidx.annotation.IntRange;
public abstract class ColorUtils { public abstract class ColorUtils {
public static int applyAlpha(int color, int alpha) { @ColorInt
public static int applyAlpha(
@ColorInt int color,
@IntRange(from = 0, to = 255) int alpha) {
return (color & 0x00FFFFFF) | (alpha << 24); return (color & 0x00FFFFFF) | (alpha << 24);
} }
// blend two colors w/ specified ratio, resulting color won't have alpha channel
@ColorInt
public static int blend(
@ColorInt int foreground,
@ColorInt int background,
@FloatRange(from = 0.0F, to = 1.0F) float ratio) {
return Color.rgb(
(int) (((1F - ratio) * Color.red(foreground)) + (ratio * Color.red(background))),
(int) (((1F - ratio) * Color.green(foreground)) + (ratio * Color.green(background))),
(int) (((1F - ratio) * Color.blue(foreground)) + (ratio * Color.blue(background)))
);
}
private ColorUtils() { private ColorUtils() {
} }
} }

View File

@ -123,12 +123,13 @@ public class TablePlugin extends AbstractMarkwonPlugin {
visitor.blockStart(tableBlock); visitor.blockStart(tableBlock);
final int length = visitor.length();
visitor.visitChildren(tableBlock); visitor.visitChildren(tableBlock);
// if (visitor.hasNext(tableBlock)) { // @since 4.3.1 apply table span for the full table
// visitor.ensureNewLine(); visitor.setSpans(length, new TableSpan());
// visitor.forceNewLine();
// }
visitor.blockEnd(tableBlock); visitor.blockEnd(tableBlock);
} }
}) })

View File

@ -5,6 +5,7 @@ import android.graphics.Canvas;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.text.Layout; import android.text.Layout;
import android.text.Spanned;
import android.text.StaticLayout; import android.text.StaticLayout;
import android.text.TextPaint; import android.text.TextPaint;
import android.text.style.ReplacementSpan; import android.text.style.ReplacementSpan;
@ -19,6 +20,8 @@ import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import io.noties.markwon.utils.LeadingMarginUtils;
public class TableRowSpan extends ReplacementSpan { public class TableRowSpan extends ReplacementSpan {
public static final int ALIGN_LEFT = 0; public static final int ALIGN_LEFT = 0;
@ -139,11 +142,17 @@ public class TableRowSpan extends ReplacementSpan {
int top, int top,
int y, int y,
int bottom, int bottom,
@NonNull Paint paint) { @NonNull Paint p) {
if (recreateLayouts(canvas.getWidth())) { if (recreateLayouts(canvas.getWidth())) {
width = canvas.getWidth(); width = canvas.getWidth();
textPaint.set(paint); // @since 4.3.1 it's important to cast to TextPaint in order to display links, etc
if (p instanceof TextPaint) {
// there must be a reason why this method receives Paint instead of TextPaint...
textPaint.set((TextPaint) p);
} else {
textPaint.set(p);
}
makeNewLayouts(); makeNewLayouts();
} }
@ -155,28 +164,25 @@ public class TableRowSpan extends ReplacementSpan {
final int w = width / size; final int w = width / size;
// feels like magic...
final int heightDiff = (bottom - top - height) / 4;
// @since 1.1.1 // @since 1.1.1
// draw backgrounds // draw backgrounds
{ {
if (header) { if (header) {
theme.applyTableHeaderRowStyle(this.paint); theme.applyTableHeaderRowStyle(paint);
} else if (odd) { } else if (odd) {
theme.applyTableOddRowStyle(this.paint); theme.applyTableOddRowStyle(paint);
} else { } else {
// even // even
theme.applyTableEvenRowStyle(this.paint); theme.applyTableEvenRowStyle(paint);
} }
// if present (0 is transparent) // if present (0 is transparent)
if (this.paint.getColor() != 0) { if (paint.getColor() != 0) {
final int save = canvas.save(); final int save = canvas.save();
try { try {
rect.set(0, 0, width, bottom - top); rect.set(0, 0, width, bottom - top);
canvas.translate(x, top - heightDiff); canvas.translate(x, top);
canvas.drawRect(rect, this.paint); canvas.drawRect(rect, paint);
} finally { } finally {
canvas.restoreToCount(save); canvas.restoreToCount(save);
} }
@ -186,25 +192,73 @@ public class TableRowSpan extends ReplacementSpan {
// @since 1.1.1 reset after applying background color // @since 1.1.1 reset after applying background color
// as background changes color attribute and if not specific tableBorderColor // as background changes color attribute and if not specific tableBorderColor
// is specified then after this row all borders will have color of this row (plus alpha) // is specified then after this row all borders will have color of this row (plus alpha)
this.paint.set(paint); paint.set(p);
theme.applyTableBorderStyle(this.paint); theme.applyTableBorderStyle(paint);
final int borderWidth = theme.tableBorderWidth(paint); final int borderWidth = theme.tableBorderWidth(paint);
final boolean drawBorder = borderWidth > 0; final boolean drawBorder = borderWidth > 0;
// why divided by 4 gives a more or less good result is still not clear (shouldn't it be 2?)
final int heightDiff = (bottom - top - height) / 4;
// required for borderTop calculation
final boolean isFirstTableRow;
// @since 4.3.1
if (drawBorder) { if (drawBorder) {
rect.set(0, 0, w, bottom - top); boolean first = false;
// only if first draw the line
{
final Spanned spanned = (Spanned) text;
final TableSpan[] spans = spanned.getSpans(start, end, TableSpan.class);
if (spans != null && spans.length > 0) {
final TableSpan span = spans[0];
if (LeadingMarginUtils.selfStart(start, text, span)) {
first = true;
rect.set((int) x, top, width, top + borderWidth);
canvas.drawRect(rect, paint);
}
}
}
// draw the line at the bottom
rect.set((int) x, bottom - borderWidth, width, bottom);
canvas.drawRect(rect, paint);
isFirstTableRow = first;
} else {
isFirstTableRow = false;
} }
final int borderWidthHalf = borderWidth / 2;
// to NOT overlap borders inset top and bottom
final int borderTop = isFirstTableRow ? borderWidth : 0;
final int borderBottom = bottom - top - borderWidth;
StaticLayout layout; StaticLayout layout;
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
layout = layouts.get(i); layout = layouts.get(i);
final int save = canvas.save(); final int save = canvas.save();
try { try {
canvas.translate(x + (i * w), top - heightDiff); canvas.translate(x + (i * w), top);
// @since 4.3.1
if (drawBorder) { if (drawBorder) {
canvas.drawRect(rect, this.paint); // first vertical border will have full width (it cannot exceed canvas)
if (i == 0) {
rect.set(0, borderTop, borderWidth, borderBottom);
} else {
rect.set(-borderWidthHalf, borderTop, borderWidthHalf, borderBottom);
}
canvas.drawRect(rect, paint);
if (i == (size - 1)) {
rect.set(w - borderWidth, borderTop, w, borderBottom);
canvas.drawRect(rect, paint);
}
} }
canvas.translate(padding, padding + heightDiff); canvas.translate(padding, padding + heightDiff);

View File

@ -0,0 +1,7 @@
package io.noties.markwon.ext.tables;
/**
* @since 4.3.1
*/
public class TableSpan {
}

View File

@ -10,6 +10,7 @@ import androidx.annotation.Px;
import io.noties.markwon.utils.ColorUtils; import io.noties.markwon.utils.ColorUtils;
import io.noties.markwon.utils.Dip; import io.noties.markwon.utils.Dip;
@SuppressWarnings("WeakerAccess")
public class TableTheme { public class TableTheme {
@NonNull @NonNull
@ -101,7 +102,8 @@ public class TableTheme {
} }
paint.setColor(color); paint.setColor(color);
paint.setStyle(Paint.Style.STROKE); // @since 4.3.1 before it was STROKE... change to FILL as we draw border differently
paint.setStyle(Paint.Style.FILL);
} }
public void applyTableOddRowStyle(@NonNull Paint paint) { public void applyTableOddRowStyle(@NonNull Paint paint) {

View File

@ -25,7 +25,9 @@ public class StrikeHandler extends TagHandler {
static { static {
boolean hasMarkdownImplementation; boolean hasMarkdownImplementation;
try { try {
org.commonmark.ext.gfm.strikethrough.Strikethrough.class.getName(); // @since 4.3.1 we class Class.forName instead of trying
// to access the class by full qualified name (which caused issues with DexGuard)
Class.forName("org.commonmark.ext.gfm.strikethrough.Strikethrough");
hasMarkdownImplementation = true; hasMarkdownImplementation = true;
} catch (Throwable t) { } catch (Throwable t) {
hasMarkdownImplementation = false; hasMarkdownImplementation = false;

View File

@ -14,7 +14,8 @@ public abstract class GifSupport {
static { static {
boolean result; boolean result;
try { try {
pl.droidsonroids.gif.GifDrawable.class.getName(); // @since 4.3.1
Class.forName("pl.droidsonroids.gif.GifDrawable");
result = true; result = true;
} catch (Throwable t) { } catch (Throwable t) {
// @since 4.1.1 instead of printing full stacktrace of the exception, // @since 4.1.1 instead of printing full stacktrace of the exception,

View File

@ -14,7 +14,7 @@ public abstract class SvgSupport {
static { static {
boolean result; boolean result;
try { try {
com.caverock.androidsvg.SVG.class.getName(); Class.forName("com.caverock.androidsvg.SVG");
result = true; result = true;
} catch (Throwable t) { } catch (Throwable t) {
// @since 4.1.1 instead of printing full stacktrace of the exception, // @since 4.1.1 instead of printing full stacktrace of the exception,

View File

@ -54,6 +54,7 @@ dependencies {
deps.with { deps.with {
implementation it['x-recycler-view'] implementation it['x-recycler-view']
implementation it['x-core'] // for precomputedTextCompat implementation it['x-core'] // for precomputedTextCompat
implementation it['x-appcompat'] // for setTextFuture
implementation it['okhttp'] implementation it['okhttp']
implementation it['prism4j'] implementation it['prism4j']
implementation it['debug'] implementation it['debug']

View File

@ -28,6 +28,7 @@
<activity android:name=".simpleext.SimpleExtActivity" /> <activity android:name=".simpleext.SimpleExtActivity" />
<activity android:name=".customextension2.CustomExtensionActivity2" /> <activity android:name=".customextension2.CustomExtensionActivity2" />
<activity android:name=".precomputed.PrecomputedActivity" /> <activity android:name=".precomputed.PrecomputedActivity" />
<activity android:name=".precomputed.PrecomputedFutureActivity" />
<activity <activity
android:name=".editor.EditorActivity" android:name=".editor.EditorActivity"
@ -38,6 +39,7 @@
<activity android:name=".tasklist.TaskListActivity" /> <activity android:name=".tasklist.TaskListActivity" />
<activity android:name=".images.ImagesActivity" /> <activity android:name=".images.ImagesActivity" />
<activity android:name=".notification.NotificationActivity" /> <activity android:name=".notification.NotificationActivity" />
<activity android:name=".table.TableActivity" />
</application> </application>

View File

@ -30,8 +30,10 @@ import io.noties.markwon.sample.inlineparser.InlineParserActivity;
import io.noties.markwon.sample.latex.LatexActivity; import io.noties.markwon.sample.latex.LatexActivity;
import io.noties.markwon.sample.notification.NotificationActivity; import io.noties.markwon.sample.notification.NotificationActivity;
import io.noties.markwon.sample.precomputed.PrecomputedActivity; import io.noties.markwon.sample.precomputed.PrecomputedActivity;
import io.noties.markwon.sample.precomputed.PrecomputedFutureActivity;
import io.noties.markwon.sample.recycler.RecyclerActivity; import io.noties.markwon.sample.recycler.RecyclerActivity;
import io.noties.markwon.sample.simpleext.SimpleExtActivity; import io.noties.markwon.sample.simpleext.SimpleExtActivity;
import io.noties.markwon.sample.table.TableActivity;
import io.noties.markwon.sample.tasklist.TaskListActivity; import io.noties.markwon.sample.tasklist.TaskListActivity;
public class MainActivity extends Activity { public class MainActivity extends Activity {
@ -123,6 +125,10 @@ public class MainActivity extends Activity {
activity = PrecomputedActivity.class; activity = PrecomputedActivity.class;
break; break;
case PRECOMPUTED_FUTURE_TEXT:
activity = PrecomputedFutureActivity.class;
break;
case EDITOR: case EDITOR:
activity = EditorActivity.class; activity = EditorActivity.class;
break; break;
@ -147,6 +153,10 @@ public class MainActivity extends Activity {
activity = NotificationActivity.class; activity = NotificationActivity.class;
break; break;
case TABLE:
activity = TableActivity.class;
break;
default: default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item); throw new IllegalStateException("No Activity is associated with sample-item: " + item);
} }

View File

@ -23,6 +23,8 @@ public enum Sample {
PRECOMPUTED_TEXT(R.string.sample_precomputed_text), PRECOMPUTED_TEXT(R.string.sample_precomputed_text),
PRECOMPUTED_FUTURE_TEXT(R.string.sample_precomputed_future_text),
EDITOR(R.string.sample_editor), EDITOR(R.string.sample_editor),
INLINE_PARSER(R.string.sample_inline_parser), INLINE_PARSER(R.string.sample_inline_parser),
@ -33,7 +35,9 @@ public enum Sample {
IMAGES(R.string.sample_images), IMAGES(R.string.sample_images),
REMOTE_VIEWS(R.string.sample_remote_views); REMOTE_VIEWS(R.string.sample_remote_views),
TABLE(R.string.sample_table);
private final int textResId; private final int textResId;

View File

@ -0,0 +1,97 @@
package io.noties.markwon.sample.basicplugins;
import android.text.Spannable;
import android.text.Spanned;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.core.spans.HeadingSpan;
public class AnchorHeadingPlugin extends AbstractMarkwonPlugin {
public interface ScrollTo {
void scrollTo(@NonNull TextView view, int top);
}
private final ScrollTo scrollTo;
AnchorHeadingPlugin(@NonNull ScrollTo scrollTo) {
this.scrollTo = scrollTo;
}
@Override
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver(new AnchorLinkResolver(scrollTo));
}
@Override
public void afterSetText(@NonNull TextView textView) {
final Spannable spannable = (Spannable) textView.getText();
// obtain heading spans
final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class);
if (spans != null) {
for (HeadingSpan span : spans) {
final int start = spannable.getSpanStart(span);
final int end = spannable.getSpanEnd(span);
final int flags = spannable.getSpanFlags(span);
spannable.setSpan(
new AnchorSpan(createAnchor(spannable.subSequence(start, end))),
start,
end,
flags
);
}
}
}
private static class AnchorLinkResolver extends LinkResolverDef {
private final ScrollTo scrollTo;
AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
this.scrollTo = scrollTo;
}
@Override
public void resolve(@NonNull View view, @NonNull String link) {
if (link.startsWith("#")) {
final TextView textView = (TextView) view;
final Spanned spanned = (Spannable) textView.getText();
final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
if (spans != null) {
final String anchor = link.substring(1);
for (AnchorSpan span : spans) {
if (anchor.equals(span.anchor)) {
final int start = spanned.getSpanStart(span);
final int line = textView.getLayout().getLineForOffset(start);
final int top = textView.getLayout().getLineTop(line);
scrollTo.scrollTo(textView, top);
return;
}
}
}
}
super.resolve(view, link);
}
}
private static class AnchorSpan {
final String anchor;
AnchorSpan(@NonNull String anchor) {
this.anchor = anchor;
}
}
@NonNull
public static String createAnchor(@NonNull CharSequence content) {
return String.valueOf(content)
.replaceAll("[^\\w]", "")
.toLowerCase();
}
}

View File

@ -3,11 +3,8 @@ package io.noties.markwon.sample.basicplugins;
import android.graphics.Color; import android.graphics.Color;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.view.View;
import android.widget.ScrollView; import android.widget.ScrollView;
import android.widget.TextView; import android.widget.TextView;
@ -23,7 +20,6 @@ import java.util.Collections;
import io.noties.markwon.AbstractMarkwonPlugin; import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.BlockHandlerDef; import io.noties.markwon.BlockHandlerDef;
import io.noties.markwon.LinkResolverDef;
import io.noties.markwon.Markwon; import io.noties.markwon.Markwon;
import io.noties.markwon.MarkwonConfiguration; import io.noties.markwon.MarkwonConfiguration;
import io.noties.markwon.MarkwonSpansFactory; import io.noties.markwon.MarkwonSpansFactory;
@ -31,7 +27,6 @@ import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.SoftBreakAddsNewLinePlugin; import io.noties.markwon.SoftBreakAddsNewLinePlugin;
import io.noties.markwon.core.CoreProps; import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.MarkwonTheme; import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.HeadingSpan;
import io.noties.markwon.core.spans.LastLineSpacingSpan; import io.noties.markwon.core.spans.LastLineSpacingSpan;
import io.noties.markwon.image.ImageItem; import io.noties.markwon.image.ImageItem;
import io.noties.markwon.image.ImagesPlugin; import io.noties.markwon.image.ImagesPlugin;
@ -62,7 +57,9 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
.add("headingNoSpace", this::headingNoSpace) .add("headingNoSpace", this::headingNoSpace)
.add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler) .add("headingNoSpaceBlockHandler", this::headingNoSpaceBlockHandler)
.add("allBlocksNoForcedLine", this::allBlocksNoForcedLine) .add("allBlocksNoForcedLine", this::allBlocksNoForcedLine)
.add("anchor", this::anchor); .add("anchor", this::anchor)
.add("letterOrderedList", this::letterOrderedList)
.add("tableOfContents", this::tableOfContents);
} }
@Override @Override
@ -323,26 +320,26 @@ public class BasicPluginsActivity extends ActivityWithMenuOptions {
} }
private void headingNoSpaceBlockHandler() { private void headingNoSpaceBlockHandler() {
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new AbstractMarkwonPlugin() {
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.blockHandler(new BlockHandlerDef() {
@Override @Override
public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) { public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
if (node instanceof Heading) { builder.blockHandler(new BlockHandlerDef() {
if (visitor.hasNext(node)) { @Override
visitor.ensureNewLine(); public void blockEnd(@NonNull MarkwonVisitor visitor, @NonNull Node node) {
// ensure new line but do not force insert one if (node instanceof Heading) {
if (visitor.hasNext(node)) {
visitor.ensureNewLine();
// ensure new line but do not force insert one
}
} else {
super.blockEnd(visitor, node);
}
} }
} else { });
super.blockEnd(visitor, node);
}
} }
}); })
} .build();
})
.build();
final String md = "" + final String md = "" +
"# Title title title title title title title title title title \n\ntext text text text"; "# Title title title title title title title title title title \n\ntext text text text";
@ -384,85 +381,6 @@ final Markwon markwon = Markwon.builder(this)
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, md);
} }
// public void step_6() {
//
// final Markwon markwon = Markwon.builder(this)
// .usePlugin(HtmlPlugin.create())
// .usePlugin(new AbstractMarkwonPlugin() {
// @Override
// public void configure(@NonNull Registry registry) {
// registry.require(HtmlPlugin.class, plugin -> plugin.addHandler(new SimpleTagHandler() {
// @Override
// public Object getSpans(@NonNull MarkwonConfiguration configuration, @NonNull RenderProps renderProps, @NonNull HtmlTag tag) {
// return new AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER);
// }
//
// @NonNull
// @Override
// public Collection<String> supportedTags() {
// return Collections.singleton("center");
// }
// }));
// }
// })
// .build();
// }
// text lifecycle (after/before)
// rendering lifecycle (before/after)
// renderProps
// process
private static class AnchorSpan {
final String anchor;
AnchorSpan(@NonNull String anchor) {
this.anchor = anchor;
}
}
@NonNull
private String createAnchor(@NonNull CharSequence content) {
return String.valueOf(content)
.replaceAll("[^\\w]", "")
.toLowerCase();
}
private static class AnchorLinkResolver extends LinkResolverDef {
interface ScrollTo {
void scrollTo(@NonNull View view, int top);
}
private final ScrollTo scrollTo;
AnchorLinkResolver(@NonNull ScrollTo scrollTo) {
this.scrollTo = scrollTo;
}
@Override
public void resolve(@NonNull View view, @NonNull String link) {
if (link.startsWith("#")) {
final TextView textView = (TextView) view;
final Spanned spanned = (Spannable) textView.getText();
final AnchorSpan[] spans = spanned.getSpans(0, spanned.length(), AnchorSpan.class);
if (spans != null) {
final String anchor = link.substring(1);
for (AnchorSpan span: spans) {
if (anchor.equals(span.anchor)) {
final int start = spanned.getSpanStart(span);
final int line = textView.getLayout().getLineForOffset(start);
final int top = textView.getLayout().getLineTop(line);
scrollTo.scrollTo(textView, top);
return;
}
}
}
}
super.resolve(view, link);
}
}
private void anchor() { private void anchor() {
final String lorem = getString(R.string.lorem); final String lorem = getString(R.string.lorem);
final String md = "" + final String md = "" +
@ -472,32 +390,46 @@ final Markwon markwon = Markwon.builder(this)
lorem; lorem;
final Markwon markwon = Markwon.builder(this) final Markwon markwon = Markwon.builder(this)
.usePlugin(new AbstractMarkwonPlugin() { .usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
@Override .build();
public void configureConfiguration(@NonNull MarkwonConfiguration.Builder builder) {
builder.linkResolver(new AnchorLinkResolver((view, top) -> scrollView.smoothScrollTo(0, top)));
}
@Override markwon.setMarkdown(textView, md);
public void afterSetText(@NonNull TextView textView) { }
final Spannable spannable = (Spannable) textView.getText();
// obtain heading spans private void letterOrderedList() {
final HeadingSpan[] spans = spannable.getSpans(0, spannable.length(), HeadingSpan.class); // bullet list nested in ordered list renders letters instead of bullets
if (spans != null) { final String md = "" +
for (HeadingSpan span : spans) { "1. Hello there!\n" +
final int start = spannable.getSpanStart(span); "1. And here is how:\n" +
final int end = spannable.getSpanEnd(span); " - First\n" +
final int flags = spannable.getSpanFlags(span); " - Second\n" +
spannable.setSpan( " - Third\n" +
new AnchorSpan(createAnchor(spannable.subSequence(start, end))), " 1. And first here\n\n";
start,
end, final Markwon markwon = Markwon.builder(this)
flags .usePlugin(new BulletListIsOrderedWithLettersWhenNestedPlugin())
); .build();
}
} markwon.setMarkdown(textView, md);
} }
})
private void tableOfContents() {
final String lorem = getString(R.string.lorem);
final String md = "" +
"# First\n" +
"" + lorem + "\n\n" +
"# Second\n" +
"" + lorem + "\n\n" +
"## Second level\n\n" +
"" + lorem + "\n\n" +
"### Level 3\n\n" +
"" + lorem + "\n\n" +
"# First again\n" +
"" + lorem + "\n\n";
final Markwon markwon = Markwon.builder(this)
.usePlugin(new TableOfContentsPlugin())
.usePlugin(new AnchorHeadingPlugin((view, top) -> scrollView.smoothScrollTo(0, top)))
.build(); .build();
markwon.setMarkdown(textView, md); markwon.setMarkdown(textView, md);

View File

@ -0,0 +1,156 @@
package io.noties.markwon.sample.basicplugins;
import android.text.TextUtils;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import org.commonmark.node.BulletList;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.OrderedList;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonSpansFactory;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.Prop;
import io.noties.markwon.core.CoreProps;
import io.noties.markwon.core.spans.BulletListItemSpan;
import io.noties.markwon.core.spans.OrderedListItemSpan;
public class BulletListIsOrderedWithLettersWhenNestedPlugin extends AbstractMarkwonPlugin {
private static final Prop<String> BULLET_LETTER = Prop.of("my-bullet-letter");
// or introduce some kind of synchronization if planning to use from multiple threads,
// for example via ThreadLocal
private final SparseIntArray bulletCounter = new SparseIntArray();
@Override
public void afterRender(@NonNull Node node, @NonNull MarkwonVisitor visitor) {
// clear counter after render
bulletCounter.clear();
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
// NB that both ordered and bullet lists are represented
// by ListItem (must inspect parent to detect the type)
builder.on(ListItem.class, (visitor, listItem) -> {
// mimic original behaviour (copy-pasta from CorePlugin)
final int length = visitor.length();
visitor.visitChildren(listItem);
final Node parent = listItem.getParent();
if (parent instanceof OrderedList) {
final int start = ((OrderedList) parent).getStartNumber();
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.ORDERED);
CoreProps.ORDERED_LIST_ITEM_NUMBER.set(visitor.renderProps(), start);
// after we have visited the children increment start number
final OrderedList orderedList = (OrderedList) parent;
orderedList.setStartNumber(orderedList.getStartNumber() + 1);
} else {
CoreProps.LIST_ITEM_TYPE.set(visitor.renderProps(), CoreProps.ListItemType.BULLET);
if (isBulletOrdered(parent)) {
// obtain current count value
final int count = currentBulletCountIn(parent);
BULLET_LETTER.set(visitor.renderProps(), createBulletLetter(count));
// update current count value
setCurrentBulletCountIn(parent, count + 1);
} else {
CoreProps.BULLET_LIST_ITEM_LEVEL.set(visitor.renderProps(), listLevel(listItem));
// clear letter info when regular bullet list is used
BULLET_LETTER.clear(visitor.renderProps());
}
}
visitor.setSpansForNodeOptional(listItem, length);
if (visitor.hasNext(listItem)) {
visitor.ensureNewLine();
}
});
}
@Override
public void configureSpansFactory(@NonNull MarkwonSpansFactory.Builder builder) {
builder.setFactory(ListItem.class, (configuration, props) -> {
final Object spans;
if (CoreProps.ListItemType.BULLET == CoreProps.LIST_ITEM_TYPE.require(props)) {
final String letter = BULLET_LETTER.get(props);
if (!TextUtils.isEmpty(letter)) {
// NB, we are using OrderedListItemSpan here!
spans = new OrderedListItemSpan(
configuration.theme(),
letter
);
} else {
spans = new BulletListItemSpan(
configuration.theme(),
CoreProps.BULLET_LIST_ITEM_LEVEL.require(props)
);
}
} else {
final String number = String.valueOf(CoreProps.ORDERED_LIST_ITEM_NUMBER.require(props))
+ "." + '\u00a0';
spans = new OrderedListItemSpan(
configuration.theme(),
number
);
}
return spans;
});
}
private int currentBulletCountIn(@NonNull Node parent) {
return bulletCounter.get(parent.hashCode(), 0);
}
private void setCurrentBulletCountIn(@NonNull Node parent, int count) {
bulletCounter.put(parent.hashCode(), count);
}
@NonNull
private static String createBulletLetter(int count) {
// or lower `a`
// `'u00a0` is non-breakable space char
return ((char) ('A' + count)) + ".\u00a0";
}
private static int listLevel(@NonNull Node node) {
int level = 0;
Node parent = node.getParent();
while (parent != null) {
if (parent instanceof ListItem) {
level += 1;
}
parent = parent.getParent();
}
return level;
}
private static boolean isBulletOrdered(@NonNull Node node) {
node = node.getParent();
while (node != null) {
if (node instanceof OrderedList) {
return true;
}
if (node instanceof BulletList) {
return false;
}
node = node.getParent();
}
return false;
}
}

View File

@ -0,0 +1,115 @@
package io.noties.markwon.sample.basicplugins;
import androidx.annotation.NonNull;
import org.commonmark.node.AbstractVisitor;
import org.commonmark.node.BulletList;
import org.commonmark.node.CustomBlock;
import org.commonmark.node.Heading;
import org.commonmark.node.Link;
import org.commonmark.node.ListItem;
import org.commonmark.node.Node;
import org.commonmark.node.Text;
import io.noties.markwon.AbstractMarkwonPlugin;
import io.noties.markwon.MarkwonVisitor;
import io.noties.markwon.core.SimpleBlockNodeVisitor;
public class TableOfContentsPlugin extends AbstractMarkwonPlugin {
@Override
public void configure(@NonNull Registry registry) {
// just to make it explicit
registry.require(AnchorHeadingPlugin.class);
}
@Override
public void configureVisitor(@NonNull MarkwonVisitor.Builder builder) {
builder.on(TableOfContentsBlock.class, new SimpleBlockNodeVisitor());
}
@Override
public void beforeRender(@NonNull Node node) {
// custom block to hold TOC
final TableOfContentsBlock block = new TableOfContentsBlock();
// create TOC title
{
final Text text = new Text("Table of contents");
final Heading heading = new Heading();
// important one - set TOC heading level
heading.setLevel(1);
heading.appendChild(text);
block.appendChild(heading);
}
final HeadingVisitor visitor = new HeadingVisitor(block);
node.accept(visitor);
// make it the very first node in rendered markdown
node.prependChild(block);
}
private static class HeadingVisitor extends AbstractVisitor {
private final BulletList bulletList = new BulletList();
private final StringBuilder builder = new StringBuilder();
private boolean isInsideHeading;
HeadingVisitor(@NonNull Node node) {
node.appendChild(bulletList);
}
@Override
public void visit(Heading heading) {
this.isInsideHeading = true;
try {
// reset build from previous content
builder.setLength(0);
// obtain level (can additionally filter by level, to skip lower ones)
final int level = heading.getLevel();
// build heading title
visitChildren(heading);
// initial list item
final ListItem listItem = new ListItem();
Node parent = listItem;
Node node = listItem;
for (int i = 1; i < level; i++) {
final ListItem li = new ListItem();
final BulletList bulletList = new BulletList();
bulletList.appendChild(li);
parent.appendChild(bulletList);
parent = li;
node = li;
}
final String content = builder.toString();
final Link link = new Link("#" + AnchorHeadingPlugin.createAnchor(content), null);
final Text text = new Text(content);
link.appendChild(text);
node.appendChild(link);
bulletList.appendChild(listItem);
} finally {
isInsideHeading = false;
}
}
@Override
public void visit(Text text) {
// can additionally check if we are building heading (to skip all other texts)
if (isInsideHeading) {
builder.append(text.getLiteral());
}
}
}
private static class TableOfContentsBlock extends CustomBlock {
}
}

View File

@ -64,7 +64,8 @@ public class EditorActivity extends ActivityWithMenuOptions {
.add("multipleEditSpans", this::multiple_edit_spans) .add("multipleEditSpans", this::multiple_edit_spans)
.add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin) .add("multipleEditSpansPlugin", this::multiple_edit_spans_plugin)
.add("pluginRequire", this::plugin_require) .add("pluginRequire", this::plugin_require)
.add("pluginNoDefaults", this::plugin_no_defaults); .add("pluginNoDefaults", this::plugin_no_defaults)
.add("heading", this::heading);
} }
@Override @Override
@ -317,6 +318,16 @@ public class EditorActivity extends ActivityWithMenuOptions {
editor, Executors.newSingleThreadExecutor(), editText)); editor, Executors.newSingleThreadExecutor(), editText));
} }
private void heading() {
final Markwon markwon = Markwon.create(this);
final MarkwonEditor editor = MarkwonEditor.builder(markwon)
.useEditHandler(new HeadingEditHandler())
.build();
editText.addTextChangedListener(MarkwonEditorTextWatcher.withPreRender(
editor, Executors.newSingleThreadExecutor(), editText));
}
private void initBottomBar() { private void initBottomBar() {
// all except block-quote wraps if have selection, or inserts at current cursor position // all except block-quote wraps if have selection, or inserts at current cursor position

View File

@ -0,0 +1,78 @@
package io.noties.markwon.sample.editor;
import android.text.Editable;
import android.text.Spanned;
import androidx.annotation.NonNull;
import io.noties.markwon.Markwon;
import io.noties.markwon.core.MarkwonTheme;
import io.noties.markwon.core.spans.HeadingSpan;
import io.noties.markwon.editor.EditHandler;
import io.noties.markwon.editor.PersistedSpans;
public class HeadingEditHandler implements EditHandler<HeadingSpan> {
private MarkwonTheme theme;
@Override
public void init(@NonNull Markwon markwon) {
this.theme = markwon.configuration().theme();
}
@Override
public void configurePersistedSpans(@NonNull PersistedSpans.Builder builder) {
builder
.persistSpan(Head1.class, () -> new Head1(theme))
.persistSpan(Head2.class, () -> new Head2(theme));
}
@Override
public void handleMarkdownSpan(
@NonNull PersistedSpans persistedSpans,
@NonNull Editable editable,
@NonNull String input,
@NonNull HeadingSpan span,
int spanStart,
int spanTextLength
) {
final Class<?> type;
switch (span.getLevel()) {
case 1: type = Head1.class; break;
case 2: type = Head2.class; break;
default:
type = null;
}
if (type != null) {
final int index = input.indexOf('\n', spanStart + spanTextLength);
final int end = index < 0
? input.length()
: index;
editable.setSpan(
persistedSpans.get(type),
spanStart,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
);
}
}
@NonNull
@Override
public Class<HeadingSpan> markdownSpanType() {
return HeadingSpan.class;
}
private static class Head1 extends HeadingSpan {
Head1(@NonNull MarkwonTheme theme) {
super(theme, 1);
}
}
private static class Head2 extends HeadingSpan {
Head2(@NonNull MarkwonTheme theme) {
super(theme, 2);
}
}
}

View File

@ -0,0 +1,95 @@
package io.noties.markwon.sample.precomputed;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import io.noties.markwon.Markwon;
import io.noties.markwon.PrecomputedFutureTextSetterCompat;
import io.noties.markwon.recycler.MarkwonAdapter;
import io.noties.markwon.sample.R;
public class PrecomputedFutureActivity extends Activity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_recycler);
final Markwon markwon = Markwon.builder(this)
.textSetter(PrecomputedFutureTextSetterCompat.create())
.build();
// create MarkwonAdapter and register two blocks that will be rendered differently
final MarkwonAdapter adapter = MarkwonAdapter.builder(R.layout.adapter_appcompat_default_entry, R.id.text)
.build();
final RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
recyclerView.setAdapter(adapter);
adapter.setMarkdown(markwon, loadReadMe(this));
// please note that we should notify updates (adapter doesn't do it implicitly)
adapter.notifyDataSetChanged();
}
@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;
}
}

View File

@ -0,0 +1,89 @@
package io.noties.markwon.sample.table;
import android.graphics.Color;
import android.os.Bundle;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.noties.debug.Debug;
import io.noties.markwon.Markwon;
import io.noties.markwon.ext.tables.TablePlugin;
import io.noties.markwon.ext.tables.TableTheme;
import io.noties.markwon.linkify.LinkifyPlugin;
import io.noties.markwon.sample.ActivityWithMenuOptions;
import io.noties.markwon.sample.MenuOptions;
import io.noties.markwon.sample.R;
import io.noties.markwon.utils.ColorUtils;
import io.noties.markwon.utils.Dip;
public class TableActivity extends ActivityWithMenuOptions {
@NonNull
@Override
public MenuOptions menuOptions() {
return MenuOptions.create()
.add("customize", this::customize)
.add("tableAndLinkify", this::tableAndLinkify);
}
private TextView textView;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_text_view);
textView = findViewById(R.id.text_view);
tableAndLinkify();
}
private void customize() {
final String md = "" +
"| HEADER | HEADER | HEADER |\n" +
"|:----:|:----:|:----:|\n" +
"| 测试 | 测试 | 测试 |\n" +
"| 测试 | 测试 | 测测测12345试测试测试 |\n" +
"| 测试 | 测试 | 123445 |\n" +
"| 测试 | 测试 | (650) 555-1212 |\n" +
"| 测试 | 测试 | [link](#) |\n";
final Markwon markwon = Markwon.builder(this)
.usePlugin(TablePlugin.create(builder -> {
final Dip dip = Dip.create(this);
builder
.tableBorderWidth(dip.toPx(2))
.tableBorderColor(Color.YELLOW)
.tableCellPadding(dip.toPx(4))
.tableHeaderRowBackgroundColor(ColorUtils.applyAlpha(Color.RED, 80))
.tableEvenRowBackgroundColor(ColorUtils.applyAlpha(Color.GREEN, 80))
.tableOddRowBackgroundColor(ColorUtils.applyAlpha(Color.BLUE, 80));
}))
.build();
markwon.setMarkdown(textView, md);
}
private void tableAndLinkify() {
final String md = "" +
"| HEADER | HEADER | HEADER |\n" +
"|:----:|:----:|:----:|\n" +
"| 测试 | 测试 | 测试 |\n" +
"| 测试 | 测试 | 测测测12345试测试测试 |\n" +
"| 测试 | 测试 | 123445 |\n" +
"| 测试 | 测试 | (650) 555-1212 |\n" +
"| 测试 | 测试 | [link](#) |\n" +
"\n" +
"测试\n" +
"\n" +
"[link link](https://link.link)";
final Markwon markwon = Markwon.builder(this)
.usePlugin(LinkifyPlugin.create())
.usePlugin(TablePlugin.create(this))
.build();
markwon.setMarkdown(textView, md);
}
}

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="16dip"
android:layout_marginRight="16dip"
android:lineSpacingExtra="2dip"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#000"
android:textSize="16sp"
tools:text="Hello" />

View File

@ -25,6 +25,8 @@
<string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string> <string name="sample_precomputed_text"># \# PrecomputedText\n\nUsage of TextSetter and PrecomputedTextCompat</string>
<string name="sample_precomputed_future_text"># \# PrecomputedFutureText\n\nUsage of TextSetter and PrecomputedFutureTextSetterCompat</string>
<string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string> <string name="sample_editor"># \# Editor\n\n`MarkwonEditor` sample usage to highlight user input in EditText</string>
<string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string> <string name="sample_inline_parser"># \# Inline Parser\n\nUsage of custom inline parser</string>
@ -37,4 +39,5 @@
<string name="sample_remote_views"># \# Notification\n\nExample usage in notifications and other remote views</string> <string name="sample_remote_views"># \# Notification\n\nExample usage in notifications and other remote views</string>
<string name="sample_table"># \# Table\n\nUsage of tables in a `TextView`</string>
</resources> </resources>