Working on new sample application

This commit is contained in:
Dimitry Ivanov 2018-12-28 18:58:33 +03:00
parent 0505845e4c
commit e01787982f
16 changed files with 617 additions and 4 deletions

View File

@ -68,6 +68,7 @@ ext {
'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0', 'okhttp' : 'com.squareup.okhttp3:okhttp:3.9.0',
'prism4j' : 'ru.noties:prism4j:1.1.0', 'prism4j' : 'ru.noties:prism4j:1.1.0',
'debug' : 'ru.noties:debug:3.0.0@jar', 'debug' : 'ru.noties:debug:3.0.0@jar',
'adapt' : 'ru.noties:adapt:1.1.0',
'dagger' : "com.google.dagger:dagger:$daggerVersion" 'dagger' : "com.google.dagger:dagger:$daggerVersion"
] ]

View File

@ -12,11 +12,23 @@ android {
versionCode 1 versionCode 1
versionName version versionName version
setProperty("archivesBaseName", "markwon-sample-$versionName") setProperty("archivesBaseName", "markwon-sample-$versionName")
resConfig 'en'
} }
lintOptions { lintOptions {
abortOnError false abortOnError false
} }
dexOptions {
preDexLibraries true
javaMaxHeapSize '5g'
}
compileOptions {
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_1_8
}
} }
dependencies { dependencies {
@ -31,12 +43,20 @@ dependencies {
implementation project(':markwon-syntax-highlight') implementation project(':markwon-syntax-highlight')
deps.with { deps.with {
implementation it['support-recycler-view']
implementation it['okhttp'] implementation it['okhttp']
implementation it['prism4j'] implementation it['prism4j']
implementation it['debug'] implementation it['debug']
implementation it['adapt']
} }
deps['annotationProcessor'].with { deps['annotationProcessor'].with {
annotationProcessor it['prism4j-bundler'] annotationProcessor it['prism4j-bundler']
} }
deps['test'].with {
testImplementation it['junit']
testImplementation it['robolectric']
testImplementation it['mockito']
}
} }

View File

@ -18,6 +18,10 @@
</intent-filter> </intent-filter>
</activity> </activity>
<activity android:name=".core.CoreActivity" />
<activity android:name=".latex.LatexActivity" />
<activity android:name=".customextension.CustomExtensionActivity" />
</application> </application>
</manifest> </manifest>

View File

@ -1,12 +1,96 @@
package ru.noties.markwon.sample; package ru.noties.markwon.sample;
import android.app.Activity; import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.VisibleForTesting;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import java.util.Arrays;
import ru.noties.adapt.Adapt;
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.core.CoreActivity;
import ru.noties.markwon.sample.customextension.CustomExtensionActivity;
import ru.noties.markwon.sample.latex.LatexActivity;
public class MainActivity extends Activity { public class MainActivity extends Activity {
static {
Debug.init(new AndroidLogDebugOutput(true));
}
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// obtain an instance of Markwon
// here we are creating as core markwon (no additional plugins are registered)
final Markwon markwon = Markwon.create(this);
final Adapt<SampleItem> adapt = Adapt.builder(SampleItem.class)
.include(SampleItem.class, new SampleItemView(markwon), new OnClickViewProcessor<SampleItem>() {
@Override
public void onClick(@NonNull SampleItem item, @NonNull View view) {
showSample(item);
}
})
.build();
adapt.setItems(Arrays.asList(SampleItem.values()));
final RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
recyclerView.setHasFixedSize(true);
recyclerView.addItemDecoration(createSampleItemDecoration());
recyclerView.setAdapter(adapt.recyclerViewAdapter());
}
@NonNull
private SampleItemDecoration createSampleItemDecoration() {
final float density = getResources().getDisplayMetrics().density;
return new SampleItemDecoration(
0xffeeeeee,
(int) (24 * density + .5F),
(int) (1 * density + .5F),
0xFFBDBDBD
);
}
private void showSample(@NonNull SampleItem item) {
startActivity(sampleItemIntent(this, item));
}
@VisibleForTesting
static Intent sampleItemIntent(@NonNull Context context, @NonNull SampleItem item) {
final Class<? extends Activity> activity;
switch (item) {
case CORE:
activity = CoreActivity.class;
break;
case LATEX:
activity = LatexActivity.class;
break;
case CUSTOM_EXTENSION:
activity = CustomExtensionActivity.class;
break;
default:
throw new IllegalStateException("No Activity is associated with sample-item: " + item);
}
return new Intent(context, activity);
} }
} }

View File

