From f95918104b258d38f71bceceda0ae241c277d8eb Mon Sep 17 00:00:00 2001 From: Dimitry Ivanov Date: Sun, 22 Oct 2017 15:19:55 +0300 Subject: [PATCH] Raw impl of task lists (parser, span, drawable) --- README.md | 3 + .../debug/DebugCheckboxDrawableView.java | 74 ++++++++ app/src/debug/res/layout/debug_checkbox.xml | 16 ++ app/src/debug/res/values/attrs.xml | 11 ++ library-task-list/build.gradle | 10 + .../markwon/tasklist/TaskListBlock.java | 6 + .../markwon/tasklist/TaskListBlockParser.java | 147 +++++++++++++++ .../markwon/tasklist/TaskListExtension.java | 18 ++ .../noties/markwon/tasklist/TaskListItem.java | 17 ++ library/build.gradle | 2 + .../main/java/ru/noties/markwon/Markwon.java | 3 +- .../renderer/SpannableMarkdownVisitor.java | 29 ++- .../noties/markwon/spans/SpannableTheme.java | 25 ++- .../markwon/spans/TaskListDrawable.java | 177 ++++++++++++++++++ .../ru/noties/markwon/spans/TaskListSpan.java | 69 +++++++ settings.gradle | 2 +- 16 files changed, 604 insertions(+), 5 deletions(-) create mode 100644 app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java create mode 100644 app/src/debug/res/layout/debug_checkbox.xml create mode 100644 app/src/debug/res/values/attrs.xml create mode 100644 library-task-list/build.gradle create mode 100644 library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java create mode 100644 library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java create mode 100644 library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java create mode 100644 library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java create mode 100644 library/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java create mode 100644 library/src/main/java/ru/noties/markwon/spans/TaskListSpan.java diff --git a/README.md b/README.md index 5a18e025..3e0d5383 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ [![markwon-image-loader](https://img.shields.io/maven-central/v/ru.noties/markwon-image-loader.svg?label=markwon-image-loader)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-image-loader%22) [![markwon-view](https://img.shields.io/maven-central/v/ru.noties/markwon-view.svg?label=markwon-view)](http://search.maven.org/#search|ga|1|g%3A%22ru.noties%22%20AND%20a%3A%markwon-view%22) +- [ ] one **one** _one_ +- [X] ~~two~~ + **Markwon** is a library for Android that renders markdown as system-native Spannables. It gives ability to display markdown in all TextView widgets (**TextView**, **Button**, **Switch**, **CheckBox**, etc), **Notifications**, **Toasts**, etc. **No WebView is required**. Library provides reasonable defaults for display style of markdown but also gives all the means to tweak the appearance if desired. All markdown features are supported (including limited support for inlined HTML code, markdown tables and images). **This file is displayed by default in the [sample-apk] application. Which is a generic markdown viewer with support to display markdown via `http`, `https` & `file` schemes and 2 themes included: Light & Dark* diff --git a/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java b/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java new file mode 100644 index 00000000..34bef820 --- /dev/null +++ b/app/src/debug/java/ru/noties/markwon/debug/DebugCheckboxDrawableView.java @@ -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); + } +} diff --git a/app/src/debug/res/layout/debug_checkbox.xml b/app/src/debug/res/layout/debug_checkbox.xml new file mode 100644 index 00000000..bf56ec39 --- /dev/null +++ b/app/src/debug/res/layout/debug_checkbox.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/debug/res/values/attrs.xml b/app/src/debug/res/values/attrs.xml new file mode 100644 index 00000000..161f7806 --- /dev/null +++ b/app/src/debug/res/values/attrs.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/library-task-list/build.gradle b/library-task-list/build.gradle new file mode 100644 index 00000000..094d9248 --- /dev/null +++ b/library-task-list/build.gradle @@ -0,0 +1,10 @@ +apply plugin: 'java' + +sourceCompatibility = "1.7" +targetCompatibility = "1.7" + +dependencies { + compile SUPPORT_ANNOTATIONS + compile COMMON_MARK + compile 'ru.noties:debug:3.0.0@jar' +} diff --git a/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java new file mode 100644 index 00000000..d477e960 --- /dev/null +++ b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlock.java @@ -0,0 +1,6 @@ +package ru.noties.markwon.tasklist; + +import org.commonmark.node.CustomBlock; + +public class TaskListBlock extends CustomBlock { +} diff --git a/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java new file mode 100644 index 00000000..f6942fe5 --- /dev/null +++ b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListBlockParser.java @@ -0,0 +1,147 @@ +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; + +import ru.noties.debug.Debug; + +class TaskListBlockParser extends AbstractBlockParser { + + private static final Pattern PATTERN = Pattern.compile("\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)"); +// private static final Pattern PATTERN_2 = Pattern.compile("^\\s*-\\s+\\[(x|X|\\s)\\]\\s+(.*)"); + + private final TaskListBlock block = new TaskListBlock(); + + private final List lines; + + TaskListBlockParser(@NonNull String startLine) { + this.lines = new ArrayList<>(3); + this.lines.add(startLine); + } + + @Override + public Block getBlock() { + return block; + } + + @Override + public BlockContinue tryContinue(ParserState parserState) { + + final BlockContinue blockContinue; + + final String line = line(parserState); + +// Debug.i("line: %s, find: %s", line, PATTERN.matcher(line).find()); + Debug.i("isBlank: %s, line: `%s`", parserState.isBlank(), line); + + if (line != null + && line.length() > 0 + && PATTERN.matcher(line).matches()) { + Debug.e(); + blockContinue = BlockContinue.atIndex(parserState.getIndex()); + } else { + Debug.e(); + blockContinue = BlockContinue.finished(); + } + + return blockContinue; + } + + @Override + public void addLine(CharSequence line) { + Debug.i("line: %s", line); + if (line != null + && line.length() > 0) { + lines.add(line.toString()); + } + } + + @Override + public void parseInlines(InlineParser inlineParser) { + + Debug.i(lines); + + Matcher matcher; + + TaskListItem item; + + for (String line : lines) { + + matcher = PATTERN.matcher(line); + + if (!matcher.matches()) { + continue; + } + + item = new TaskListItem().done(isDone(matcher.group(1))); + + inlineParser.parse(matcher.group(2), item); + + block.appendChild(item); + } + + } + + @Override + public boolean isContainer() { + return false; + } + + @Override + public boolean canContain(Block block) { + Debug.i("block: %s", block); + return false; + } + + @Override + public void closeBlock() { + Debug.e(block); + Debug.trace(); + } + + 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()) { + + return BlockStart.of(new TaskListBlockParser(line)) + .atIndex(state.getIndex() + line.length()); + } + + return BlockStart.none(); + } + } + + @Nullable + private static String line(@NonNull ParserState state) { + final CharSequence lineRaw = state.getLine(); + return lineRaw != null + ? lineRaw.toString() + : null; + } + + private static boolean isDone(@NonNull String value) { + return "X".equals(value) + || "x".equals(value); + } +} diff --git a/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java new file mode 100644 index 00000000..535e31e8 --- /dev/null +++ b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListExtension.java @@ -0,0 +1,18 @@ +package ru.noties.markwon.tasklist; + +import android.support.annotation.NonNull; + +import org.commonmark.parser.Parser; + +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()); + } +} diff --git a/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java new file mode 100644 index 00000000..cef9fcff --- /dev/null +++ b/library-task-list/src/main/java/ru/noties/markwon/tasklist/TaskListItem.java @@ -0,0 +1,17 @@ +package ru.noties.markwon.tasklist; + +import org.commonmark.node.CustomNode; + +public class TaskListItem extends CustomNode { + + private boolean done; + + public boolean done() { + return done; + } + + public TaskListItem done(boolean done) { + this.done = done; + return this; + } +} diff --git a/library/build.gradle b/library/build.gradle index c384c1b6..aba4fc62 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -18,6 +18,8 @@ dependencies { compile COMMON_MARK compile COMMON_MARK_STRIKETHROUGHT compile COMMON_MARK_TABLE + + compile project(':library-task-list') } if (project.hasProperty('release')) { diff --git a/library/src/main/java/ru/noties/markwon/Markwon.java b/library/src/main/java/ru/noties/markwon/Markwon.java index 09ac6ac4..9f74ebbd 100644 --- a/library/src/main/java/ru/noties/markwon/Markwon.java +++ b/library/src/main/java/ru/noties/markwon/Markwon.java @@ -15,6 +15,7 @@ import org.commonmark.parser.Parser; import java.util.Arrays; import ru.noties.markwon.renderer.SpannableRenderer; +import ru.noties.markwon.tasklist.TaskListExtension; @SuppressWarnings("WeakerAccess") public abstract class Markwon { @@ -27,7 +28,7 @@ public abstract class Markwon { */ 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(); } diff --git a/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java index 022d706b..a9c00690 100644 --- a/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java +++ b/library/src/main/java/ru/noties/markwon/renderer/SpannableMarkdownVisitor.java @@ -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,19 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { newLine(); } + @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) { @@ -279,7 +296,17 @@ public class SpannableMarkdownVisitor extends AbstractVisitor { setSpan(length, new StrikethroughSpan()); } else if (!handleTableNodes(customNode)) { - super.visit(customNode); + + if (customNode instanceof TaskListItem) { + + final int length = builder.length(); + visitChildren(customNode); + setSpan(length, new TaskListSpan(configuration.theme(), blockQuoteIndent, length, ((TaskListItem) customNode).done())); + newLine(); + + } else { + super.visit(customNode); + } } } diff --git a/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java b/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java index 58f7fd04..63d8b69b 100644 --- a/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java +++ b/library/src/main/java/ru/noties/markwon/spans/SpannableTheme.java @@ -4,6 +4,7 @@ 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; @@ -39,9 +40,13 @@ public class SpannableTheme { } public static Builder builderWithDefaults(@NonNull Context context) { + + 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 +54,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 +153,8 @@ public class SpannableTheme { // by default paint.color * TABLE_ODD_ROW_DEF_ALPHA protected final int tableOddRowBackgroundColor; + protected final Drawable taskListDrawable; + protected SpannableTheme(@NonNull Builder builder) { this.linkColor = builder.linkColor; this.blockMargin = builder.blockMargin; @@ -169,6 +177,7 @@ public class SpannableTheme { this.tableBorderColor = builder.tableBorderColor; this.tableBorderWidth = builder.tableBorderWidth; this.tableOddRowBackgroundColor = builder.tableOddRowBackgroundColor; + this.taskListDrawable = builder.taskListDrawable; } @@ -363,6 +372,11 @@ public class SpannableTheme { paint.setStyle(Paint.Style.FILL); } + @NonNull + public Drawable getTaskListDrawable() { + return taskListDrawable; + } + public static class Builder { private int linkColor; @@ -386,6 +400,7 @@ public class SpannableTheme { private int tableBorderColor; private int tableBorderWidth; private int tableOddRowBackgroundColor; + private Drawable taskListDrawable; Builder() { } @@ -412,6 +427,7 @@ public class SpannableTheme { this.tableBorderColor = theme.tableBorderColor; this.tableBorderWidth = theme.tableBorderWidth; this.tableOddRowBackgroundColor = theme.tableOddRowBackgroundColor; + this.taskListDrawable = theme.taskListDrawable; } public Builder linkColor(@ColorInt int linkColor) { @@ -519,6 +535,11 @@ public class SpannableTheme { return this; } + public Builder taskListDrawable(@NonNull Drawable taskListDrawable) { + this.taskListDrawable = taskListDrawable; + return this; + } + public SpannableTheme build() { return new SpannableTheme(this); } diff --git a/library/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java b/library/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java new file mode 100644 index 00000000..f53931a1 --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/spans/TaskListDrawable.java @@ -0,0 +1,177 @@ +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; + +@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 both 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); + } + } +} diff --git a/library/src/main/java/ru/noties/markwon/spans/TaskListSpan.java b/library/src/main/java/ru/noties/markwon/spans/TaskListSpan.java new file mode 100644 index 00000000..af647558 --- /dev/null +++ b/library/src/main/java/ru/noties/markwon/spans/TaskListSpan.java @@ -0,0 +1,69 @@ +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; + +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(); + } + + @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 int save = c.save(); + try { + + final int width = theme.getBlockMargin(); + final int height = bottom - top; + + final Drawable drawable = theme.getTaskListDrawable(); + + 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); + } + } +} diff --git a/settings.gradle b/settings.gradle index 59f2e834..4baa558b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':library', ':library-image-loader', ':library-view' +include ':app', ':library', ':library-image-loader', ':library-view', ':library-task-list'