Task lists (#4)
* Task lists implementation * Added since annotation to newly added method/classes
This commit is contained in:
parent
eb3c53938b
commit
d1d3f51704
@ -38,6 +38,11 @@ compile 'ru.noties:markwon-view:1.0.0' // optional
|
||||
* * Underline (`<u>`)
|
||||
* * Strike-through (`<s>`, `<strike>`, `<del>`)
|
||||
* other inline html is rendered via (`Html.fromHtml(...)`)
|
||||
* Task lists:
|
||||
|
||||
- [ ] Not _done_
|
||||
- [X] **Done** with `X`
|
||||
- [x] ~~and~~ **or** small `x`
|
||||
|
||||
---
|
||||
|
||||
|
@ -0,0 +1,74 @@
|
||||
package ru.noties.markwon.debug;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import ru.noties.markwon.R;
|
||||
import ru.noties.markwon.spans.TaskListDrawable;
|
||||
|
||||
public class DebugCheckboxDrawableView extends View {
|
||||
|
||||
private Drawable drawable;
|
||||
|
||||
public DebugCheckboxDrawableView(Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public DebugCheckboxDrawableView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, @Nullable AttributeSet attrs) {
|
||||
|
||||
int checkedColor = 0;
|
||||
int normalColor = 0;
|
||||
int checkMarkColor = 0;
|
||||
|
||||
boolean checked = false;
|
||||
|
||||
if (attrs != null) {
|
||||
final TypedArray array = context.obtainStyledAttributes(attrs, R.styleable.DebugCheckboxDrawableView);
|
||||
try {
|
||||
|
||||
checkedColor = array.getColor(R.styleable.DebugCheckboxDrawableView_fcdv_checkedFillColor, checkedColor);
|
||||
normalColor = array.getColor(R.styleable.DebugCheckboxDrawableView_fcdv_normalOutlineColor, normalColor);
|
||||
checkMarkColor = array.getColor(R.styleable.DebugCheckboxDrawableView_fcdv_checkMarkColor, checkMarkColor);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
checked = array.getBoolean(R.styleable.DebugCheckboxDrawableView_fcdv_checked, checked);
|
||||
} finally {
|
||||
array.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
final TaskListDrawable drawable = new TaskListDrawable(checkedColor, normalColor, checkMarkColor);
|
||||
final int[] state;
|
||||
if (checked) {
|
||||
state = new int[]{android.R.attr.state_checked};
|
||||
} else {
|
||||
state = new int[0];
|
||||
}
|
||||
drawable.setState(state);
|
||||
|
||||
this.drawable = drawable;
|
||||
|
||||
setLayerType(LAYER_TYPE_SOFTWARE, null);
|
||||
|
||||
setWillNotDraw(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
drawable.setBounds(0, 0, getWidth(), getHeight());
|
||||
drawable.draw(canvas);
|
||||
}
|
||||
}
|
16
app/src/debug/res/layout/debug_checkbox.xml
Normal file
16
app/src/debug/res/layout/debug_checkbox.xml
Normal file
@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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="match_parent">
|
||||
|
||||
<ru.noties.markwon.debug.DebugCheckboxDrawableView
|
||||
android:layout_width="128dip"
|
||||
android:layout_height="128dip"
|
||||
android:layout_gravity="center"
|
||||
app:fcdv_checkMarkColor="#000"
|
||||
app:fcdv_checked="true"
|
||||
app:fcdv_checkedFillColor="#F00"
|
||||
app:fcdv_normalOutlineColor="#ccc" />
|
||||
|
||||
</FrameLayout>
|
11
app/src/debug/res/values/attrs.xml
Normal file
11
app/src/debug/res/values/attrs.xml
Normal file
@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<declare-styleable name="DebugCheckboxDrawableView">
|
||||
<attr name="fcdv_checkedFillColor" format="color" />
|
||||
<attr name="fcdv_normalOutlineColor" format="color" />
|
||||
<attr name="fcdv_checkMarkColor" format="color" />
|
||||
<attr name="fcdv_checked" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
@ -105,6 +105,16 @@ public Builder tableBorderWidth(@Dimension int tableBorderWidth);
|
||||
public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor);
|
||||
```
|
||||
|
||||
#### Task lists
|
||||
|
||||
Task lists are supported but with some limitations. First of all, task list cannot be nested
|
||||
(in a list, quote, etc). By default (if used factory method `builderWithDefaults`) TaskListDrawable
|
||||
will be used with `linkColor` as the primary color and `windowBackground` as the checkMarkColor.
|
||||
|
||||
```java
|
||||
public Builder taskListDrawable(@NonNull Drawable taskListDrawable);
|
||||
```
|
||||
|
||||
|
||||
### Contents
|
||||
|
||||
|
@ -6,7 +6,7 @@ org.gradle.configureondemand=true
|
||||
android.enableBuildCache=true
|
||||
android.buildCacheDir=build/pre-dex-cache
|
||||
|
||||
VERSION_NAME=1.0.0
|
||||
VERSION_NAME=1.0.1
|
||||
|
||||
GROUP=ru.noties
|
||||
POM_DESCRIPTION=Markwon
|
||||
|
@ -15,19 +15,26 @@ import org.commonmark.parser.Parser;
|
||||
import java.util.Arrays;
|
||||
|
||||
import ru.noties.markwon.renderer.SpannableRenderer;
|
||||
import ru.noties.markwon.tasklist.TaskListExtension;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
public abstract class Markwon {
|
||||
|
||||
/**
|
||||
* Helper method to obtain a Parser with registered strike-through & table extensions
|
||||
* & task lists (added in 1.0.1)
|
||||
*
|
||||
* @return a Parser instance that is supported by this library
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Parser createParser() {
|
||||
return new Parser.Builder()
|
||||
.extensions(Arrays.asList(StrikethroughExtension.create(), TablesExtension.create()))
|
||||
.extensions(Arrays.asList(
|
||||
StrikethroughExtension.create(),
|
||||
TablesExtension.create(),
|
||||
TaskListExtension.create()
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -91,6 +98,7 @@ public abstract class Markwon {
|
||||
* @return parsed markdown
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Nullable
|
||||
public static CharSequence markdown(@NonNull Context context, @Nullable String markdown) {
|
||||
final CharSequence out;
|
||||
if (TextUtils.isEmpty(markdown)) {
|
||||
@ -111,6 +119,7 @@ public abstract class Markwon {
|
||||
* @see SpannableConfiguration
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Nullable
|
||||
public static CharSequence markdown(@NonNull SpannableConfiguration configuration, @Nullable String markdown) {
|
||||
final CharSequence out;
|
||||
if (TextUtils.isEmpty(markdown)) {
|
||||
|
@ -14,6 +14,7 @@ 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.Emphasis;
|
||||
import org.commonmark.node.FencedCodeBlock;
|
||||
@ -51,7 +52,10 @@ import ru.noties.markwon.spans.LinkSpan;
|
||||
import ru.noties.markwon.spans.OrderedListItemSpan;
|
||||
import ru.noties.markwon.spans.StrongEmphasisSpan;
|
||||
import ru.noties.markwon.spans.TableRowSpan;
|
||||
import ru.noties.markwon.spans.TaskListSpan;
|
||||
import ru.noties.markwon.spans.ThematicBreakSpan;
|
||||
import ru.noties.markwon.tasklist.TaskListBlock;
|
||||
import ru.noties.markwon.tasklist.TaskListItem;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
@ -269,6 +273,22 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
newLine();
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@Override
|
||||
public void visit(CustomBlock customBlock) {
|
||||
if (customBlock instanceof TaskListBlock) {
|
||||
blockQuoteIndent += 1;
|
||||
visitChildren(customBlock);
|
||||
blockQuoteIndent -= 1;
|
||||
newLine();
|
||||
builder.append('\n');
|
||||
} else {
|
||||
super.visit(customBlock);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void visit(CustomNode customNode) {
|
||||
|
||||
@ -278,6 +298,29 @@ public class SpannableMarkdownVisitor extends AbstractVisitor {
|
||||
visitChildren(customNode);
|
||||
setSpan(length, new StrikethroughSpan());
|
||||
|
||||
} else if (customNode instanceof TaskListItem) {
|
||||
|
||||
// new in 1.0.1
|
||||
|
||||
final TaskListItem listItem = (TaskListItem) customNode;
|
||||
|
||||
final int length = builder.length();
|
||||
|
||||
blockQuoteIndent += listItem.indent();
|
||||
|
||||
visitChildren(customNode);
|
||||
|
||||
setSpan(length, new TaskListSpan(
|
||||
configuration.theme(),
|
||||
blockQuoteIndent,
|
||||
length,
|
||||
listItem.done()
|
||||
));
|
||||
|
||||
newLine();
|
||||
|
||||
blockQuoteIndent -= listItem.indent();
|
||||
|
||||
} else if (!handleTableNodes(customNode)) {
|
||||
super.visit(customNode);
|
||||
}
|
||||
|
@ -8,16 +8,9 @@ import org.commonmark.node.Node;
|
||||
|
||||
import ru.noties.markwon.SpannableConfiguration;
|
||||
|
||||
// please note that this class does not implement Renderer in order to return CharSequence (instead of String)
|
||||
public class SpannableRenderer {
|
||||
|
||||
// todo
|
||||
// * LinkDrawableSpan, that draws link whilst image is still loading (it must be clickable...)
|
||||
// * Common interface for images (in markdown & inline-html)
|
||||
// * util method to properly copy markdown (with lists/links, etc)
|
||||
// * util to apply empty line height
|
||||
// * transform relative urls to absolute ones...
|
||||
|
||||
@Nullable
|
||||
public CharSequence render(@NonNull SpannableConfiguration configuration, @Nullable Node node) {
|
||||
final CharSequence out;
|
||||
if (node == null) {
|
||||
|
@ -4,44 +4,81 @@ import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.Dimension;
|
||||
import android.support.annotation.FloatRange;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextPaint;
|
||||
import android.util.TypedValue;
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class SpannableTheme {
|
||||
//
|
||||
// // this method should be used if TextView is known beforehand
|
||||
// // it will correctly measure the `space` char and set it as `codeMultilineMargin`
|
||||
// // otherwise this value must be set explicitly
|
||||
// public static SpannableTheme create(@NonNull TextView textView) {
|
||||
// return builderWithDefaults(textView.getContext())
|
||||
// .codeMultilineMargin((int) (textView.getPaint().measureText("\u00a0") + .5F))
|
||||
// .build();
|
||||
// }
|
||||
|
||||
// this create default theme (except for `codeMultilineMargin` property)
|
||||
/**
|
||||
* Factory method to obtain an instance of {@link SpannableTheme} with all values as defaults
|
||||
*
|
||||
* @param context Context in order to resolve defaults
|
||||
* @return {@link SpannableTheme} instance
|
||||
* @see #builderWithDefaults(Context)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static SpannableTheme create(@NonNull Context context) {
|
||||
return builderWithDefaults(context).build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain an instance of {@link Builder}. Please note, that no default
|
||||
* values are set. This might be useful if you require a lot of special styling that differs
|
||||
* a lot with default one
|
||||
*
|
||||
* @return {@link Builder instance}
|
||||
* @see #builderWithDefaults(Context)
|
||||
* @see #builder(SpannableTheme)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to create a {@link Builder} instance and initialize it with values
|
||||
* from supplied {@link SpannableTheme}
|
||||
*
|
||||
* @param copyFrom {@link SpannableTheme} to copy values from
|
||||
* @return {@link Builder} instance
|
||||
* @see #builderWithDefaults(Context)
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builder(@NonNull SpannableTheme copyFrom) {
|
||||
return new Builder(copyFrom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory method to obtain a {@link Builder} instance initialized with default values taken
|
||||
* from current application theme.
|
||||
*
|
||||
* @param context Context to obtain default styling values (colors, etc)
|
||||
* @return {@link Builder} instance
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@NonNull
|
||||
public static Builder builderWithDefaults(@NonNull Context context) {
|
||||
|
||||
// by default we will be using link color for the checkbox color
|
||||
// & window background as a checkMark color
|
||||
final int linkColor = resolve(context, android.R.attr.textColorLink);
|
||||
final int backgroundColor = resolve(context, android.R.attr.colorBackground);
|
||||
|
||||
final Dip dip = new Dip(context);
|
||||
return new Builder()
|
||||
.linkColor(resolve(context, android.R.attr.textColorLink))
|
||||
.linkColor(linkColor)
|
||||
.codeMultilineMargin(dip.toPx(8))
|
||||
.blockMargin(dip.toPx(24))
|
||||
.blockQuoteWidth(dip.toPx(4))
|
||||
@ -49,7 +86,8 @@ public class SpannableTheme {
|
||||
.headingBreakHeight(dip.toPx(1))
|
||||
.thematicBreakHeight(dip.toPx(4))
|
||||
.tableCellPadding(dip.toPx(4))
|
||||
.tableBorderWidth(dip.toPx(1));
|
||||
.tableBorderWidth(dip.toPx(1))
|
||||
.taskListDrawable(new TaskListDrawable(linkColor, linkColor, backgroundColor));
|
||||
}
|
||||
|
||||
private static int resolve(Context context, @AttrRes int attr) {
|
||||
@ -147,6 +185,10 @@ public class SpannableTheme {
|
||||
// by default paint.color * TABLE_ODD_ROW_DEF_ALPHA
|
||||
protected final int tableOddRowBackgroundColor;
|
||||
|
||||
// drawable that will be used to render checkbox (should be stateful)
|
||||
// TaskListDrawable can be used
|
||||
protected final Drawable taskListDrawable;
|
||||
|
||||
protected SpannableTheme(@NonNull Builder builder) {
|
||||
this.linkColor = builder.linkColor;
|
||||
this.blockMargin = builder.blockMargin;
|
||||
@ -169,6 +211,7 @@ public class SpannableTheme {
|
||||
this.tableBorderColor = builder.tableBorderColor;
|
||||
this.tableBorderWidth = builder.tableBorderWidth;
|
||||
this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor;
|
||||
this.taskListDrawable = builder.taskListDrawable;
|
||||
}
|
||||
|
||||
|
||||
@ -243,10 +286,16 @@ public class SpannableTheme {
|
||||
|
||||
// custom typeface was set
|
||||
if (codeTypeface != null) {
|
||||
|
||||
paint.setTypeface(codeTypeface);
|
||||
|
||||
// please note that we won't be calculating textSize
|
||||
// (like we do when no Typeface is provided), if it's some specific typeface
|
||||
// we would confuse users about textSize
|
||||
if (codeTextSize != 0) {
|
||||
paint.setTextSize(codeTextSize);
|
||||
}
|
||||
|
||||
} else {
|
||||
paint.setTypeface(Typeface.MONOSPACE);
|
||||
final float textSize;
|
||||
@ -363,6 +412,15 @@ public class SpannableTheme {
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a Drawable to be used as a checkbox indication in task lists
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@Nullable
|
||||
public Drawable getTaskListDrawable() {
|
||||
return taskListDrawable;
|
||||
}
|
||||
|
||||
public static class Builder {
|
||||
|
||||
private int linkColor;
|
||||
@ -386,6 +444,7 @@ public class SpannableTheme {
|
||||
private int tableBorderColor;
|
||||
private int tableBorderWidth;
|
||||
private int tableOddRowBackgroundColor;
|
||||
private Drawable taskListDrawable;
|
||||
|
||||
Builder() {
|
||||
}
|
||||
@ -412,119 +471,159 @@ public class SpannableTheme {
|
||||
this.tableBorderColor = theme.tableBorderColor;
|
||||
this.tableBorderWidth = theme.tableBorderWidth;
|
||||
this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor;
|
||||
this.taskListDrawable = theme.taskListDrawable;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder linkColor(@ColorInt int linkColor) {
|
||||
this.linkColor = linkColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockMargin(@Dimension int blockMargin) {
|
||||
this.blockMargin = blockMargin;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockQuoteWidth(@Dimension int blockQuoteWidth) {
|
||||
this.blockQuoteWidth = blockQuoteWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder blockQuoteColor(@ColorInt int blockQuoteColor) {
|
||||
this.blockQuoteColor = blockQuoteColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder listItemColor(@ColorInt int listItemColor) {
|
||||
this.listItemColor = listItemColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder bulletListItemStrokeWidth(@Dimension int bulletListItemStrokeWidth) {
|
||||
this.bulletListItemStrokeWidth = bulletListItemStrokeWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder bulletWidth(@Dimension int bulletWidth) {
|
||||
this.bulletWidth = bulletWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeTextColor(@ColorInt int codeTextColor) {
|
||||
this.codeTextColor = codeTextColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeBackgroundColor(@ColorInt int codeBackgroundColor) {
|
||||
this.codeBackgroundColor = codeBackgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeMultilineMargin(@Dimension int codeMultilineMargin) {
|
||||
this.codeMultilineMargin = codeMultilineMargin;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeTypeface(@NonNull Typeface codeTypeface) {
|
||||
this.codeTypeface = codeTypeface;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder codeTextSize(@Dimension int codeTextSize) {
|
||||
this.codeTextSize = codeTextSize;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder headingBreakHeight(@Dimension int headingBreakHeight) {
|
||||
this.headingBreakHeight = headingBreakHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder headingBreakColor(@ColorInt int headingBreakColor) {
|
||||
this.headingBreakColor = headingBreakColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder scriptTextSizeRatio(@FloatRange(from = .0F, to = Float.MAX_VALUE) float scriptTextSizeRatio) {
|
||||
this.scriptTextSizeRatio = scriptTextSizeRatio;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder thematicBreakColor(@ColorInt int thematicBreakColor) {
|
||||
this.thematicBreakColor = thematicBreakColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder thematicBreakHeight(@Dimension int thematicBreakHeight) {
|
||||
this.thematicBreakHeight = thematicBreakHeight;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableCellPadding(@Dimension int tableCellPadding) {
|
||||
this.tableCellPadding = tableCellPadding;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableBorderColor(@ColorInt int tableBorderColor) {
|
||||
this.tableBorderColor = tableBorderColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableBorderWidth(@Dimension int tableBorderWidth) {
|
||||
this.tableBorderWidth = tableBorderWidth;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Builder tableOddRowBackgroundColor(@ColorInt int tableOddRowBackgroundColor) {
|
||||
this.tableOddRowBackgroundColor = tableOddRowBackgroundColor;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Supplied Drawable must be stateful ({@link Drawable#isStateful()} -> true). If a task
|
||||
* is marked as done, then this drawable will be updated with an {@code int[] { android.R.attr.state_checked }}
|
||||
* as the state, otherwise an empty array will be used. This library provides a ready to be
|
||||
* used Drawable: {@link TaskListDrawable}
|
||||
*
|
||||
* @param taskListDrawable Drawable to be used as the task list indication (checkbox)
|
||||
* @see TaskListDrawable
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@NonNull
|
||||
public Builder taskListDrawable(@NonNull Drawable taskListDrawable) {
|
||||
this.taskListDrawable = taskListDrawable;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public SpannableTheme build() {
|
||||
return new SpannableTheme(this);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Dip {
|
||||
|
||||
private final float density;
|
||||
|
||||
Dip(@NonNull Context context) {
|
||||
|
@ -0,0 +1,180 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.ColorFilter;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.RectF;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.ColorInt;
|
||||
import android.support.annotation.IntRange;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class TaskListDrawable extends Drawable {
|
||||
|
||||
// represent ratios (not exact coordinates)
|
||||
private static final Point POINT_0 = new Point(2.75F / 18, 8.25F / 18);
|
||||
private static final Point POINT_1 = new Point(7.F / 18, 12.5F / 18);
|
||||
private static final Point POINT_2 = new Point(15.25F / 18, 4.75F / 18);
|
||||
|
||||
private final int checkedFillColor;
|
||||
private final int normalOutlineColor;
|
||||
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final RectF rectF = new RectF();
|
||||
|
||||
private final Paint checkMarkPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path checkMarkPath = new Path();
|
||||
|
||||
private boolean isChecked;
|
||||
|
||||
// unfortunately we cannot rely on TextView to be LAYER_TYPE_SOFTWARE
|
||||
// if we could we would draw our checkMarkPath with PorterDuff.CLEAR
|
||||
public TaskListDrawable(@ColorInt int checkedFillColor, @ColorInt int normalOutlineColor, @ColorInt int checkMarkColor) {
|
||||
this.checkedFillColor = checkedFillColor;
|
||||
this.normalOutlineColor = normalOutlineColor;
|
||||
|
||||
checkMarkPaint.setColor(checkMarkColor);
|
||||
checkMarkPaint.setStyle(Paint.Style.STROKE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onBoundsChange(Rect bounds) {
|
||||
super.onBoundsChange(bounds);
|
||||
|
||||
// we should exclude stroke with from final bounds (half of the strokeWidth from all sides)
|
||||
|
||||
// we should have square shape
|
||||
final float min = Math.min(bounds.width(), bounds.height());
|
||||
final float stroke = min / 8;
|
||||
|
||||
final float side = min - stroke;
|
||||
rectF.set(0, 0, side, side);
|
||||
|
||||
paint.setStrokeWidth(stroke);
|
||||
checkMarkPaint.setStrokeWidth(stroke);
|
||||
|
||||
checkMarkPath.reset();
|
||||
|
||||
POINT_0.moveTo(checkMarkPath, side);
|
||||
POINT_1.lineTo(checkMarkPath, side);
|
||||
POINT_2.lineTo(checkMarkPath, side);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void draw(@NonNull Canvas canvas) {
|
||||
|
||||
final Paint.Style style;
|
||||
final int color;
|
||||
|
||||
if (isChecked) {
|
||||
style = Paint.Style.FILL_AND_STROKE;
|
||||
color = checkedFillColor;
|
||||
} else {
|
||||
style = Paint.Style.STROKE;
|
||||
color = normalOutlineColor;
|
||||
}
|
||||
paint.setStyle(style);
|
||||
paint.setColor(color);
|
||||
|
||||
final Rect bounds = getBounds();
|
||||
|
||||
final float left = (bounds.width() - rectF.width()) / 2;
|
||||
final float top = (bounds.height() - rectF.height()) / 2;
|
||||
|
||||
final float radius = rectF.width() / 8;
|
||||
|
||||
final int save = canvas.save();
|
||||
try {
|
||||
|
||||
canvas.translate(left, top);
|
||||
|
||||
canvas.drawRoundRect(rectF, radius, radius, paint);
|
||||
|
||||
if (isChecked) {
|
||||
canvas.drawPath(checkMarkPath, checkMarkPaint);
|
||||
}
|
||||
} finally {
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAlpha(@IntRange(from = 0, to = 255) int alpha) {
|
||||
paint.setAlpha(alpha);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setColorFilter(@Nullable ColorFilter colorFilter) {
|
||||
paint.setColorFilter(colorFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOpacity() {
|
||||
return PixelFormat.OPAQUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStateful() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean onStateChange(int[] state) {
|
||||
|
||||
final boolean checked;
|
||||
|
||||
final int length = state != null
|
||||
? state.length
|
||||
: 0;
|
||||
|
||||
if (length > 0) {
|
||||
|
||||
boolean inner = false;
|
||||
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (android.R.attr.state_checked == state[i]) {
|
||||
inner = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
checked = inner;
|
||||
} else {
|
||||
checked = false;
|
||||
}
|
||||
|
||||
final boolean result = checked != isChecked;
|
||||
if (result) {
|
||||
invalidateSelf();
|
||||
isChecked = checked;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static class Point {
|
||||
|
||||
final float x;
|
||||
final float y;
|
||||
|
||||
Point(float x, float y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
void moveTo(@NonNull Path path, float side) {
|
||||
path.moveTo(side * x, side * y);
|
||||
}
|
||||
|
||||
void lineTo(@NonNull Path path, float side) {
|
||||
path.lineTo(side * x, side * y);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
package ru.noties.markwon.spans;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.Layout;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
public class TaskListSpan implements LeadingMarginSpan {
|
||||
|
||||
private final SpannableTheme theme;
|
||||
private final int blockIndent;
|
||||
private final int start;
|
||||
private final boolean isDone;
|
||||
|
||||
public TaskListSpan(@NonNull SpannableTheme theme, int blockIndent, int start, boolean isDone) {
|
||||
this.theme = theme;
|
||||
this.blockIndent = blockIndent;
|
||||
this.start = start;
|
||||
this.isDone = isDone;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getLeadingMargin(boolean first) {
|
||||
return theme.getBlockMargin() * blockIndent;
|
||||
}
|
||||
|
||||
@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) {
|
||||
|
||||
if (this.start != start) {
|
||||
return;
|
||||
}
|
||||
|
||||
final Drawable drawable = theme.getTaskListDrawable();
|
||||
if (drawable == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final int save = c.save();
|
||||
try {
|
||||
|
||||
final int width = theme.getBlockMargin();
|
||||
final int height = bottom - top;
|
||||
|
||||
final int w = (int) (width * .75F + .5F);
|
||||
final int h = (int) (height * .75F + .5F);
|
||||
|
||||
drawable.setBounds(0, 0, w, h);
|
||||
|
||||
if (drawable.isStateful()) {
|
||||
final int[] state;
|
||||
if (isDone) {
|
||||
state = new int[]{android.R.attr.state_checked};
|
||||
} else {
|
||||
state = new int[0];
|
||||
}
|
||||
drawable.setState(state);
|
||||
}
|
||||
|
||||
final int l = (width * (blockIndent - 1)) + ((width - w) / 2);
|
||||
final int t = top + ((height - h) / 2);
|
||||
|
||||
c.translate(l, t);
|
||||
drawable.draw(c);
|
||||
|
||||
} finally {
|
||||
c.restoreToCount(save);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package ru.noties.markwon.tasklist;
|
||||
|
||||
import org.commonmark.node.CustomBlock;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
public class TaskListBlock extends CustomBlock {
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
package ru.noties.markwon.tasklist;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.commonmark.node.Block;
|
||||
import org.commonmark.parser.InlineParser;
|
||||
import org.commonmark.parser.block.AbstractBlockParser;
|
||||
import org.commonmark.parser.block.AbstractBlockParserFactory;
|
||||
import org.commonmark.parser.block.BlockContinue;
|
||||
import org.commonmark.parser.block.BlockStart;
|
||||
import org.commonmark.parser.block.MatchedBlockParser;
|
||||
import org.commonmark.parser.block.ParserState;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
class TaskListBlockParser extends AbstractBlockParser {
|
||||
|
||||
private static final Pattern PATTERN = Pattern.compile("\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)");
|
||||
|
||||
private final TaskListBlock block = new TaskListBlock();
|
||||
|
||||
private final List<Item> items = new ArrayList<>(3);
|
||||
|
||||
private int indent = 0;
|
||||
|
||||
TaskListBlockParser(@NonNull String startLine, int startIndent) {
|
||||
items.add(new Item(startLine, startIndent));
|
||||
indent = startIndent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Block getBlock() {
|
||||
return block;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BlockContinue tryContinue(ParserState parserState) {
|
||||
|
||||
final BlockContinue blockContinue;
|
||||
|
||||
final String line = line(parserState);
|
||||
|
||||
final int currentIndent = parserState.getIndent();
|
||||
if (currentIndent > indent) {
|
||||
indent += 2;
|
||||
} else if (currentIndent < indent && indent > 1) {
|
||||
indent -= 2;
|
||||
}
|
||||
|
||||
if (line != null
|
||||
&& line.length() > 0
|
||||
&& PATTERN.matcher(line).matches()) {
|
||||
blockContinue = BlockContinue.atIndex(parserState.getIndex());
|
||||
} else {
|
||||
blockContinue = BlockContinue.finished();
|
||||
}
|
||||
|
||||
return blockContinue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addLine(CharSequence line) {
|
||||
if (length(line) > 0) {
|
||||
items.add(new Item(line.toString(), indent));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void parseInlines(InlineParser inlineParser) {
|
||||
|
||||
Matcher matcher;
|
||||
|
||||
TaskListItem listItem;
|
||||
|
||||
for (Item item : items) {
|
||||
matcher = PATTERN.matcher(item.line);
|
||||
if (!matcher.matches()) {
|
||||
continue;
|
||||
}
|
||||
listItem = new TaskListItem()
|
||||
.done(isDone(matcher.group(1)))
|
||||
.indent(item.indent / 2);
|
||||
inlineParser.parse(matcher.group(2), listItem);
|
||||
block.appendChild(listItem);
|
||||
}
|
||||
}
|
||||
|
||||
static class Factory extends AbstractBlockParserFactory {
|
||||
|
||||
@Override
|
||||
public BlockStart tryStart(ParserState state, MatchedBlockParser matchedBlockParser) {
|
||||
|
||||
final String line = line(state);
|
||||
|
||||
if (line != null
|
||||
&& line.length() > 0
|
||||
&& PATTERN.matcher(line).matches()) {
|
||||
|
||||
final int length = line.length();
|
||||
final int index = state.getIndex();
|
||||
final int atIndex = index != 0
|
||||
? index + (length - index)
|
||||
: length;
|
||||
|
||||
return BlockStart.of(new TaskListBlockParser(line, state.getIndent()))
|
||||
.atIndex(atIndex);
|
||||
}
|
||||
|
||||
return BlockStart.none();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String line(@NonNull ParserState state) {
|
||||
final CharSequence lineRaw = state.getLine();
|
||||
return lineRaw != null
|
||||
? lineRaw.toString()
|
||||
: null;
|
||||
}
|
||||
|
||||
private static int length(@Nullable CharSequence text) {
|
||||
return text != null
|
||||
? text.length()
|
||||
: 0;
|
||||
}
|
||||
|
||||
private static boolean isDone(@NonNull String value) {
|
||||
return "X".equals(value)
|
||||
|| "x".equals(value);
|
||||
}
|
||||
|
||||
private static class Item {
|
||||
|
||||
final String line;
|
||||
final int indent;
|
||||
|
||||
Item(@NonNull String line, int indent) {
|
||||
this.line = line;
|
||||
this.indent = indent;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
package ru.noties.markwon.tasklist;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.commonmark.parser.Parser;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
public class TaskListExtension implements Parser.ParserExtension {
|
||||
|
||||
@NonNull
|
||||
public static TaskListExtension create() {
|
||||
return new TaskListExtension();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void extend(Parser.Builder parserBuilder) {
|
||||
parserBuilder.customBlockParserFactory(new TaskListBlockParser.Factory());
|
||||
}
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package ru.noties.markwon.tasklist;
|
||||
|
||||
import org.commonmark.node.CustomNode;
|
||||
|
||||
/**
|
||||
* @since 1.0.1
|
||||
*/
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public class TaskListItem extends CustomNode {
|
||||
|
||||
private boolean done;
|
||||
private int indent;
|
||||
|
||||
public boolean done() {
|
||||
return done;
|
||||
}
|
||||
|
||||
public TaskListItem done(boolean done) {
|
||||
this.done = done;
|
||||
return this;
|
||||
}
|
||||
|
||||
public int indent() {
|
||||
return indent;
|
||||
}
|
||||
|
||||
public TaskListItem indent(int indent) {
|
||||
this.indent = indent;
|
||||
return this;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user