Working with sample application

This commit is contained in:
Dimitry Ivanov 2019-01-02 22:49:54 +03:00
parent e01787982f
commit 6f8c1dfaee
25 changed files with 904 additions and 106 deletions

View File

@ -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<SimpleEntry.Holder, Node> {
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<Node, Spanned> cache = new HashMap<>();
private final int layoutResId;
@ -60,15 +64,15 @@ public class SimpleEntry implements MarkwonAdapter.Entry<SimpleEntry.Holder, Nod
cache.clear();
}
static class Holder extends MarkwonAdapter.Holder {
public static class Holder extends MarkwonAdapter.Holder {
final TextView textView;
Holder(@NonNull View itemView) {
protected Holder(@NonNull View itemView) {
super(itemView);
this.textView = requireView(R.id.text);
this.textView.setSpannableFactory(FACTORY);
this.textView.setSpannableFactory(NO_COPY_SPANNABLE_FACTORY);
}
}

View File

@ -170,6 +170,8 @@ class MarkwonBuilderImpl implements Markwon.Builder {
}
}
// important thing here is to check if corePlugin is added
// add it _only_ if it's not present
if (hasCoreDependents && !hasCore) {
final List<MarkwonPlugin> out = new ArrayList<>(plugins.size() + 1);
// add default instance of CorePlugin

View File

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

View File

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

View File

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

View File

@ -1,21 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingLeft="16dip"
android:paddingTop="8dip"
android:paddingRight="16dip"
android:paddingBottom="8dip">
<!--<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"-->
<!--xmlns:app="http://schemas.android.com/apk/res-auto"-->
<!--android:layout_width="match_parent"-->
<!--android:layout_height="wrap_content"-->
<!--android:clipChildren="false"-->
<!--android:clipToPadding="false"-->
<!--android:fillViewport="true"-->
<!--android:paddingLeft="16dip"-->
<!--android:paddingTop="8dip"-->
<!--android:paddingRight="16dip"-->
<!--android:paddingBottom="8dip">-->
<ru.noties.markwon.sample.extension.recycler.TableEntryView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/table_entry"
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:tev_debugData="head1,head2,head3|col1,col2,col3|col1,col2,col3|col1,col2,col3"
app:tev_rowEvenBackgroundColor="#40ff0000" />
</HorizontalScrollView>
<!--</HorizontalScrollView>-->

View File

@ -3,6 +3,8 @@
<declare-styleable name="TableEntryView">
<attr name="tev_rowEvenBackgroundColor" format="color" />
<attr name="tev_borderColor" format="color" />
<attr name="tev_borderWidth" format="dimension" />
<attr name="tev_debugData" format="string" />
</declare-styleable>
</resources>

View File

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

View File

@ -3,6 +3,8 @@
xmlns:tools="http://schemas.android.com/tools"
package="ru.noties.markwon.sample">
<uses-permission android:name="android.permission.INTERNET" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
@ -21,6 +23,8 @@
<activity android:name=".core.CoreActivity" />
<activity android:name=".latex.LatexActivity" />
<activity android:name=".customextension.CustomExtensionActivity" />
<activity android:name=".basicplugins.BasicPluginsActivity" />
<activity android:name=".recycler.RecyclerActivity" />
</application>

View File

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

View File

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

View File

@ -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).
* <p>
* Please note that when a plugin is registered and it <em>depends</em> 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()}).
* <p>
* 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 <em>utilities</em>:
* <ul>
* <li>SyntaxHighlight</li>
* <li>LinkSpan.Resolver</li>
* <li>UrlProcessor</li>
* <li>ImageSizeResolver</li>
* </ul>
* <p>
* 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
}

View File

@ -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).
* <p>
* Please note that when a plugin is registered and it <em>depends</em> 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()}).
* <p>
* 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);
}
}

View File

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

View File

@ -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<TableEntry.TableNodeHolder, TableBlock> {
private final Map<TableBlock, Table> 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);
}
}
}

View File

@ -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<Table.Row> rows = new ArrayList<>();
for (String row : data.split("\\|")) {
final List<Table.Column> 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<Table.Row> 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<Table.Column> 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;
}
}

View File

@ -0,0 +1 @@
../../../../../README.md

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
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" />

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingLeft="16dip"
android:paddingRight="16dip"
android:scrollbarStyle="outsideInset">
<TextView
android:id="@+id/text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:lineSpacingExtra="2dip"
android:paddingTop="8dip"
android:paddingBottom="8dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textSize="16sp"
tools:text="# Hello there! and taskjs" />
</HorizontalScrollView>

View File

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<HorizontalScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingLeft="16dip"
android:paddingTop="8dip"
android:paddingRight="16dip"
android:paddingBottom="8dip"
android:scrollbarStyle="outsideInset">
<ru.noties.markwon.sample.recycler.TableEntryView
android:id="@+id/table_entry"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:tev_debugData="head1,head2,head3|col1,col2,col3|col1,col2,col3|col1,col2,col3"
app:tev_rowEvenBackgroundColor="#40ff0000" />
</HorizontalScrollView>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="0px"
android:layout_height="match_parent"
android:layout_weight="1"
android:padding="4dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#000"
android:textSize="16sp"
tools:text="Table content" />

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" />

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="TableEntryView">
<attr name="tev_rowEvenBackgroundColor" format="color" />
<attr name="tev_borderColor" format="color" />
<attr name="tev_borderWidth" format="dimension" />
<attr name="tev_debugData" format="string" />
</declare-styleable>
</resources>

View File

@ -4,7 +4,15 @@
<!--Ignore missing translation warning-->
<string name="sample_core"># \# Core\n\nSimple usage example</string>
<string name="sample_latex"># \# LaTeX\n\nShows how to display a **LaTeX** formula in a Markwon powered application</string>
<string name="sample_custom_extension"># \# Custom extension\n\nShows how to create a custom extension to display an icon referenced in markdown as `@ic-android-black-24`</string>
<string name="sample_basic_plugins"># \# Basic plugins\n\nShows basic usage of plugins</string>
<string name="sample_latex"># \# LaTeX\n\nShows how to display a **LaTeX** formula</string>
<string name="sample_custom_extension"># \# Custom extension\n\nShows how to create a custom
extension to display an icon referenced in markdown as `@ic-android-black-24`</string>
<string name="sample_recycler"># \# Recycler\n\nShow how to render markdown in a RecyclerView.
Renders code blocks wrapped in a HorizontalScrollView. Renders tables in a custom view.</string>
</resources>