@ -0,0 +1,26 @@
package ru.noties.markwon.sample;
import android.support.annotation.StringRes;
public enum SampleItem {
// all usages of markwon without plugins (parse, render, setMarkwon, etc)
CORE(R.string.sample_core),
LATEX(R.string.sample_latex),
CUSTOM_EXTENSION(R.string.sample_custom_extension),
;
private final int textResId;
SampleItem(@StringRes int textResId) {
this.textResId = textResId;
}
@StringRes
public int textResId() {
return textResId;
}
}

View File

@ -0,0 +1,97 @@
package ru.noties.markwon.sample;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.annotation.ColorInt;
import android.support.annotation.Px;
import android.support.v7.widget.RecyclerView;
import android.view.View;
class SampleItemDecoration extends RecyclerView.ItemDecoration {
private final Rect rect = new Rect();
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final int oddItemBackgroundColor;
private final int bottomPadding;
private final int dividerHeight;
private final int dividerColor;
SampleItemDecoration(
@ColorInt int oddItemBackgroundColor,
@Px int bottomPadding,
@Px int dividerHeight,
@ColorInt int dividerColor) {
this.oddItemBackgroundColor = oddItemBackgroundColor;
this.bottomPadding = bottomPadding;
this.dividerHeight = dividerHeight;
this.dividerColor = dividerColor;
paint.setStyle(Paint.Style.FILL);
}
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
// if bottom < parent.getBottom() -> draw bottom background
paint.setColor(dividerColor);
View view;
// we will use this flag afterwards (if we will have to draw bottom background)
// so, if last item is even (no background) -> draw odd
// if last item is odd -> draw no background
//
// let's start with true, so if we have no items no background will be drawn
boolean isOdd = true;
for (int i = 0, count = parent.getChildCount(); i < count; i++) {
view = parent.getChildAt(i);
isOdd = parent.getChildAdapterPosition(view) % 2 != 0;
// odd
if (isOdd) {
rect.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
paint.setColor(oddItemBackgroundColor);
c.drawRect(rect, paint);
// set divider color back
paint.setColor(dividerColor);
}
rect.set(0, view.getBottom(), c.getWidth(), view.getBottom() + dividerHeight);
c.drawRect(rect, paint);
}
if (!isOdd && rect.bottom < parent.getBottom()) {
paint.setColor(oddItemBackgroundColor);
rect.set(0, rect.bottom, c.getWidth(), parent.getBottom());
c.drawRect(rect, paint);
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
// divider to bottom
// + {if last} -> bottomPadding
final int position = parent.getChildAdapterPosition(view);
final RecyclerView.Adapter<?> adapter = parent.getAdapter();
final boolean isLast = adapter != null && position == adapter.getItemCount() - 1;
final int bottom = isLast
? bottomPadding + dividerHeight
: dividerHeight;
outRect.set(0, 0, 0, bottom);
}
}

View File

@ -0,0 +1,84 @@
package ru.noties.markwon.sample;
import android.support.annotation.NonNull;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Spanned;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import java.util.EnumMap;
import ru.noties.adapt.Holder;
import ru.noties.adapt.ItemView;
import ru.noties.markwon.Markwon;
class SampleItemView extends ItemView<SampleItem, SampleItemView.SampleHolder> {
private final Markwon markwon;
// instance specific factory
private final NoCopySpannableFactory factory;
// instance specific cache
private final EnumMap<SampleItem, Spanned> cache;
SampleItemView(@NonNull Markwon markwon) {
this.markwon = markwon;
this.factory = new NoCopySpannableFactory();
this.cache = new EnumMap<>(SampleItem.class);
}
@NonNull
@Override
public SampleHolder createHolder(@NonNull LayoutInflater inflater, @NonNull ViewGroup parent) {
final SampleHolder holder = new SampleHolder(inflater.inflate(
R.layout.adapt_sample_item,
parent,
false));
// set Spannable.Factory so when TextView will receive a new content
// it won't create new Spannable and copy all the spans but instead
// re-use existing Spannable thus improving performance
holder.textView.setSpannableFactory(factory);
return holder;
}
@Override
public void bindHolder(@NonNull SampleHolder holder, @NonNull SampleItem item) {
// retrieve an item from cache or create new one
// simple lazy loading pattern (cache on first call then re-use)
Spanned spanned = cache.get(item);
if (spanned == null) {
spanned = markwon.toMarkdown(context(holder).getString(item.textResId()));
cache.put(item, spanned);
}
holder.textView.setText(spanned);
}
static class SampleHolder extends Holder {
final TextView textView;
SampleHolder(@NonNull View view) {
super(view);
this.textView = requireView(R.id.text);
}
}
private static class NoCopySpannableFactory extends Spannable.Factory {
@Override
public Spannable newSpannable(CharSequence source) {
return source instanceof Spannable
? (Spannable) source
: new SpannableString(source);
}
}
}

