Initial commit (migrating from testing project to library)
This commit is contained in:
commit
3e9ff80da1
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
*.iml
|
||||||
|
.gradle
|
||||||
|
/local.properties
|
||||||
|
/.idea
|
||||||
|
.DS_Store
|
||||||
|
/build
|
||||||
|
/captures
|
||||||
|
.externalNativeBuild
|
||||||
|
gradlew
|
||||||
|
gradlew.bat
|
||||||
|
**/build
|
32
app/build.gradle
Normal file
32
app/build.gradle
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
apply plugin: 'com.android.application'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion 25
|
||||||
|
buildToolsVersion "25.0.2"
|
||||||
|
defaultConfig {
|
||||||
|
applicationId "ru.noties.markwon"
|
||||||
|
minSdkVersion 15
|
||||||
|
targetSdkVersion 25
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0"
|
||||||
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled false
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
|
||||||
|
exclude group: 'com.android.support', module: 'support-annotations'
|
||||||
|
})
|
||||||
|
compile 'com.android.support:appcompat-v7:25.3.1'
|
||||||
|
compile 'com.atlassian.commonmark:commonmark:0.9.0'
|
||||||
|
compile 'com.atlassian.commonmark:commonmark-ext-gfm-strikethrough:0.9.0'
|
||||||
|
compile 'ru.noties:debug:3.0.0@jar'
|
||||||
|
testCompile 'junit:junit:4.12'
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumentation test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("ru.noties.markwon", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
20
app/src/main/AndroidManifest.xml
Normal file
20
app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="ru.noties.markwon">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true"
|
||||||
|
android:theme="@style/AppTheme">
|
||||||
|
<activity android:name=".MainActivity">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
63
app/src/main/assets/test.md
Normal file
63
app/src/main/assets/test.md
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
# Hello!
|
||||||
|
|
||||||
|
**bold *italic*** _just italic_
|
||||||
|
|
||||||
|
[](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%22scrollable%22)
|
||||||
|
|
||||||
|
> Quote
|
||||||
|
>> Second Quote
|
||||||
|
>>> Third one, yuhuu!
|
||||||
|
|
||||||
|
`can a code have **markdown?**` so go it doesn't
|
||||||
|
|
||||||
|
|
||||||
|
## Unordered list
|
||||||
|
|
||||||
|
* first
|
||||||
|
* second
|
||||||
|
* * second first
|
||||||
|
* * second __second__ jks8feif fdsuif yuweru sdfoisdfu wutwe iower wtew ruweir weoir wutywr wer woeirwr wieyriow eryowe rwyeor oweryower o
|
||||||
|
* third `and some code`
|
||||||
|
|
||||||
|
|
||||||
|
1. okay
|
||||||
|
2. okay 2
|
||||||
|
1. okay again
|
||||||
|
* it's also here
|
||||||
|
2. and this
|
||||||
|
3. and that
|
||||||
|
|
||||||
|
### Quoted list
|
||||||
|
|
||||||
|
> * first
|
||||||
|
> * second
|
||||||
|
> * third
|
||||||
|
> * * third first
|
||||||
|
>> * yo #1
|
||||||
|
>> * yo #2
|
||||||
|
|
||||||
|
|
||||||
|
<b>j<i><del>o</del></i></b>
|
||||||
|
|
||||||
|
|
||||||
|
#### Code block
|
||||||
|
|
||||||
|
```java
|
||||||
|
final String s = "this id code block";
|
||||||
|
s.length() > 0;
|
||||||
|
```
|
||||||
|
---
|
||||||
|
okay, have a good day!
|
||||||
|
|
||||||
|
Yo<sup>**2**<sup>22</sup><sub>42</sub></sup>
|
||||||
|
|
||||||
|
To compare<sub>~~13~~</sub>
|
||||||
|
|
||||||
|
~~Just strike it~~
|
||||||
|
|
||||||
|
<br /><br /><br /><br />
|
||||||
|
|
||||||
|
<font color="#FF0000">RED</font>
|
||||||
|
|
||||||
|
**PS** additional text to check if this view scrolls gracefully, sofihweo fwfw fuwf weyf pwefiowef twe weuifphw efwepfuwoefh wfypiwe fuwoef wiefg wtefw uf ywfyw fweouf wpfyw fwfe# Hello!
|
||||||
|
|
88
app/src/main/java/ru/noties/markwon/MainActivity.java
Normal file
88
app/src/main/java/ru/noties/markwon/MainActivity.java
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.text.style.StrikethroughSpan;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.commonmark.ext.gfm.strikethrough.StrikethroughExtension;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.parser.Parser;
|
||||||
|
import ru.noties.debug.AndroidLogDebugOutput;
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
import ru.noties.markwon.spans.DrawableSpanUtils;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Scanner;
|
||||||
|
|
||||||
|
public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
static {
|
||||||
|
Debug.init(new AndroidLogDebugOutput(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_main);
|
||||||
|
|
||||||
|
final TextView textView = (TextView) findViewById(R.id.activity_main);
|
||||||
|
|
||||||
|
new Thread(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
InputStream stream = null;
|
||||||
|
Scanner scanner = null;
|
||||||
|
String md = null;
|
||||||
|
try {
|
||||||
|
stream = getAssets().open("test.md");
|
||||||
|
scanner = new Scanner(stream).useDelimiter("\\A");
|
||||||
|
if (scanner.hasNext()) {
|
||||||
|
md = scanner.next();
|
||||||
|
}
|
||||||
|
} catch (Throwable t) {
|
||||||
|
Debug.e(t);
|
||||||
|
} finally {
|
||||||
|
if (stream != null) {
|
||||||
|
try { stream.close(); } catch (IOException e) {}
|
||||||
|
}
|
||||||
|
if (scanner != null) {
|
||||||
|
scanner.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (md != null) {
|
||||||
|
final long start = SystemClock.uptimeMillis();
|
||||||
|
final Parser parser = new Parser.Builder()
|
||||||
|
.extensions(Arrays.asList(StrikethroughExtension.create()))
|
||||||
|
.build();
|
||||||
|
final Node node = parser.parse(md);
|
||||||
|
final CharSequence text = new SpannableRenderer()._render(node/*, new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
textView.setText(textView.getText());
|
||||||
|
final Drawable drawable = null;
|
||||||
|
drawable.setCallback(textView);
|
||||||
|
}
|
||||||
|
}*/);
|
||||||
|
final long end = SystemClock.uptimeMillis();
|
||||||
|
Debug.i("Rendered: %d ms, length: %d", end - start, text.length());
|
||||||
|
textView.post(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
// NB! LinkMovementMethod forces frequent updates...
|
||||||
|
// textView.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
textView.setText(text);
|
||||||
|
DrawableSpanUtils.scheduleDrawables(textView);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
7
app/src/main/java/ru/noties/markwon/Markwon.java
Normal file
7
app/src/main/java/ru/noties/markwon/Markwon.java
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
public class Markwon {
|
||||||
|
|
||||||
|
// todo, annotation processor to PRE_COMPILE markdown!! no... multiple lnguages and you are out, forget about it
|
||||||
|
// view for debugging (to view in preview) x3!
|
||||||
|
}
|
442
app/src/main/java/ru/noties/markwon/SpannableRenderer.java
Normal file
442
app/src/main/java/ru/noties/markwon/SpannableRenderer.java
Normal file
@ -0,0 +1,442 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.ColorFilter;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.support.annotation.IntRange;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.style.AbsoluteSizeSpan;
|
||||||
|
import android.text.style.StrikethroughSpan;
|
||||||
|
import android.text.style.URLSpan;
|
||||||
|
|
||||||
|
import org.commonmark.ext.gfm.strikethrough.Strikethrough;
|
||||||
|
import org.commonmark.node.AbstractVisitor;
|
||||||
|
import org.commonmark.node.BlockQuote;
|
||||||
|
import org.commonmark.node.BulletList;
|
||||||
|
import org.commonmark.node.Code;
|
||||||
|
import org.commonmark.node.CustomBlock;
|
||||||
|
import org.commonmark.node.CustomNode;
|
||||||
|
import org.commonmark.node.Document;
|
||||||
|
import org.commonmark.node.Emphasis;
|
||||||
|
import org.commonmark.node.FencedCodeBlock;
|
||||||
|
import org.commonmark.node.HardLineBreak;
|
||||||
|
import org.commonmark.node.Heading;
|
||||||
|
import org.commonmark.node.HtmlBlock;
|
||||||
|
import org.commonmark.node.HtmlInline;
|
||||||
|
import org.commonmark.node.Image;
|
||||||
|
import org.commonmark.node.IndentedCodeBlock;
|
||||||
|
import org.commonmark.node.Link;
|
||||||
|
import org.commonmark.node.ListItem;
|
||||||
|
import org.commonmark.node.Node;
|
||||||
|
import org.commonmark.node.OrderedList;
|
||||||
|
import org.commonmark.node.Paragraph;
|
||||||
|
import org.commonmark.node.SoftLineBreak;
|
||||||
|
import org.commonmark.node.StrongEmphasis;
|
||||||
|
import org.commonmark.node.Text;
|
||||||
|
import org.commonmark.node.ThematicBreak;
|
||||||
|
import org.commonmark.renderer.Renderer;
|
||||||
|
|
||||||
|
import java.util.ArrayDeque;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Deque;
|
||||||
|
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
import ru.noties.markwon.spans.BlockQuoteSpan;
|
||||||
|
import ru.noties.markwon.spans.CodeSpan;
|
||||||
|
import ru.noties.markwon.spans.DrawableSpan;
|
||||||
|
import ru.noties.markwon.spans.EmphasisSpan;
|
||||||
|
import ru.noties.markwon.spans.ListItemSpan;
|
||||||
|
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||||
|
import ru.noties.markwon.spans.SubSpan;
|
||||||
|
import ru.noties.markwon.spans.SupSpan;
|
||||||
|
import ru.noties.markwon.spans.ThematicBreakSpan;
|
||||||
|
|
||||||
|
public class SpannableRenderer implements Renderer {
|
||||||
|
|
||||||
|
// todo, util to extract all drawables and attach to textView (gif, animations, lazyLoading, etc)
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(Node node, Appendable output) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String render(Node node) {
|
||||||
|
// hm.. doesn't make sense to render to string
|
||||||
|
throw null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public CharSequence _render(Node node) {
|
||||||
|
final SpannableStringBuilder builder = new SpannableStringBuilder();
|
||||||
|
node.accept(new SpannableNodeRenderer(builder));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class SpannableNodeRenderer extends AbstractVisitor {
|
||||||
|
|
||||||
|
// private static final float[] HEADING_SIZES = {
|
||||||
|
// 1.5f, 1.4f, 1.3f, 1.2f, 1.1f, 1f,
|
||||||
|
// };
|
||||||
|
|
||||||
|
private final SpannableStringBuilder builder;
|
||||||
|
|
||||||
|
private int blockQuoteIndent;
|
||||||
|
private int listLevel;
|
||||||
|
|
||||||
|
SpannableNodeRenderer(SpannableStringBuilder builder) {
|
||||||
|
this.builder = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(HardLineBreak hardLineBreak) {
|
||||||
|
Debug.i(hardLineBreak);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Text text) {
|
||||||
|
Debug.i(text);
|
||||||
|
builder.append(text.getLiteral());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(StrongEmphasis strongEmphasis) {
|
||||||
|
final int length = builder.length();
|
||||||
|
visitChildren(strongEmphasis);
|
||||||
|
builder.setSpan(new StrongEmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Emphasis emphasis) {
|
||||||
|
final int length = builder.length();
|
||||||
|
visitChildren(emphasis);
|
||||||
|
builder.setSpan(new EmphasisSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(IndentedCodeBlock indentedCodeBlock) {
|
||||||
|
Debug.i(indentedCodeBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BlockQuote blockQuote) {
|
||||||
|
builder.append('\n');
|
||||||
|
final int length = builder.length();
|
||||||
|
blockQuoteIndent += 1;
|
||||||
|
visitChildren(blockQuote);
|
||||||
|
builder.setSpan(new BlockQuoteSpan(blockQuoteIndent), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
blockQuoteIndent -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Code code) {
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append(code.getLiteral());
|
||||||
|
// builder.setSpan(new ForegroundColorSpan(0xff00ff00), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
builder.setSpan(new CodeSpan(false, length, builder.length()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(BulletList bulletList) {
|
||||||
|
Debug.i(bulletList, bulletList.getBulletMarker());
|
||||||
|
visitChildren(bulletList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ListItem listItem) {
|
||||||
|
Debug.i(listItem);
|
||||||
|
// builder.append('\n');
|
||||||
|
if (builder.charAt(builder.length() - 1) != '\n') {
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
final int length = builder.length();
|
||||||
|
blockQuoteIndent += 1;
|
||||||
|
listLevel += 1;
|
||||||
|
visitChildren(listItem);
|
||||||
|
// builder.setSpan(new BulletSpan(4, 0xff0000ff), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
builder.setSpan(new ListItemSpan(blockQuoteIndent, listLevel > 1, length), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
blockQuoteIndent -= 1;
|
||||||
|
listLevel -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(ThematicBreak thematicBreak) {
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append('\n')
|
||||||
|
.append(' '); // without space it won't render
|
||||||
|
builder.setSpan(new ThematicBreakSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(OrderedList orderedList) {
|
||||||
|
Debug.i(orderedList, orderedList.getDelimiter(), orderedList.getStartNumber());
|
||||||
|
// todo, ordering numbers
|
||||||
|
super.visit(orderedList);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(SoftLineBreak softLineBreak) {
|
||||||
|
Debug.i(softLineBreak);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Heading heading) {
|
||||||
|
Debug.i(heading);
|
||||||
|
if (builder.length() != 0 && builder.charAt(builder.length() - 1) != '\n') {
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
final int length = builder.length();
|
||||||
|
visitChildren(heading);
|
||||||
|
final int max = 120;
|
||||||
|
final int one = 20; // total is 6
|
||||||
|
final int size = max - ((heading.getLevel() - 1) * one);
|
||||||
|
builder.setSpan(new AbsoluteSizeSpan(size), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
builder.append('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(FencedCodeBlock fencedCodeBlock) {
|
||||||
|
builder.append('\n');
|
||||||
|
final int length = builder.length();
|
||||||
|
builder.append(fencedCodeBlock.getLiteral());
|
||||||
|
builder.setSpan(new CodeSpan(true, length, builder.length() - 1), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Paragraph paragraph) {
|
||||||
|
Debug.i(paragraph);
|
||||||
|
if (listLevel == 0
|
||||||
|
&& blockQuoteIndent == 0) {
|
||||||
|
builder.append('\n')
|
||||||
|
.append('\n');
|
||||||
|
}
|
||||||
|
visitChildren(paragraph);
|
||||||
|
|
||||||
|
if (listLevel == 0
|
||||||
|
&& blockQuoteIndent == 0) {
|
||||||
|
builder.append('\n')
|
||||||
|
.append('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private int htmlStart = -1;
|
||||||
|
private final Deque<HtmlInlineItem> htmlStack = new ArrayDeque<>();
|
||||||
|
|
||||||
|
private static class HtmlInlineItem {
|
||||||
|
|
||||||
|
final int start;
|
||||||
|
final String tag;
|
||||||
|
|
||||||
|
private HtmlInlineItem(int start, String tag) {
|
||||||
|
this.start = start;
|
||||||
|
this.tag = tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(HtmlInline htmlInline) {
|
||||||
|
|
||||||
|
// Debug.i(htmlInline, htmlStart);
|
||||||
|
// Debug.i(htmlInline.getLiteral(), htmlInline.toString());
|
||||||
|
|
||||||
|
// okay, it's seems that we desperately need to understand if it's opening tag or closing
|
||||||
|
|
||||||
|
final HtmlTag tag = parseTag(htmlInline.getLiteral());
|
||||||
|
|
||||||
|
Debug.i(htmlInline.getLiteral(), tag);
|
||||||
|
|
||||||
|
if (tag != null) {
|
||||||
|
Debug.i("tag: %s, closing: %s", tag.tag, tag.closing);
|
||||||
|
if (!tag.closing) {
|
||||||
|
htmlStack.push(new HtmlInlineItem(builder.length(), tag.tag));
|
||||||
|
visitChildren(htmlInline);
|
||||||
|
} else {
|
||||||
|
final HtmlInlineItem item = htmlStack.pop();
|
||||||
|
final int start = item.start;
|
||||||
|
final int end = builder.length();
|
||||||
|
// here, additionally, we can render some tags ourselves (sup/sub)
|
||||||
|
if ("sup".equals(item.tag)) {
|
||||||
|
builder.setSpan(new SupSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
} else if("sub".equals(item.tag)) {
|
||||||
|
builder.setSpan(new SubSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
} else if("del".equals(item.tag)) {
|
||||||
|
// weird, but `Html` class does not return a spannable for `<del>o</del>`
|
||||||
|
// seems like a bug
|
||||||
|
builder.setSpan(new StrikethroughSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
} else {
|
||||||
|
final String html = "<" + item.tag + ">" + (builder.subSequence(start, end).toString()) + "</" + item.tag + ">";
|
||||||
|
final Spanned spanned = Html.fromHtml(html);
|
||||||
|
final Object[] spans = spanned.getSpans(0, spanned.length(), Object.class);
|
||||||
|
|
||||||
|
Debug.i("html: %s, start: %d, end: %d, spans: %s", html, start, end, Arrays.toString(spans));
|
||||||
|
|
||||||
|
if (spans != null
|
||||||
|
&& spans.length > 0) {
|
||||||
|
for (Object span: spans) {
|
||||||
|
Debug.i(span);
|
||||||
|
builder.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
super.visit(htmlInline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class HtmlTag {
|
||||||
|
final String tag;
|
||||||
|
final boolean closing;
|
||||||
|
HtmlTag(String tag, boolean closing) {
|
||||||
|
this.tag = tag;
|
||||||
|
this.closing = closing;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "HtmlTag{" +
|
||||||
|
"tag='" + tag + '\'' +
|
||||||
|
", closing=" + closing +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HtmlTag parseTag(String in) {
|
||||||
|
|
||||||
|
final HtmlTag out;
|
||||||
|
|
||||||
|
final int length = in != null
|
||||||
|
? in.length()
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
Debug.i(in, length);
|
||||||
|
|
||||||
|
if (length == 0 || length < 3) {
|
||||||
|
out = null;
|
||||||
|
} else {
|
||||||
|
|
||||||
|
final boolean closing = '<' == in.charAt(0) && '/' == in.charAt(1);
|
||||||
|
final String tag = closing
|
||||||
|
? in.substring(2, in.length() - 1)
|
||||||
|
: in.substring(1, in.length() - 1);
|
||||||
|
out = new HtmlTag(tag, closing);
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(HtmlBlock htmlBlock) {
|
||||||
|
// interestring thing... what is it also?
|
||||||
|
Debug.i(htmlBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(CustomBlock customBlock) {
|
||||||
|
// not supported, what is it anyway?
|
||||||
|
Debug.i(customBlock);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Document document) {
|
||||||
|
// the whole document, no need to do anything
|
||||||
|
Debug.i(document);
|
||||||
|
super.visit(document);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Link link) {
|
||||||
|
Debug.i(link);
|
||||||
|
final int length = builder.length();
|
||||||
|
visitChildren(link);
|
||||||
|
builder.setSpan(new URLSpan(link.getDestination()), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(Image image) {
|
||||||
|
// not supported... maybe for now?
|
||||||
|
Debug.i(image);
|
||||||
|
super.visit(image);
|
||||||
|
|
||||||
|
final int length = builder.length();
|
||||||
|
final TestDrawable drawable = new TestDrawable();
|
||||||
|
final DrawableSpan span = new DrawableSpan(drawable);
|
||||||
|
builder.append(" ");
|
||||||
|
builder.setSpan(span, length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void visit(CustomNode customNode) {
|
||||||
|
|
||||||
|
Debug.i(customNode);
|
||||||
|
|
||||||
|
if (customNode instanceof Strikethrough) {
|
||||||
|
final int length = builder.length();
|
||||||
|
visitChildren(customNode);
|
||||||
|
builder.setSpan(new StrikethroughSpan(), length, builder.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
} else {
|
||||||
|
super.visit(customNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static class TestDrawable extends Drawable {
|
||||||
|
|
||||||
|
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
private boolean called;
|
||||||
|
|
||||||
|
TestDrawable() {
|
||||||
|
setBounds(0, 0, 50, 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(@NonNull final Canvas canvas) {
|
||||||
|
canvas.clipRect(getBounds());
|
||||||
|
if (!called) {
|
||||||
|
canvas.drawColor(0xFF00ff00);
|
||||||
|
handler.removeCallbacksAndMessages(null);
|
||||||
|
handler.postDelayed(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
called = true;
|
||||||
|
setBounds(0, 0, 400, 400);
|
||||||
|
invalidateSelf();
|
||||||
|
}
|
||||||
|
}, 2000L);
|
||||||
|
} else {
|
||||||
|
canvas.drawColor(0xFFff0000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getOpacity() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicWidth() {
|
||||||
|
return called ? 400 : 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getIntrinsicHeight() {
|
||||||
|
return called ? 400 : 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
|
public class BlockQuoteSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
private final int indent;
|
||||||
|
|
||||||
|
public BlockQuoteSpan(int indent) {
|
||||||
|
this.indent = indent;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLeadingMargin(boolean first) {
|
||||||
|
return 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||||
|
// Debug.i("x: %d, dir: %d, top: %d, baseline: %d, bottom: %d, first: %s",
|
||||||
|
// x, dir, top, baseline, bottom, first
|
||||||
|
// );
|
||||||
|
|
||||||
|
final int save = c.save();
|
||||||
|
try {
|
||||||
|
final int left = 24 * (indent - 1);
|
||||||
|
// final RectF rectF = new RectF(0, 0, 16, 16);
|
||||||
|
final Rect rect = new Rect(left, top, left + 8, bottom);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
paint.setColor(0xFFf0f0f0);
|
||||||
|
c.drawRect(rect, paint);
|
||||||
|
// c.translate(x, .0F);
|
||||||
|
// c.drawOval(rectF, paint);
|
||||||
|
} finally {
|
||||||
|
c.restoreToCount(save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
199
app/src/main/java/ru/noties/markwon/spans/CodeSpan.java
Normal file
199
app/src/main/java/ru/noties/markwon/spans/CodeSpan.java
Normal file
@ -0,0 +1,199 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.support.annotation.IntRange;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.StaticLayout;
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
import android.text.style.LineHeightSpan;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
|
// we will use Replacement span because code blocks cannot contain other markdown
|
||||||
|
// so we will render the string (not a charSequence with possible metric affecting spans)
|
||||||
|
public class CodeSpan extends ReplacementSpan/* implements LeadingMarginSpan*/ {
|
||||||
|
|
||||||
|
private final boolean multiline;
|
||||||
|
private final int start;
|
||||||
|
private final int end;
|
||||||
|
|
||||||
|
public CodeSpan(boolean multiline, int start, int end) {
|
||||||
|
this.multiline = multiline;
|
||||||
|
this.start = start;
|
||||||
|
this.end = end;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize(
|
||||||
|
@NonNull Paint paint,
|
||||||
|
CharSequence text,
|
||||||
|
@IntRange(from = 0) int start,
|
||||||
|
@IntRange(from = 0) int end,
|
||||||
|
@Nullable Paint.FontMetricsInt fm
|
||||||
|
) {
|
||||||
|
|
||||||
|
final CharSequence cs = text.subSequence(start, end);
|
||||||
|
final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F);
|
||||||
|
|
||||||
|
// final StaticLayout layout = new StaticLayout(cs, new TextPaint(paint), 10000, Layout.Alignment.ALIGN_NORMAL, 1.F, .0F, false);
|
||||||
|
// final float width = layout.getLineWidth(0);
|
||||||
|
// final int out = 32 + (int) (width + .5F);
|
||||||
|
|
||||||
|
// Debug.i("text: %s, width: %s", cs, width);
|
||||||
|
|
||||||
|
if (fm != null) {
|
||||||
|
// we add a padding top & bottom
|
||||||
|
Debug.i("a: %s, d: %s, t: %s, b: %s", fm.ascent, fm.descent, fm.top, fm.bottom);
|
||||||
|
final float ratio = .62F; // golden ratio
|
||||||
|
fm.ascent = fm.ascent - 8;
|
||||||
|
fm.descent = (int) (-fm.ascent * ratio);
|
||||||
|
fm.top = fm.ascent;
|
||||||
|
fm.bottom = fm.descent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return width;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(
|
||||||
|
@NonNull Canvas canvas,
|
||||||
|
CharSequence text,
|
||||||
|
@IntRange(from = 0) int start,
|
||||||
|
@IntRange(from = 0) int end,
|
||||||
|
float x,
|
||||||
|
int top,
|
||||||
|
int y,
|
||||||
|
int bottom,
|
||||||
|
@NonNull Paint paint
|
||||||
|
) {
|
||||||
|
|
||||||
|
Debug.i("text: %s, x: %s, top: %s, y: %s, bottom: %s", text.subSequence(start, end), x, top, y, bottom);
|
||||||
|
|
||||||
|
final CharSequence cs = text.subSequence(start, end);
|
||||||
|
|
||||||
|
final int width = 32 + (int) (paint.measureText(cs, 0, cs.length()) + .5F);
|
||||||
|
|
||||||
|
final int left = (int) (x + .5F);
|
||||||
|
final int right = multiline
|
||||||
|
? canvas.getWidth()
|
||||||
|
: left + width;
|
||||||
|
|
||||||
|
final Rect rect = new Rect(
|
||||||
|
left,
|
||||||
|
top,
|
||||||
|
right,
|
||||||
|
bottom
|
||||||
|
);
|
||||||
|
|
||||||
|
final Paint p = new Paint();
|
||||||
|
p.setStyle(Paint.Style.FILL);
|
||||||
|
p.setColor(0x80ff0000);
|
||||||
|
canvas.drawRect(rect, p);
|
||||||
|
|
||||||
|
// y center position
|
||||||
|
final int b = bottom - ((bottom - top) / 2) - (int) ((paint.descent() + paint.ascent()) / 2);
|
||||||
|
p.setColor(0xFF000000);
|
||||||
|
canvas.drawText(cs, 0, cs.length(), x + 16, b, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public int getLeadingMargin(boolean first) {
|
||||||
|
// return 1;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// @Override
|
||||||
|
// public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||||
|
//// Debug.i("x: %d, top: %d, bottom: %d", x, top, bottom);
|
||||||
|
//
|
||||||
|
//// Debug.i("this: [%d, %d], came: [%d, %d]", this.start, this.end, start, end);
|
||||||
|
// Debug.i("x: %d, canvas: [%d-%d], text: %s", x, c.getWidth(), c.getHeight(), (text.subSequence(start, end)));
|
||||||
|
//
|
||||||
|
// // the thing is... if we do not draw, then text won't be drawn also
|
||||||
|
// final Rect rect = new Rect();
|
||||||
|
//
|
||||||
|
// final Paint paint = new Paint();
|
||||||
|
// paint.setStyle(Paint.Style.FILL);
|
||||||
|
// paint.setColor(0xffcccccc);
|
||||||
|
//
|
||||||
|
// rect.set(x, top, c.getWidth(), bottom);
|
||||||
|
// c.drawRect(rect, paint);
|
||||||
|
//
|
||||||
|
// if (this.start == start) {
|
||||||
|
// this.top = top;
|
||||||
|
//
|
||||||
|
//// final int save = c.save();
|
||||||
|
//// try {
|
||||||
|
//// c.drawColor(0x00ffffff);
|
||||||
|
//// } finally {
|
||||||
|
//// c.restoreToCount(save);
|
||||||
|
//// }
|
||||||
|
//
|
||||||
|
//// c.drawColor(0x00ffffff);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (this.end == end) {
|
||||||
|
// // draw borders
|
||||||
|
// final Rect r = new Rect(x + 1, this.top, c.getWidth() - x, bottom);
|
||||||
|
// final Paint pa = new Paint();
|
||||||
|
// pa.setStyle(Paint.Style.STROKE);
|
||||||
|
// pa.setColor(0xff999999);
|
||||||
|
// c.drawRect(r, pa);
|
||||||
|
// }
|
||||||
|
//// rect.inset((int) paint.getStrokeWidth(), (int) paint.getStrokeWidth());
|
||||||
|
//// paint.setStyle(Paint.Style.STROKE);
|
||||||
|
//// paint.setColor(0xff333333);
|
||||||
|
//// c.drawRect(rect, paint);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @Override
|
||||||
|
// public void chooseHeight(CharSequence text, int start, int end, int spanstartv, int v, Paint.FontMetricsInt fm) {
|
||||||
|
//// int ht = mDrawable.getIntrinsicHeight();
|
||||||
|
////
|
||||||
|
//// int need = ht - (v + fm.descent - fm.ascent - istartv);
|
||||||
|
//// if (need > 0)
|
||||||
|
//// fm.descent += need;
|
||||||
|
////
|
||||||
|
//// need = ht - (v + fm.bottom - fm.top - istartv);
|
||||||
|
//// if (need > 0)
|
||||||
|
//// fm.bottom += need;
|
||||||
|
////
|
||||||
|
//
|
||||||
|
//// final int lineOffset = v - spanstartv;
|
||||||
|
//// final int desired = 128;
|
||||||
|
//// final int currentLineHeight = -fm.ascent + fm.descent;
|
||||||
|
//// final float ratio = (float) desired / currentLineHeight;
|
||||||
|
////
|
||||||
|
//// Debug.i("fm, came: %s", fm);
|
||||||
|
//// Debug.i("lineOffset: %d, current: %d, ratio: %s", lineOffset, currentLineHeight, ratio);
|
||||||
|
////
|
||||||
|
//// fm.ascent = (int) (ratio * fm.ascent + .5F);
|
||||||
|
//// fm.descent = (int) (ratio * fm.descent + .5F);
|
||||||
|
////
|
||||||
|
//// Debug.i("fm, out: %s", fm);
|
||||||
|
//
|
||||||
|
//// Debug.i("top: %d, bottom: %d, ascent: %d, descent: %d", fm.top, fm.bottom, fm.ascent, fm.descent);
|
||||||
|
//// Debug.i("lineHeight: %d, v: %d, spanstartv: %d", lineOffset, v, spanstartv);
|
||||||
|
////
|
||||||
|
//// final int h = 128;
|
||||||
|
//// final int descentNeed = h - (v + fm.descent - fm.ascent - spanstartv);
|
||||||
|
//// if (descentNeed > 0) {
|
||||||
|
//// fm.ascent -= descentNeed / 2;
|
||||||
|
//// fm.descent += descentNeed / 2;
|
||||||
|
//// }
|
||||||
|
//// final int bottomNeed = h - (v + fm.bottom - fm.top - spanstartv);
|
||||||
|
//// if (bottomNeed > 0) {
|
||||||
|
//// fm.top -= bottomNeed;
|
||||||
|
//// fm.bottom += bottomNeed;
|
||||||
|
//// }
|
||||||
|
////
|
||||||
|
//// Debug.i("out, ascent: %d, descent: %d, bottom: %d", fm.ascent, fm.descent, fm.bottom);
|
||||||
|
// }
|
||||||
|
}
|
97
app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
Normal file
97
app/src/main/java/ru/noties/markwon/spans/DrawableSpan.java
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
|
import android.support.annotation.IntRange;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.text.style.ReplacementSpan;
|
||||||
|
|
||||||
|
public class DrawableSpan extends ReplacementSpan {
|
||||||
|
|
||||||
|
@IntDef({ ALIGN_BOTTOM, ALIGN_BASELINE, ALIGN_CENTER })
|
||||||
|
@interface Alignment {}
|
||||||
|
|
||||||
|
public static final int ALIGN_BOTTOM = 0;
|
||||||
|
public static final int ALIGN_BASELINE = 1;
|
||||||
|
public static final int ALIGN_CENTER = 2;
|
||||||
|
|
||||||
|
private final Drawable drawable;
|
||||||
|
private final int alignment;
|
||||||
|
|
||||||
|
public DrawableSpan(@NonNull Drawable drawable) {
|
||||||
|
this(drawable, ALIGN_BOTTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawableSpan(@NonNull Drawable drawable, @Alignment int alignment) {
|
||||||
|
this.drawable = drawable;
|
||||||
|
this.alignment = alignment;
|
||||||
|
|
||||||
|
// additionally set intrinsic bounds if empty
|
||||||
|
final Rect rect = drawable.getBounds();
|
||||||
|
if (rect.isEmpty()) {
|
||||||
|
drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getSize(
|
||||||
|
@NonNull Paint paint,
|
||||||
|
CharSequence text,
|
||||||
|
@IntRange(from = 0) int start,
|
||||||
|
@IntRange(from = 0) int end,
|
||||||
|
@Nullable Paint.FontMetricsInt fm) {
|
||||||
|
|
||||||
|
final Rect rect = drawable.getBounds();
|
||||||
|
|
||||||
|
if (fm != null) {
|
||||||
|
fm.ascent = -rect.bottom;
|
||||||
|
fm.descent = 0;
|
||||||
|
|
||||||
|
fm.top = fm.ascent;
|
||||||
|
fm.bottom = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rect.right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void draw(
|
||||||
|
@NonNull Canvas canvas,
|
||||||
|
CharSequence text,
|
||||||
|
@IntRange(from = 0) int start,
|
||||||
|
@IntRange(from = 0) int end,
|
||||||
|
float x,
|
||||||
|
int top,
|
||||||
|
int y,
|
||||||
|
int bottom,
|
||||||
|
@NonNull Paint paint) {
|
||||||
|
|
||||||
|
final Drawable drawable = this.drawable;
|
||||||
|
|
||||||
|
final int b = bottom - drawable.getBounds().bottom;
|
||||||
|
|
||||||
|
final int save = canvas.save();
|
||||||
|
try {
|
||||||
|
final int translationY;
|
||||||
|
if (ALIGN_CENTER == alignment) {
|
||||||
|
translationY = (int) (b / 2.F + .5F);
|
||||||
|
} else if (ALIGN_BASELINE == alignment) {
|
||||||
|
translationY = b - paint.getFontMetricsInt().descent;
|
||||||
|
} else {
|
||||||
|
translationY = b;
|
||||||
|
}
|
||||||
|
canvas.translate(x, translationY);
|
||||||
|
drawable.draw(canvas);
|
||||||
|
} finally {
|
||||||
|
canvas.restoreToCount(save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Drawable getDrawable() {
|
||||||
|
return drawable;
|
||||||
|
}
|
||||||
|
}
|
116
app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
Normal file
116
app/src/main/java/ru/noties/markwon/spans/DrawableSpanUtils.java
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
|
public class DrawableSpanUtils {
|
||||||
|
|
||||||
|
// this method is not completely valid because DynamicDrawableSpan stores
|
||||||
|
// a drawable in weakReference & it could easily be freed, thus we might need
|
||||||
|
// to re-schedule a new one, but we have no means to do it
|
||||||
|
public static void scheduleDrawables(@NonNull final TextView textView) {
|
||||||
|
|
||||||
|
final CharSequence cs = textView.getText();
|
||||||
|
final int length = cs != null
|
||||||
|
? cs.length()
|
||||||
|
: 0;
|
||||||
|
if (length == 0 || !(cs instanceof Spanned)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Object[] spans = ((Spanned) cs).getSpans(0, length, Object.class);
|
||||||
|
if (spans != null
|
||||||
|
&& spans.length > 0) {
|
||||||
|
|
||||||
|
final List<Drawable> list = new ArrayList<>(2);
|
||||||
|
|
||||||
|
for (Object span: spans) {
|
||||||
|
if (span instanceof DrawableSpan) {
|
||||||
|
list.add(((DrawableSpan) span).getDrawable());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.size() > 0) {
|
||||||
|
textView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() {
|
||||||
|
@Override
|
||||||
|
public void onViewAttachedToWindow(View v) {
|
||||||
|
// can it happen that the same view first detached & them attached with all previous content? hm..
|
||||||
|
// no op for now
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDetachedFromWindow(View v) {
|
||||||
|
// remove callbacks...
|
||||||
|
textView.removeOnAttachStateChangeListener(this);
|
||||||
|
for (Drawable drawable: list) {
|
||||||
|
drawable.setCallback(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (Drawable drawable: list) {
|
||||||
|
drawable.setCallback(new DrawableCallbackImpl(textView, drawable.getBounds()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private DrawableSpanUtils() {}
|
||||||
|
|
||||||
|
private static class DrawableCallbackImpl implements Drawable.Callback {
|
||||||
|
|
||||||
|
private final TextView view;
|
||||||
|
private Rect previousBounds;
|
||||||
|
|
||||||
|
DrawableCallbackImpl(TextView view, Rect initialBounds) {
|
||||||
|
this.view = view;
|
||||||
|
this.previousBounds = new Rect(initialBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invalidateDrawable(@NonNull Drawable who) {
|
||||||
|
|
||||||
|
// okay... teh thing is IF we do not change bounds size, normal invalidate would do
|
||||||
|
// but if the size has changed, then we need to update the whole layout...
|
||||||
|
|
||||||
|
final Rect rect = who.getBounds();
|
||||||
|
|
||||||
|
if (!previousBounds.equals(rect)) {
|
||||||
|
// the only method that seems to work when bounds have changed
|
||||||
|
view.setText(view.getText());
|
||||||
|
previousBounds = new Rect(rect);
|
||||||
|
} else {
|
||||||
|
// if bounds are the same then simple invalidate would do
|
||||||
|
final int scrollX = view.getScrollX();
|
||||||
|
final int scrollY = view.getScrollY();
|
||||||
|
view.postInvalidate(
|
||||||
|
scrollX + rect.left,
|
||||||
|
scrollY + rect.top,
|
||||||
|
scrollX + rect.right,
|
||||||
|
scrollY + rect.bottom
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
|
||||||
|
final long delay = when - SystemClock.uptimeMillis();
|
||||||
|
view.postDelayed(what, delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
|
||||||
|
view.removeCallbacks(what);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
Normal file
17
app/src/main/java/ru/noties/markwon/spans/EmphasisSpan.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
|
||||||
|
public class EmphasisSpan extends MetricAffectingSpan {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMeasureState(TextPaint p) {
|
||||||
|
p.setTextSkewX(-0.25f);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
tp.setTextSkewX(-0.25f);
|
||||||
|
}
|
||||||
|
}
|
50
app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java
Normal file
50
app/src/main/java/ru/noties/markwon/spans/ListItemSpan.java
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
import ru.noties.debug.Debug;
|
||||||
|
|
||||||
|
public class ListItemSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
private final int blockIndent;
|
||||||
|
private final boolean nested;
|
||||||
|
private final int start;
|
||||||
|
|
||||||
|
public ListItemSpan(int blockIndent, boolean nested, int start) {
|
||||||
|
this.blockIndent = blockIndent;
|
||||||
|
this.nested = nested;
|
||||||
|
this.start = start;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLeadingMargin(boolean first) {
|
||||||
|
return 36;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||||
|
// Debug.i("x: %d, dir: %d, top: %d, baseline: %d, bottom: %d, first: %s",
|
||||||
|
// x, dir, top, baseline, bottom, first
|
||||||
|
// );
|
||||||
|
|
||||||
|
// if there was a line break, we don't need to draw it
|
||||||
|
if (this.start != start) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int save = c.save();
|
||||||
|
try {
|
||||||
|
final int left = 24 * (blockIndent - 1) + (first ? 12 : 0);
|
||||||
|
final RectF rectF = new RectF(left, top, left + 16, bottom);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setStyle(nested ? Paint.Style.STROKE : Paint.Style.FILL);
|
||||||
|
paint.setColor(0xFFff0000);
|
||||||
|
c.drawOval(rectF, paint);
|
||||||
|
} finally {
|
||||||
|
c.restoreToCount(save);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
|
||||||
|
public class StrongEmphasisSpan extends MetricAffectingSpan {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMeasureState(TextPaint p) {
|
||||||
|
p.setFakeBoldText(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
tp.setFakeBoldText(true);
|
||||||
|
}
|
||||||
|
}
|
19
app/src/main/java/ru/noties/markwon/spans/SubSpan.java
Normal file
19
app/src/main/java/ru/noties/markwon/spans/SubSpan.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
|
||||||
|
public class SubSpan extends MetricAffectingSpan {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
tp.setTextSize(tp.getTextSize() * .75F);
|
||||||
|
tp.baselineShift -= (int) (tp.ascent() / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMeasureState(TextPaint tp) {
|
||||||
|
tp.setTextSize(tp.getTextSize() * .75F);
|
||||||
|
tp.baselineShift -= (int) (tp.ascent() / 2);
|
||||||
|
}
|
||||||
|
}
|
19
app/src/main/java/ru/noties/markwon/spans/SupSpan.java
Normal file
19
app/src/main/java/ru/noties/markwon/spans/SupSpan.java
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.text.TextPaint;
|
||||||
|
import android.text.style.MetricAffectingSpan;
|
||||||
|
|
||||||
|
public class SupSpan extends MetricAffectingSpan {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint tp) {
|
||||||
|
tp.setTextSize(tp.getTextSize() * .75F);
|
||||||
|
tp.baselineShift += (int) (tp.ascent() / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateMeasureState(TextPaint tp) {
|
||||||
|
tp.setTextSize(tp.getTextSize() * .75F);
|
||||||
|
tp.baselineShift += (int) (tp.ascent() / 2);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.text.Layout;
|
||||||
|
import android.text.style.LeadingMarginSpan;
|
||||||
|
|
||||||
|
public class ThematicBreakSpan implements LeadingMarginSpan {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getLeadingMargin(boolean first) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void drawLeadingMargin(Canvas c, Paint p, int x, int dir, int top, int baseline, int bottom, CharSequence text, int start, int end, boolean first, Layout layout) {
|
||||||
|
final int middle = (bottom - top) / 2;
|
||||||
|
final Rect rect = new Rect(0, top + middle - 2, c.getWidth(), top + middle + 2);
|
||||||
|
final Paint paint = new Paint();
|
||||||
|
paint.setStyle(Paint.Style.FILL);
|
||||||
|
paint.setColor(0x80000000);
|
||||||
|
c.drawRect(rect, paint);
|
||||||
|
}
|
||||||
|
}
|
17
app/src/main/res/layout/activity_main.xml
Normal file
17
app/src/main/res/layout/activity_main.xml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ScrollView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/activity_main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_margin="16dip"
|
||||||
|
tools:context="ru.noties.markwon.MainActivity"
|
||||||
|
tools:text="yo\nman"/>
|
||||||
|
|
||||||
|
</ScrollView>
|
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.2 KiB |
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.5 KiB |
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
6
app/src/main/res/values-w820dp/dimens.xml
Normal file
6
app/src/main/res/values-w820dp/dimens.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Example customization of dimensions originally defined in res/values/dimens.xml
|
||||||
|
(such as screen margins) for screens with more than 820dp of available width. This
|
||||||
|
would include 7" and 10" devices in landscape (~960dp and ~1280dp respectively). -->
|
||||||
|
<dimen name="activity_horizontal_margin">64dp</dimen>
|
||||||
|
</resources>
|
6
app/src/main/res/values/colors.xml
Normal file
6
app/src/main/res/values/colors.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="colorPrimary">#3F51B5</color>
|
||||||
|
<color name="colorPrimaryDark">#303F9F</color>
|
||||||
|
<color name="colorAccent">#FF4081</color>
|
||||||
|
</resources>
|
5
app/src/main/res/values/dimens.xml
Normal file
5
app/src/main/res/values/dimens.xml
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
<!-- Default screen margins, per the Android Design guidelines. -->
|
||||||
|
<dimen name="activity_horizontal_margin">16dp</dimen>
|
||||||
|
<dimen name="activity_vertical_margin">16dp</dimen>
|
||||||
|
</resources>
|
3
app/src/main/res/values/strings.xml
Normal file
3
app/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Markwon</string>
|
||||||
|
</resources>
|
11
app/src/main/res/values/styles.xml
Normal file
11
app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
|
<item name="colorAccent">@color/colorAccent</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
17
app/src/test/java/ru/noties/markwon/ExampleUnitTest.java
Normal file
17
app/src/test/java/ru/noties/markwon/ExampleUnitTest.java
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
package ru.noties.markwon;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
36
build.gradle
Normal file
36
build.gradle
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||||
|
|
||||||
|
buildscript {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
dependencies {
|
||||||
|
classpath 'com.android.tools.build:gradle:2.3.1'
|
||||||
|
|
||||||
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
// in the individual module build.gradle files
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
jcenter()
|
||||||
|
}
|
||||||
|
version = VERSION_NAME
|
||||||
|
}
|
||||||
|
|
||||||
|
task clean(type: Delete) {
|
||||||
|
delete rootProject.buildDir
|
||||||
|
}
|
||||||
|
|
||||||
|
ext {
|
||||||
|
|
||||||
|
// Config
|
||||||
|
BUILD_TOOLS = '25.0.2'
|
||||||
|
TARGET_SDK = 25
|
||||||
|
MIN_SDK = 16
|
||||||
|
|
||||||
|
// Dependencies
|
||||||
|
final def supportVersion = '25.3.1'
|
||||||
|
SUPPORT_ANNOTATIONS = "com.android.support:support-annotations:$supportVersion"
|
||||||
|
}
|
9
gradle.properties
Normal file
9
gradle.properties
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
org.gradle.daemon=true
|
||||||
|
org.gradle.jvmargs=-Xmx5g -Dfile.encoding=UTF-8
|
||||||
|
#org.gradle.parallel=true
|
||||||
|
org.gradle.configureondemand=true
|
||||||
|
|
||||||
|
android.enableBuildCache=true
|
||||||
|
android.buildCacheDir=build/pre-dex-cache
|
||||||
|
|
||||||
|
VERSION_NAME=1.0.0
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
6
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#Tue May 09 19:02:54 MSK 2017
|
||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-3.3-all.zip
|
18
library-spans/build.gradle
Normal file
18
library-spans/build.gradle
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
|
||||||
|
android {
|
||||||
|
|
||||||
|
compileSdkVersion TARGET_SDK
|
||||||
|
buildToolsVersion BUILD_TOOLS
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion MIN_SDK
|
||||||
|
targetSdkVersion TARGET_SDK
|
||||||
|
versionCode 1
|
||||||
|
versionName version
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile SUPPORT_ANNOTATIONS
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.test.InstrumentationRegistry;
|
||||||
|
import android.support.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.junit.runner.RunWith;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Instrumentation test, which will execute on an Android device.
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
@RunWith(AndroidJUnit4.class)
|
||||||
|
public class ExampleInstrumentedTest {
|
||||||
|
@Test
|
||||||
|
public void useAppContext() throws Exception {
|
||||||
|
// Context of the app under test.
|
||||||
|
Context appContext = InstrumentationRegistry.getTargetContext();
|
||||||
|
|
||||||
|
assertEquals("ru.noties.markwon.spans.test", appContext.getPackageName());
|
||||||
|
}
|
||||||
|
}
|
10
library-spans/src/main/AndroidManifest.xml
Normal file
10
library-spans/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
|
||||||
|
package="ru.noties.markwon.spans">
|
||||||
|
|
||||||
|
<application android:allowBackup="true" android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true">
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
3
library-spans/src/main/res/values/strings.xml
Normal file
3
library-spans/src/main/res/values/strings.xml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name">Spans</string>
|
||||||
|
</resources>
|
@ -0,0 +1,17 @@
|
|||||||
|
package ru.noties.markwon.spans;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import static org.junit.Assert.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Example local unit test, which will execute on the development machine (host).
|
||||||
|
*
|
||||||
|
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||||
|
*/
|
||||||
|
public class ExampleUnitTest {
|
||||||
|
@Test
|
||||||
|
public void addition_isCorrect() throws Exception {
|
||||||
|
assertEquals(4, 2 + 2);
|
||||||
|
}
|
||||||
|
}
|
1
settings.gradle
Normal file
1
settings.gradle
Normal file
@ -0,0 +1 @@
|
|||||||
|
include ':app', ':library-spans'
|
Loading…
x
Reference in New Issue
Block a user