View File

@ -0,0 +1,208 @@
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 {
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();
step_6();
step_7();
}
/**
* Create a simple instance of Markwon with only Core plugin registered
* this will handle all _natively_ supported by commonmark-java nodes:
* <ul>
* <li>StrongEmphasis</li>
* <li>Emphasis</li>
* <li>BlockQuote</li>
* <li>Code</li>
* <li>FencedCodeBlock</li>
* <li>IndentedCodeBlock</li>
* <li>ListItem (bullet-list and ordered list</li>
* <li>Heading</li>
* <li>Link</li>
* <li>ThematicBreak</li>
* <li>Paragraph (please note that there is no default span for a paragraph registered)</li>
* </ul>
* <p>
* and basic core functionality:
* <ul>
* <li>Append text</li>
* <li>Insert new lines (soft and hard breaks)</li>
* </ul>
*/
private void step_1() {
// short call
final Markwon markwon = Markwon.create(this);
// this is the same as calling
final Markwon markwon2 = Markwon.builder(this)
.usePlugin(CorePlugin.create())
.build();
}
/**
* To simply apply raw (non-parsed) markdown call {@link Markwon#setMarkdown(TextView, String)}
*/
private void step_2() {
// this is raw markdown
final String markdown = "Hello **markdown**!";
final Markwon markwon = Markwon.create(this);
// this will parse raw markdown and set parsed content to specified TextView
markwon.setMarkdown(textView, markdown);
}
/**
* To apply markdown in a different context (other than textView) use {@link Markwon#toMarkdown(String)}
* <p>
* Please note that some features won't work unless they are used in a TextView context. For example
* there might be misplaced ordered lists (ordered list must have TextPaint in order to properly measure
* its number). But also images and tables (they belong to independent modules now). Images and tables
* are using some work-arounds in order to be displayed in relatively limited context without proper way
* of invalidation. But if a Toast for example is created with a custom view
* ({@code new Toast(this).setView(...) }) and has access to a TextView everything <em>should</em> work.
*/
private void step_3() {
final String markdown = "*Toast* __here__!\n\n> And a quote!";
final Markwon markwon = Markwon.create(this);
final Spanned spanned = markwon.toMarkdown(markdown);
Toast.makeText(this, spanned, Toast.LENGTH_LONG).show();
}
/**
* To apply already parsed markdown use {@link Markwon#setParsedMarkdown(TextView, Spanned)}
*/
private void step_4() {
final String markdown = "This **is** pre-parsed [markdown](#)";
final Markwon markwon = Markwon.create(this);
// parse markdown to obtain a Node
final Node node = markwon.parse(markdown);
// create a spanned content from parsed node
final Spanned spanned = markwon.render(node);
// 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,6 @@
package ru.noties.markwon.sample.customextension;
import android.app.Activity;
public class CustomExtensionActivity extends Activity {
}

View File

@ -0,0 +1,15 @@
package ru.noties.markwon.sample.latex;
import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.Nullable;
public class LatexActivity extends Activity {
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
}

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:overScrollMode="never"
android:scrollbars="vertical" />
</FrameLayout>

View File

@ -0,0 +1,13 @@
<?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:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:lineSpacingExtra="2dip"
android:padding="16dip"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="#212121"
android:textSize="17sp"
tools:text="# This is text" />

View File

@ -0,0 +1,6 @@
<resources>
<style name="BaseAppTheme" parent="android:Theme.Material.Light.DarkActionBar" />
</resources>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--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>
</resources>

View File

@ -1,8 +1,7 @@
<resources> <resources>
<!-- Base application theme. --> <style name="BaseAppTheme" parent="android:Theme.Holo.Light.DarkActionBar" />
<style name="AppTheme" parent="android:Theme.Material.Light.DarkActionBar">
<!-- Customize your theme here. --> <style name="AppTheme" parent="BaseAppTheme" />
</style>
</resources> </resources>

View File

@ -0,0 +1,25 @@
package ru.noties.markwon.sample;
import android.content.Context;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import static org.junit.Assert.assertNotNull;
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
public class MainActivityTest {
@Test
public void all_sample_items_have_activity_associated() {
final Context context = RuntimeEnvironment.application;
for (SampleItem item : SampleItem.values()) {
// we assert as not null, but in case of an error this method should throw
assertNotNull(MainActivity.sampleItemIntent(context, item));
}
}
}